├── .env ├── .env.production ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── resumeData.json ├── templates.json └── vite.svg ├── src ├── App.vue ├── api │ └── qwenAPI.ts ├── assets │ ├── fonts │ │ └── zql.woff2 │ ├── icons │ │ ├── 1.svg │ │ ├── ai.svg │ │ ├── dark.svg │ │ ├── example.svg │ │ ├── resume.svg │ │ ├── setting.svg │ │ ├── templateStore.svg │ │ └── white.svg │ ├── imgs │ │ ├── loading.gif │ │ └── logo.png │ ├── styles │ │ ├── dark.css │ │ └── theme.css │ └── vue.svg ├── components │ ├── Header │ │ └── index.vue │ ├── SvgIcon.vue │ ├── ThemeSwitcher │ │ └── index.vue │ └── narrow │ │ └── index.vue ├── data │ └── resumeDataTemplate.ts ├── directives │ └── lazyLoad.ts ├── env.d.ts ├── main.ts ├── router │ └── index.ts ├── store │ ├── index.ts │ ├── useResumeStore.ts │ └── useSettingsStore.ts ├── template │ ├── dev │ │ ├── config.json │ │ ├── index.vue │ │ └── preview.jpg │ ├── templateA │ │ ├── config.json │ │ ├── index.vue │ │ └── preview.jpg │ ├── templateB │ │ ├── config.json │ │ ├── index.vue │ │ └── preview.jpg │ ├── templateC │ │ ├── config.json │ │ ├── index.vue │ │ └── preview.jpg │ └── templateD │ │ ├── config.json │ │ ├── index.vue │ │ └── preview.jpg ├── types │ ├── aiDialogue.d.ts │ ├── color.d.ts │ ├── components.d.ts │ ├── html2pdf.d.ts │ ├── resume.d.ts │ ├── svg.d.ts │ └── template.d.ts ├── utils │ ├── colorUtils.ts │ └── getTemplates.ts ├── views │ ├── 404.vue │ ├── aiDeep │ │ ├── components │ │ │ ├── AIChat.vue │ │ │ └── userInput.vue │ │ └── index.vue │ ├── coding.vue │ ├── resume │ │ ├── components │ │ │ ├── AIEnhancePopover.vue │ │ │ ├── education.vue │ │ │ ├── honor.vue │ │ │ ├── personalInfo.vue │ │ │ ├── project.vue │ │ │ ├── resumeEdit.vue │ │ │ ├── resumePreview.vue │ │ │ ├── selfEvaluation.vue │ │ │ ├── skill.vue │ │ │ └── workExperience.vue │ │ ├── index.vue │ │ └── styles │ │ │ └── styles.css │ ├── resumeDesign │ │ └── index.vue │ ├── setting │ │ └── index.vue │ └── template │ │ └── index.vue ├── vite-env.d.ts └── worker │ ├── aiWorker.ts │ └── workerPool.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── worker.js /.env: -------------------------------------------------------------------------------- 1 | VITE_API_URL = 'https://resumeai.404.pub/' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_API_URL = 'https://resumeai.404.pub/' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AIResume 2 | 3 | [![GitHub Stars](https://img.shields.io/github/stars/weidong-repo/AIResume)](https://github.com/weidong-repo/AIResume/stargazers) 4 | [![GitHub Forks](https://img.shields.io/github/forks/weidong-repo/AIResume)](https://github.com/weidong-repo/AIResume/network/members) 5 | [![GitHub Issues](https://img.shields.io/github/issues/weidong-repo/AIResume)](https://github.com/weidong-repo/AIResume/issues) 6 | [![GitHub Solved Issues](https://img.shields.io/github/issues-closed/weidong-repo/AIResume)](https://github.com/weidong-repo/AIResume/issues?q=is%3Aissue+is%3Aclosed) 7 | [![GitHub Last Commit](https://img.shields.io/github/last-commit/weidong-repo/AIResume)](https://github.com/weidong-repo/AIResume/commits/main) 8 | 9 | ## 📌 项目介绍 10 | 11 | **AIResume** 是一个开源的简历制作平台,帮助用户轻松创建专业简历,融合 AI 技术,辅助用户润色简历。我们欢迎对前端技术感兴趣的朋友参与 **`模板开发`**! 12 | 13 | - **技术栈**:Vue 3 + Vite + TypeScript + Ant Design Vue 14 | - **项目预览**:[AIResume 预览地址](https://resume.404.pub/) (部署于 Cloudflare Pages) 15 | 16 | ## 🎨 项目预览 17 | 18 | 编辑简历 19 | 20 | ![image-20250222224820478](https://img.fish9.cn/blog-img/2023/image-20250222224820478.png) 21 | 22 | 简历市场 23 | 24 | ![image-20250222224844722](https://img.fish9.cn/blog-img/2023/image-20250222224844722.png) 25 | 26 | AI模拟拷打: 27 | 28 | ![img](https://img.fish9.cn/blog-img/2023/image-20250226124049111.png) 29 | 30 | AI润色 31 | 32 | ![image-20250222224945177](https://img.fish9.cn/blog-img/2023/image-20250222224945177.png) 33 | 34 | 简历高度自定义配置” 35 | 36 | ![image-20250310231433143](https://img.fish9.cn/blog-img/2023/image-20250310231433143.png) 37 | 38 | ## 🚀 快速开始 39 | 40 | ### 1️⃣ 运行环境要求 41 | 42 | - **Node.js**:18+ 43 | 44 | ### 2️⃣ 克隆并安装依赖 45 | 46 | ```bash 47 | git clone https://github.com/weidong-repo/AIResume.git 48 | cd AIResume 49 | npm install 50 | ``` 51 | 52 | ### 3️⃣ 运行项目 53 | 54 | ```bash 55 | npm run dev 56 | ``` 57 | 58 | ## 🔥想自己部署同样的项目? 59 | 60 | 点击此处链接,[发行版本](https://github.com/weidong-repo/AIResume/releases) 61 | 找到最新版本的release.zip 62 | 然后把release.zip解压部署到您网站根目录上,访问域名即可 63 | 64 | ## 🔥欢迎有前端能力的朋友开发简历模板加入项目 65 | 66 | 简历模板开发方式: 67 | 1. 复制一份`/template/dev`目录,然后按照里面的数据挂载到前端即可。 68 | 2. 然后完善您模板目录下的`config.json`和`preview.jpg`(注意,config.json中的id务必是唯一值) 69 | 3. 最后,请在`/public/templates.json`文件中加上您开发的模板信息(直接复制`config.json`的即可!) 70 | 71 | 72 | 73 | ## 🌍 使用 Cloudflare Worker 进行 API 反向代理 74 | 75 | 本项目可使用 **Cloudflare Worker** 进行反向代理,以解决跨域问题。例如,针对 **阿里云百炼 API**: 76 | 77 | 1. 将 `workers.js` 上传至 Cloudflare Worker 78 | 2. 配置密钥 `API_URL` 指向大模型 API 地址(本项目接口适配 OpenAI 兼容 API,如阿里云、DeepSeek 等) 79 | 80 | 示例(阿里云 API 地址): 81 | 82 | ```bash 83 | https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions 84 | ``` 85 | 86 | ✅ 兼容 OpenAI API 的大模型均可无缝切换! 87 | 88 | **只需更改 `API_URL` 和 API Key,即可快速替换大模型!** 89 | 90 | ------ 91 | 92 | ## 🛠️ 其它反向代理方式 93 | 94 | 如果不想使用 Cloudflare Worker,也可以使用其他工具进行反代。**核心要求:只需解决跨域问题,即可流畅调用大模型 API!** 95 | 96 | ------ 97 | 98 | ## 🎯 已完成功能 99 | 100 | 主要功能: 101 | 102 | - ✅ 简历编辑,数据前端持久化 103 | - ✅ 简历导出为 PDF 104 | - ✅ 简历多模板,支持热插拔切换 105 | - ✅ 多套简历模板,支持前端开发者共创 106 | - ✅ 简历撰写的时候,AI可以进行润色 107 | - ✅ AI简历深挖 利用ai 基于单个项目或者经历的长对话对简历进行深度优化 108 | - ✅ AI模拟面试 针对单一项目或者经历对用户进行面试拷打 109 | 110 | 细节功能: 111 | 112 | - ✅ 模板主题色切换 113 | 114 | - ✅ 简历高度自定义,如段落间距、区块间距、字体大小、页边距等 115 | 116 | - ✅ 网站整体明/暗色切换 117 | 118 | - ✅ 右侧实时预览,自动同步用户编辑内容 119 | 120 | - ✅ 预览界面可拖动缩放简历 121 | 122 | - ✅ 导出 / 导入简历数据 123 | 124 | - ✅ 清空数据 125 | 126 | - ✅ 预填充示例数据 127 | 128 | - ✅ 一键填充虚假数据(快速查看简历效果) 129 | 130 | - ✅ 模板市场展示 131 | 132 | - ✅ 模板信息展示作者的昵称以及网站 133 | 134 | ------ 135 | 136 | ## 📝 待实现功能 137 | 138 | - [ ] **AI 面试官**(大模型读取简历,进行实时对话 / 语音通话) 139 | - [ ] **可视化简历设计**(支持非前端开发者用户拖拽设计简历) 140 | - [ ] **简历布局调整**(左侧拖拽调整右侧内容块顺序) 141 | - [ ] **数据隐藏功能**(支持隐藏部分信息,但数据仍保留) 142 | 143 | 🔥 **欢迎 Star & Fork 本项目,一起完善 AIResume!** 144 | 145 | # Stars历史记录 146 | 147 | [![Star History Chart](https://api.star-history.com/svg?repos=weidong-repo/AIResume&type=Date)](https://www.star-history.com/#weidong-repo/AIResume&Date) 148 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + Vue + TS 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend2", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --open", 8 | "build": "vue-tsc -b && vite build", 9 | "build:prod": "vue-tsc -b && vite build --mode production", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "ant-design-vue": "^4.2.6", 14 | "axios": "^1.7.9", 15 | "chroma-js": "^3.1.2", 16 | "crypto-js": "^4.2.0", 17 | "html2pdf.js": "^0.10.2", 18 | "marked": "^15.0.7", 19 | "pinia": "^3.0.1", 20 | "pinia-plugin-persistedstate": "^4.2.0", 21 | "vite-plugin-svg-icons": "^2.0.1", 22 | "vue": "^3.5.13", 23 | "vue-router": "^4.5.0" 24 | }, 25 | "devDependencies": { 26 | "@types/chroma-js": "^3.1.1", 27 | "@types/crypto-js": "^4.2.2", 28 | "@vitejs/plugin-vue": "^5.2.1", 29 | "@vue/tsconfig": "^0.7.0", 30 | "fast-glob": "^3.3.3", 31 | "typescript": "~5.7.2", 32 | "vite": "^6.1.0", 33 | "vue-tsc": "^2.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "202501", 4 | "name": "简洁模板-单栏", 5 | "description": "简约而不简单的单栏简历模板。", 6 | "folderPath": "templateA", 7 | "thumbnail": "preview.jpg", 8 | "author": "吃猫的鱼", 9 | "link": "https://github.com/weidong-repo" 10 | }, 11 | { 12 | "id": "202502", 13 | "name": "简洁模板-单栏", 14 | "description": "简约而不简单的简历模板。", 15 | "folderPath": "templateB", 16 | "thumbnail": "preview.jpg", 17 | "author": "AI", 18 | "link": "https://github.com/weidong-repo" 19 | }, 20 | { 21 | "id": "202503", 22 | "name": "简洁模板-双栏", 23 | "description": "AI构建的简洁双栏简历模板", 24 | "folderPath": "templateC", 25 | "thumbnail": "preview.jpg", 26 | "author": "AI", 27 | "link": "https://github.com/weidong-repo" 28 | }, 29 | { 30 | "id": "202504", 31 | "name": "简洁模板-单栏", 32 | "description": "AI构建的简洁单栏简历模板", 33 | "folderPath": "templateD", 34 | "thumbnail": "preview.jpg", 35 | "author": "AI", 36 | "link": "https://github.com/weidong-repo" 37 | }, 38 | { 39 | "id": "devlop", 40 | "name": "开发版本【开发用】", 41 | "description": "供开发者参考开发的模板-建议开发者开发的时候复制模板使用。建议把本模板放到最后", 42 | "folderPath": "dev", 43 | "thumbnail": "preview.jpg", 44 | "author": "吃猫的鱼", 45 | "link": "https://github.com/weidong-repo" 46 | } 47 | ] -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | 50 | 66 | -------------------------------------------------------------------------------- /src/api/qwenAPI.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "vue"; 2 | import { useSettingsStore } from "../store/useSettingsStore"; 3 | import type { DialogueHistory } from "../types/aiDialogue"; 4 | import { WorkerPool } from "../worker/workerPool"; 5 | 6 | //读取用户设置的API地址和API Key 7 | const settingsStore = useSettingsStore(); 8 | const API_URL = computed(() => settingsStore.aliApiUrl); 9 | const userApiKey = computed(() => settingsStore.aliApiKey); 10 | const model = computed(() => settingsStore.modelName); 11 | // 创建线程池,最多 4 个工作线程 12 | const workerPool = new WorkerPool(4); 13 | export async function sendToQwenAIDialogue(messages: DialogueHistory, 14 | onResponse: (responseText: string, isComplete: boolean) => void): Promise { 15 | workerPool.execute(messages, userApiKey.value, model.value, API_URL.value, onResponse); 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/fonts/zql.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/assets/fonts/zql.woff2 -------------------------------------------------------------------------------- /src/assets/icons/1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/ai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/resume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/templateStore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/imgs/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/assets/imgs/loading.gif -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /src/assets/styles/dark.css: -------------------------------------------------------------------------------- 1 | :root.dark { 2 | /* 基础颜色 */ 3 | --bg-color: #1A1A1A; 4 | --bg-card-color: #363636; 5 | --text-color: #F5F5F7; 6 | --text-color2: #18181A; 7 | --primary-color: #656565; 8 | --primary-color-hover: #9c87fe; 9 | --primary-color-active: #d3cfff; 10 | --card-color: #656565; 11 | 12 | /* 色阶 */ 13 | --color-1: #0F0F10; 14 | /* 最深,用于主背景 */ 15 | --color-2: #1C1C1E; 16 | /* 次背景,如卡片、弹窗 */ 17 | --color-3: #2C2C2E; 18 | /* 边框、分割线 */ 19 | --color-4: #3A3A3C; 20 | /* 按钮背景、悬浮效果 */ 21 | --color-5: #4D4D4F; 22 | /* 图标、次要文本 */ 23 | --color-6: #636366; 24 | /* 普通文本、图标 */ 25 | --color-7: #8E8E93; 26 | /* 最亮,用于标题、强调文字 */ 27 | } 28 | 29 | :root[theme='dark'] { 30 | /* 聊天界面暗色变量 */ 31 | --chat-bg: var(--bg-color); 32 | --chat-user-bubble: var(--color-4); 33 | --chat-ai-bubble: var(--color-2); 34 | --chat-bubble-shadow: rgba(0, 0, 0, 0.2); 35 | --chat-input-bg: var(--color-2); 36 | --chat-border: var(--color-3); 37 | --chat-input-text: var(--text-color); 38 | --chat-placeholder: var(--color-6); 39 | } 40 | 41 | body { 42 | transition: background-color 0.3s, color 0.3s; 43 | background-color: var(--bg-color); 44 | color: var(--text-color); 45 | font-family: 'zql'; 46 | transition: all 0.3s; 47 | box-sizing: border-box; 48 | } -------------------------------------------------------------------------------- /src/assets/styles/theme.css: -------------------------------------------------------------------------------- 1 | /* 引入字体文件 */ 2 | @font-face { 3 | font-family: 'zql'; 4 | src: url('@/assets/fonts/zql.woff2') format('woff2') 5 | } 6 | 7 | :root { 8 | /* 基础颜色 */ 9 | --bg-color: #F3F0FE; 10 | --bg-card-color: #ffffff; 11 | --text-color: #000000; 12 | --text-color2: #ffffff; 13 | --primary-color: #672DEA; 14 | --primary-color-hover: #581FD2; 15 | --primary-color-active: #4A1BB0; 16 | --card-color: white; 17 | 18 | /* 色阶 */ 19 | --color-1: #1A0740; 20 | /* 最深,用于标题、文本强调 */ 21 | --color-2: #310A72; 22 | /* 深色背景、边框 */ 23 | --color-3: #4A1BBC; 24 | /* 主要按钮、链接 */ 25 | --color-4: #5E21E0; 26 | /* 较亮的按钮、悬浮效果 */ 27 | --color-5: #713af4; 28 | /* 次级按钮、图标 */ 29 | --color-6: #9c87fe; 30 | /* 较亮文本、次级背景 */ 31 | --color-7: #d3cfff; 32 | /* 最亮,用于背景高亮、阴影 */ 33 | 34 | /* 聊天界面变量 */ 35 | --chat-bg: #f7f7f9; 36 | --chat-user-bubble: var(--color-4); 37 | --chat-ai-bubble: var(--bg-card-color); 38 | --chat-bubble-shadow: rgba(0, 0, 0, 0.05); 39 | --chat-input-bg: var(--bg-card-color); 40 | --chat-border: #e8e8e8; 41 | --chat-input-text: var(--text-color); 42 | --chat-placeholder: #999999; 43 | } 44 | 45 | body { 46 | transition: background-color 0.3s, color 0.3s; 47 | background-color: var(--bg-color); 48 | color: var(--text-color); 49 | font-family: 'zql'; 50 | transition: all 0.3s; 51 | box-sizing: border-box; 52 | } -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 70 | 71 | 105 | -------------------------------------------------------------------------------- /src/components/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/narrow/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /src/data/resumeDataTemplate.ts: -------------------------------------------------------------------------------- 1 | export const resumeTemplate = { 2 | // 基本信息 3 | personalInfo: { 4 | name: '', // 姓名 5 | gender: '', // 性别 6 | phone: '', // 联系电话 7 | email: '', // 电子邮箱 8 | university: '', // 所在大学 9 | politicalStatus: '', // 政治面貌 10 | website: '', // 个人网站 11 | avatar: '', // 头像 12 | major: '', // 专业 13 | age: '', // 年龄 14 | applicationPosition: '' // 申请职位 15 | }, 16 | 17 | // 教育经历 18 | education: [ 19 | { 20 | id: 1, // 唯一标识 21 | school: '', // 学校名称 22 | degree: '', // 学位 23 | major: '', // 专业 24 | startDate: '', // 开始时间 25 | endDate: '', // 结束时间1 26 | } 27 | ], 28 | 29 | // 工作经验 30 | workExperience: [ 31 | { 32 | id: 2, // 唯一标识 33 | company: '', // 公司名称 34 | position: '', // 职位 35 | startDate: '', // 开始时间 36 | endDate: '', // 结束时间 37 | description: '' // 描述 38 | } 39 | ], 40 | 41 | // 技能 42 | skills: [ 43 | { 44 | id: 3, // 唯一标识 45 | skillName: '' // 技能名称 46 | } 47 | ], 48 | 49 | // 项目经验 50 | projects: [ 51 | { 52 | id: 4, // 唯一标识 53 | projectName: '', // 项目名称 54 | role: '', // 担任角色 55 | startDate: '', // 开始时间 56 | endDate: '', // 结束时间 57 | description: '', // 项目描述 58 | briefIntroduction: '' // 项目简介 59 | } 60 | ], 61 | // 荣誉奖项 62 | honors: [ 63 | { 64 | id: 5, // 唯一标识 65 | honorName: '', // 荣誉名称 66 | date: '', // 获奖时间 67 | description: '' // 描述 68 | } 69 | ], 70 | // 自我评价 71 | summary: '', 72 | // 简历设置 73 | resumeSetting: { 74 | themeColor1: "#3653c9", // 主题颜色1(深色) 75 | themeColor2: "#bdcbff", // 主题颜色2(浅色) 76 | fontSize: 13, 77 | sectionSpacing: 1, 78 | paragraphSpacing: 0, 79 | currentTemplate: "202501", 80 | padding_top_bottom: 4, 81 | padding_left_right: 15 82 | } 83 | } as const; 84 | -------------------------------------------------------------------------------- /src/directives/lazyLoad.ts: -------------------------------------------------------------------------------- 1 | import myImage from '@/assets/imgs/loading.gif'; 2 | export default { 3 | mounted(el: HTMLImageElement, binding: any) { 4 | el.src = myImage 5 | const loadImage = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => { 6 | const entry = entries[0]; 7 | if (entry.isIntersecting) { 8 | el.src = binding.value; 9 | observer.unobserve(el); 10 | } 11 | }; 12 | const observer = new IntersectionObserver(loadImage, { root: null, threshold: 0.1 }); 13 | observer.observe(el); 14 | (el as any).__lazyObserver__ = observer; // 绑定 observer 到元素 15 | }, 16 | unmounted(el: HTMLImageElement) { 17 | if ((el as any).__lazyObserver__) { 18 | (el as any).__lazyObserver__.disconnect(); 19 | } 20 | } 21 | }; -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import Antd from 'ant-design-vue'; 4 | import App from './App.vue'; 5 | import 'ant-design-vue/dist/reset.css'; 6 | import 'virtual:svg-icons-register' 7 | import router from './router'; // 引入路由 8 | // 引入全局主题颜色 9 | import './assets/styles/theme.css'; 10 | import './assets/styles/dark.css'; 11 | // 持久化pinia 12 | import piniaPersist from 'pinia-plugin-persistedstate' 13 | import lazyLoad from './directives/lazyLoad'; 14 | // import { ConfigProvider } from 'ant-design-vue'; 15 | // svg插件配置代码 16 | // import 'virtual:svg-icons-register' 17 | 18 | const pinia = createPinia() 19 | pinia.use(piniaPersist) // 启用持久化功能 20 | 21 | 22 | const app = createApp(App); 23 | app.use(router); 24 | app.use(pinia); 25 | app.directive('lazy', lazyLoad); 26 | app.use(Antd); 27 | app.mount('#app'); 28 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; 2 | 3 | 4 | // 定义路由 5 | const routes: Array = [ 6 | { 7 | path: '/', 8 | name: 'resume', 9 | component: () => import('@/views/resume/index.vue'), 10 | meta: { title: 'AI简历 - 简历制作' } 11 | }, 12 | { 13 | path: '/resumeDesign', 14 | name: 'resumeDesign', 15 | component: () => import('@/views/resumeDesign/index.vue'), 16 | meta: { title: 'AI简历 - 简历设计' } 17 | }, 18 | { 19 | path: '/template', 20 | name: 'template', 21 | component: () => import('@/views/template/index.vue'), 22 | meta: { title: 'AI简历 - 简历模板' } 23 | }, 24 | { 25 | path: '/setting', 26 | name: 'setting', 27 | component: () => import('@/views/setting/index.vue'), 28 | meta: { title: 'AI简历 - 网站配置' } 29 | }, 30 | { 31 | path: '/aiDeep', 32 | name: 'aiDeep', 33 | component: () => import('@/views/aiDeep/index.vue'), 34 | meta: { title: 'AI简历 - AI深度交流', keepAlive: true } 35 | }, 36 | { 37 | path: '/:pathMatch(.*)*', 38 | name: 'NotFound', 39 | component: () => import('../views/404.vue') 40 | } 41 | ]; 42 | 43 | // 创建路由实例 44 | const router = createRouter({ 45 | history: createWebHistory(import.meta.env.BASE_URL), // 使用 HTML5 历史模式 46 | routes 47 | }); 48 | router.afterEach((to) => { 49 | document.title = (to.meta?.title as string) || '默认标题'; 50 | }); 51 | export default router; 52 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import { type App } from 'vue'; 3 | 4 | // 统一导出所有 Store 模块 5 | export { useResumeStore } from './useResumeStore'; 6 | export { useSettingsStore } from './useSettingsStore'; 7 | 8 | 9 | const pinia = createPinia(); 10 | export function setupStores(app: App) { 11 | app.use(pinia); 12 | } 13 | -------------------------------------------------------------------------------- /src/store/useResumeStore.ts: -------------------------------------------------------------------------------- 1 | // src/store/useResumeStore.ts 2 | import { defineStore } from 'pinia'; 3 | import { resumeTemplate } from '../data/resumeDataTemplate.ts'; 4 | import { message } from 'ant-design-vue'; 5 | // 定义类型 6 | import type { 7 | Education, Honor, PersonalInfo, 8 | Project, ResumeState, Skill, 9 | WorkExperience, ResumeSetting 10 | } from '../types/resume'; 11 | 12 | export const useResumeStore = defineStore('resume', { 13 | state: (): ResumeState => { 14 | // 从 localStorage 获取保存的数据 15 | const savedResumeData = localStorage.getItem('resumeData'); 16 | const savedCurrentId = localStorage.getItem('currentId'); 17 | const isFirstVisit = localStorage.getItem('isFirstVisit') === null; // 检查是否首次访问 18 | const currentId = savedCurrentId && !isNaN(Number(savedCurrentId)) 19 | ? Number(savedCurrentId) 20 | : 1; 21 | // 此处首先用模板数据初始化,然后再从 localStorage 中读取数据 22 | // 这样做的好处是,程序更新后,新增的字段会自动添加到模板中 23 | let resumeData = JSON.parse(JSON.stringify(resumeTemplate)); 24 | // 如果本地有保存过的数据,则合并覆盖模板数据 25 | if (savedResumeData) { 26 | try { 27 | const parsed = JSON.parse(savedResumeData); 28 | resumeData = { ...resumeData, ...parsed }; 29 | } catch (e) { 30 | console.error('解析 localStorage 失败:', e); 31 | } 32 | } 33 | // 如果是首次访问,标记并自动填充数据 34 | if (isFirstVisit) { 35 | localStorage.setItem('isFirstVisit', 'false'); 36 | } 37 | 38 | return { 39 | ...resumeData, 40 | currentId, 41 | isFirstVisit, // 添加到state中 42 | }; 43 | }, 44 | actions: { 45 | // 初始化时检查最大 id,后面新增的时候,id是递增的 46 | initializeCurrentId() { 47 | const allIds = [ 48 | ...this.education.map(item => item.id), 49 | ...this.workExperience.map(item => item.id), 50 | ...this.skills.map(item => item.id), 51 | ...this.projects.map(item => item.id), 52 | ...this.honors.map(item => item.id) 53 | ]; 54 | this.currentId = allIds.length > 0 ? Math.max(...allIds) + 1 : 1; 55 | localStorage.setItem('currentId', JSON.stringify(this.currentId)); 56 | }, 57 | // 导出数据 58 | exportData() { 59 | const dataStr = JSON.stringify(this.$state, null, 2); // 格式化 JSON 60 | const blob = new Blob([dataStr], { type: "application/json" }); 61 | const url = URL.createObjectURL(blob); 62 | const a = document.createElement("a"); 63 | a.href = url; 64 | a.download = "resume_data.json"; 65 | document.body.appendChild(a); 66 | a.click(); 67 | document.body.removeChild(a); 68 | URL.revokeObjectURL(url); 69 | }, 70 | // 导入数据 71 | importData(file: File) { 72 | const reader = new FileReader(); 73 | reader.onload = (event) => { 74 | try { 75 | const jsonData = JSON.parse(event.target?.result as string); 76 | this.$state = jsonData; // 直接覆盖 Pinia 状态 77 | this.saveToLocalStorage(); // 保存到 localStorage 78 | message.success('数据导入成功!'); 79 | } catch (error) { 80 | message.error('数据解析失败,请检查文件格式!'); 81 | } 82 | }; 83 | reader.readAsText(file); 84 | }, 85 | // 清空 86 | clearData() { 87 | // 重置数据 88 | Object.assign(this.$state, JSON.parse(JSON.stringify(resumeTemplate))); // 彻底重置数据 89 | this.currentId = 1; // 重置 ID 计数 90 | this.saveToLocalStorage(); // 更新 localStorage 91 | message.success('数据已清空'); 92 | }, 93 | // 自动填充数据 94 | async autoFillData() { 95 | try { 96 | const response = await fetch('/resumeData.json'); 97 | const data = await response.json(); 98 | this.$state = { ...data, isFirstVisit: false }; // 保持 isFirstVisit 99 | this.saveToLocalStorage(); 100 | message.success('数据已自动填充'); 101 | } catch (error) { 102 | message.error('加载数据失败'); 103 | } 104 | }, 105 | // 保存到 localStorage 106 | saveToLocalStorage() { 107 | localStorage.setItem('resumeData', JSON.stringify(this.$state)); 108 | localStorage.setItem('currentId', JSON.stringify(this.currentId)); 109 | }, 110 | 111 | // 通用新增方法 112 | addItem(list: T[], newItem: Omit) { 113 | const newEntry = { ...newItem, id: this.currentId++ } as T; 114 | list.push(newEntry); 115 | this.saveToLocalStorage(); 116 | }, 117 | // 简历设置内容 118 | updateResumeSetting(updatedSetting: Partial) { 119 | this.resumeSetting = { ...this.resumeSetting, ...updatedSetting }; 120 | this.saveToLocalStorage(); 121 | }, 122 | 123 | // 通用删除方法 124 | deleteItem(list: T[], id: number) { 125 | const index = list.findIndex(item => item.id === id); 126 | if (index !== -1) { 127 | list.splice(index, 1); 128 | this.saveToLocalStorage(); 129 | } 130 | }, 131 | 132 | // 通用更新方法 133 | updateItem(list: T[], updatedItem: T) { 134 | const index = list.findIndex(item => item.id === updatedItem.id); 135 | if (index !== -1) { 136 | list[index] = updatedItem; 137 | this.saveToLocalStorage(); 138 | } 139 | }, 140 | 141 | // 更新个人信息 142 | updatePersonalInfo(updatedInfo: Partial) { 143 | this.personalInfo = { ...this.personalInfo, ...updatedInfo }; 144 | this.saveToLocalStorage(); 145 | }, 146 | 147 | // 更新自我评价 148 | updateSummary(updatedSummary: string) { 149 | this.summary = updatedSummary; 150 | this.saveToLocalStorage(); 151 | }, 152 | 153 | // 新增教育经历 154 | addEducation(newItem: Omit) { 155 | this.addItem(this.education, newItem); 156 | }, 157 | 158 | // 删除教育经历 159 | deleteEducation(id: number) { 160 | this.deleteItem(this.education, id); 161 | }, 162 | 163 | // 更新教育经历 164 | updateEducation(updatedItem: Education) { 165 | this.updateItem(this.education, updatedItem); 166 | }, 167 | 168 | // 新增工作经验 169 | addWorkExperience(newItem: Omit) { 170 | this.addItem(this.workExperience, newItem); 171 | }, 172 | 173 | // 删除工作经验 174 | deleteWorkExperience(id: number) { 175 | this.deleteItem(this.workExperience, id); 176 | }, 177 | 178 | // 更新工作经验 179 | updateWorkExperience(updatedItem: WorkExperience) { 180 | this.updateItem(this.workExperience, updatedItem); 181 | }, 182 | 183 | // 新增技能 184 | addSkill(newItem: Omit) { 185 | this.addItem(this.skills, newItem); 186 | }, 187 | 188 | // 删除技能 189 | deleteSkill(id: number) { 190 | this.deleteItem(this.skills, id); 191 | }, 192 | 193 | // 更新技能 194 | updateSkill(updatedItem: Skill) { 195 | this.updateItem(this.skills, updatedItem); 196 | }, 197 | 198 | // 新增项目经验 199 | addProject(newItem: Omit) { 200 | this.addItem(this.projects, newItem); 201 | }, 202 | 203 | // 删除项目经验 204 | deleteProject(id: number) { 205 | this.deleteItem(this.projects, id); 206 | }, 207 | 208 | // 更新项目经验 209 | updateProject(updatedItem: Project) { 210 | this.updateItem(this.projects, updatedItem); 211 | }, 212 | 213 | // 新增荣誉奖项 214 | addHonor(newItem: Omit) { 215 | this.addItem(this.honors, newItem); 216 | }, 217 | 218 | // 删除荣誉奖项 219 | deleteHonor(id: number) { 220 | this.deleteItem(this.honors, id); 221 | }, 222 | 223 | // 更新荣誉奖项 224 | updateHonor(updatedItem: Honor) { 225 | this.updateItem(this.honors, updatedItem); 226 | }, 227 | 228 | loadFromLocalStorage() { 229 | const stored = localStorage.getItem('resumeStore'); 230 | if (stored) { 231 | this.$state = JSON.parse(stored); 232 | } 233 | }, 234 | 235 | // 初始化检查 236 | async initCheck() { 237 | if (this.isFirstVisit) { 238 | await this.autoFillData(); 239 | } 240 | } 241 | } 242 | }); 243 | 244 | 245 | -------------------------------------------------------------------------------- /src/store/useSettingsStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref, watch } from 'vue'; 3 | 4 | export const useSettingsStore = defineStore( 5 | 'settings', 6 | () => { 7 | // 是否为暗黑模式 8 | const isDark = ref(localStorage.getItem('theme') === 'dark'); 9 | // 主题颜色 10 | const theme = ref(isDark.value ? '#9c87fe' : '#672DEA'); 11 | // 阿里云apikey 12 | const aliApiKey = ref(''); 13 | // 阿里云调用接口 14 | const aliApiUrl = import.meta.env.VITE_API_URL; //这里用默认的 15 | //模型名称 16 | const modelName = ref('qwen-turbo'); 17 | // 切换主题 18 | const toggleTheme = () => { 19 | isDark.value = !isDark.value; 20 | theme.value = isDark.value ? '#9c87fe' : '#672DEA'; 21 | localStorage.setItem('theme', isDark.value ? 'dark' : 'light'); 22 | document.documentElement.classList.toggle('dark', isDark.value); 23 | }; 24 | 25 | // 初始化主题 26 | const initTheme = () => { 27 | isDark.value = localStorage.getItem('theme') === 'dark'; 28 | theme.value = isDark.value ? '#9c87fe' : '#672DEA'; 29 | document.documentElement.classList.toggle('dark', isDark.value); 30 | }; 31 | 32 | // 监听 isDark 变化,自动更新主题颜色和 class 33 | watch(isDark, (value) => { 34 | theme.value = value ? '#9c87fe' : '#672DEA'; 35 | document.documentElement.classList.toggle('dark', value); 36 | }); 37 | 38 | return { 39 | isDark, 40 | theme, 41 | toggleTheme, 42 | initTheme, 43 | aliApiKey, 44 | aliApiUrl, 45 | modelName 46 | }; 47 | }, 48 | { 49 | persist: true, // 开启持久化存储 50 | } 51 | ); 52 | -------------------------------------------------------------------------------- /src/template/dev/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "devlop", 3 | "name": "开发版本【开发用】", 4 | "description": "供开发者参考开发的模板-建议开发者开发的时候复制模板使用。", 5 | "folderPath": "dev", 6 | "thumbnail": "preview.jpg" 7 | } -------------------------------------------------------------------------------- /src/template/dev/index.vue: -------------------------------------------------------------------------------- 1 | 112 | 113 | 163 | 164 | 295 | -------------------------------------------------------------------------------- /src/template/dev/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/template/dev/preview.jpg -------------------------------------------------------------------------------- /src/template/templateA/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "202501", 3 | "name": "简洁模板-单栏", 4 | "description": "简约而不简单的单栏简历模板。", 5 | "folderPath": "templateA", 6 | "thumbnail": "preview.jpg", 7 | "author": "吃猫的鱼", 8 | "link": "https://github.com/weidong-repo" 9 | } -------------------------------------------------------------------------------- /src/template/templateA/index.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 169 | 170 | -------------------------------------------------------------------------------- /src/template/templateA/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/template/templateA/preview.jpg -------------------------------------------------------------------------------- /src/template/templateB/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "202502", 3 | "name": "简洁模板-单栏", 4 | "description": "简约而不简单的简历模板。", 5 | "folderPath": "templateB", 6 | "thumbnail": "preview.jpg", 7 | "author": "AI", 8 | "link": "https://github.com/weidong-repo" 9 | } -------------------------------------------------------------------------------- /src/template/templateB/index.vue: -------------------------------------------------------------------------------- 1 | 117 | 118 | 153 | 154 | 307 | -------------------------------------------------------------------------------- /src/template/templateB/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/template/templateB/preview.jpg -------------------------------------------------------------------------------- /src/template/templateC/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "202503", 3 | "name": "简洁模板-双栏", 4 | "description": "AI构建的简洁双栏简历模板", 5 | "folderPath": "templateC", 6 | "thumbnail": "preview.jpg", 7 | "author": "AI", 8 | "link": "https://github.com/weidong-repo" 9 | } -------------------------------------------------------------------------------- /src/template/templateC/index.vue: -------------------------------------------------------------------------------- 1 | 112 | 113 | 155 | 156 | 453 | -------------------------------------------------------------------------------- /src/template/templateC/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/template/templateC/preview.jpg -------------------------------------------------------------------------------- /src/template/templateD/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "202504", 3 | "name": "简洁模板-单栏", 4 | "description": "AI构建的简洁单栏简历模板", 5 | "folderPath": "templateD", 6 | "thumbnail": "preview.jpg", 7 | "author": "AI", 8 | "link": "https://github.com/weidong-repo" 9 | } -------------------------------------------------------------------------------- /src/template/templateD/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weidong-repo/AIResume/92d3089458676f31d1d397b7ecb2fdf21e488293/src/template/templateD/preview.jpg -------------------------------------------------------------------------------- /src/types/aiDialogue.d.ts: -------------------------------------------------------------------------------- 1 | export interface AIDialogue { 2 | /** 3 | * 角色 4 | * "user" 表示用户 5 | * "ai" 表示 AI 6 | * "system" 表示系统角色 7 | */ 8 | role: "user" | "assistant" | "system"; 9 | 10 | /** 11 | * 对话内容 12 | */ 13 | content: string; 14 | } 15 | 16 | // 对话历史(多轮对话) 17 | export type DialogueHistory = AIDialogue[]; 18 | -------------------------------------------------------------------------------- /src/types/color.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ColorShades { 3 | lighter: string; // 最亮色 4 | light: string; // 亮色 5 | base: string; // 基础色 6 | dark: string; // 深色 7 | darker: string; // 更深色 8 | deepest: string; // 最深色 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/types/components.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } -------------------------------------------------------------------------------- /src/types/html2pdf.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'html2pdf.js' { 2 | const html2pdf: any; 3 | export = html2pdf; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/resume.d.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from "../types/template"; 2 | export interface PersonalInfo { 3 | name: string; 4 | gender: string; 5 | phone: string; 6 | email: string; 7 | university: string; 8 | politicalStatus: string; 9 | website: string; 10 | avatar: string; 11 | major: string; 12 | applicationPosition: string; 13 | age: string; 14 | } 15 | 16 | export interface Education { 17 | id: number; 18 | school: string; 19 | degree: string; 20 | major: string; 21 | startDate: string; 22 | endDate: string; 23 | } 24 | 25 | export interface WorkExperience { 26 | id: number; 27 | company: string; 28 | position: string; 29 | startDate: string | null; 30 | endDate: string | null; 31 | description: string; 32 | } 33 | 34 | export interface Skill { 35 | id: number; 36 | skillName: string; 37 | } 38 | 39 | export interface Project { 40 | id: number; 41 | projectName: string; 42 | role: string; 43 | startDate: string; 44 | endDate: string; 45 | // 项目简介 46 | briefIntroduction: string; 47 | description: string; 48 | } 49 | 50 | export interface Honor { 51 | id: number; 52 | honorName: string; 53 | date: string; 54 | description: string; 55 | } 56 | 57 | 58 | export interface ResumeSetting { 59 | themeColor1: string; // 主题颜色1(深色) 60 | themeColor2: string; // 主题颜色2(浅色) 61 | fontSize: number; // 字体大小 62 | sectionSpacing: number; // 板块之间的间距 63 | paragraphSpacing: number; // 段落之间的间距 64 | currentTemplate: String; // 当前简历模板ID 65 | padding_left_right: number; // 左右边距 66 | padding_top_bottom: number; // 上下边距 67 | } 68 | 69 | export interface ResumeState { 70 | personalInfo: PersonalInfo; 71 | education: Education[]; 72 | workExperience: WorkExperience[]; 73 | skills: Skill[]; 74 | projects: Project[]; 75 | honors: Honor[]; 76 | summary: string; 77 | currentId: number; 78 | isFirstVisit: boolean; 79 | resumeSetting: ResumeSetting; 80 | } -------------------------------------------------------------------------------- /src/types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:svg-icons-register' { 2 | const content: any 3 | export default content 4 | } -------------------------------------------------------------------------------- /src/types/template.d.ts: -------------------------------------------------------------------------------- 1 | export interface Template { 2 | id: string; 3 | name: string; 4 | description?: string; 5 | folderPath?: String; 6 | thumbnail?: String; 7 | author: String; 8 | link: String; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/colorUtils.ts: -------------------------------------------------------------------------------- 1 | import chroma from "chroma-js"; 2 | import type { ColorShades } from '../types/color'; 3 | 4 | export function generateColorShades(baseColor: string): ColorShades { 5 | // 增加 lighter 的亮度,使用更大的 brighten 参数 6 | const colors = chroma.scale([chroma(baseColor).brighten(3), baseColor, chroma(baseColor).darken(2)]) 7 | .mode("lab") 8 | .colors(6); 9 | 10 | return { 11 | lighter: colors[0], // 更亮的颜色 12 | light: colors[1], 13 | base: colors[2], // 基础色 14 | dark: colors[3], 15 | darker: colors[4], // 深色 16 | deepest: colors[5] // 最深色 17 | }; 18 | } -------------------------------------------------------------------------------- /src/utils/getTemplates.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '../types/template'; 2 | 3 | export const getTemplates = async (): Promise => { 4 | try { 5 | const response = await fetch('/templates.json'); // 使用绝对路径 6 | if (!response.ok) { 7 | throw new Error('无法获取模板列表'); 8 | } 9 | return await response.json(); 10 | } catch (error) { 11 | console.error('获取模板列表失败:', error); 12 | return []; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/views/aiDeep/components/AIChat.vue: -------------------------------------------------------------------------------- 1 | 146 | 147 | 206 | 207 | -------------------------------------------------------------------------------- /src/views/aiDeep/components/userInput.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/aiDeep/index.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 84 | 85 | 110 | -------------------------------------------------------------------------------- /src/views/coding.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | 39 | 63 | -------------------------------------------------------------------------------- /src/views/resume/components/AIEnhancePopover.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 105 | 106 | 140 | -------------------------------------------------------------------------------- /src/views/resume/components/education.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 85 | 86 | -------------------------------------------------------------------------------- /src/views/resume/components/honor.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 80 | 81 | -------------------------------------------------------------------------------- /src/views/resume/components/personalInfo.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 131 | 132 | 182 | -------------------------------------------------------------------------------- /src/views/resume/components/project.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 103 | 104 | 127 | -------------------------------------------------------------------------------- /src/views/resume/components/resumeEdit.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /src/views/resume/components/resumePreview.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 373 | 374 | 378 | -------------------------------------------------------------------------------- /src/views/resume/components/selfEvaluation.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 47 | -------------------------------------------------------------------------------- /src/views/resume/components/skill.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 69 | 70 | -------------------------------------------------------------------------------- /src/views/resume/components/workExperience.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 95 | 96 | -------------------------------------------------------------------------------- /src/views/resume/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 57 | 58 | 81 | 82 | 144 | -------------------------------------------------------------------------------- /src/views/resume/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* 预览区域的样式 */ 2 | .preview { 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | background-color: var(--color-7); 8 | cursor: grab; 9 | user-select: none; 10 | } 11 | 12 | /* 当预览区域被点击时更改光标 */ 13 | .preview:active { 14 | cursor: grabbing; 15 | } 16 | 17 | .setting { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | z-index: 999; 23 | padding: 10px; 24 | box-shadow: 0 4px 18px rgb(167 167 167 / 40%); 25 | background-color: #ffffff3b; 26 | backdrop-filter: blur(2px) saturate(180%); 27 | display: flex; 28 | justify-content: flex-end; 29 | align-items: center; 30 | 31 | } 32 | 33 | /* 美化切换颜色 */ 34 | .changeColor { 35 | border: none; 36 | background-color: transparent; 37 | cursor: pointer; 38 | padding: 0; 39 | margin: 0 10px; 40 | width: 30px; 41 | height: 30px; 42 | border-radius: 50%; 43 | outline: none; 44 | transition: background-color 0.3s; 45 | } 46 | 47 | .resume-content { 48 | position: relative; 49 | z-index: 1; 50 | top: 50%; 51 | left: 50%; 52 | width: 794px; 53 | min-height: 1123px; 54 | background-color: white; 55 | color: black; 56 | box-shadow: 0 2px 16px rgba(0, 0, 0, 0.4); 57 | } -------------------------------------------------------------------------------- /src/views/resumeDesign/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | 26 | 50 | -------------------------------------------------------------------------------- /src/views/setting/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 52 | 53 | 114 | -------------------------------------------------------------------------------- /src/views/template/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 96 | 97 | 98 | 99 | 249 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/worker/aiWorker.ts: -------------------------------------------------------------------------------- 1 | // 发送请求的worker 2 | self.onmessage = async (event) => { 3 | const { taskId, messages, userApiKey, model, API_URL } = event.data; 4 | const requestData = { 5 | model, 6 | messages, 7 | stream: true, // 流式响应 8 | }; 9 | try { 10 | const response = await fetch(API_URL, { 11 | method: 'POST', 12 | headers: { 13 | 'Authorization': `Bearer ${userApiKey}`, // 认证信息 14 | 'Content-Type': 'application/json', 15 | }, 16 | body: JSON.stringify(requestData), 17 | }); 18 | if (response.status === 401) { 19 | self.postMessage({ taskId, isComplete: true, result: '认证失败,请检查 API Key 是否正确' }); 20 | return; 21 | } else if (!response.ok) { 22 | self.postMessage({ taskId, isComplete: true, result: `请求失败,错误码: ${response.status}` }); 23 | return; 24 | } 25 | if (!response.body) { 26 | self.postMessage({ taskId, isComplete: true, result: '服务器未返回流数据' }); 27 | return; 28 | } 29 | 30 | // 读取流式响应数据(sse) 31 | const reader = response.body.getReader(); 32 | const decoder = new TextDecoder(); 33 | let currentText = ''; //存结果 34 | 35 | while (true) { 36 | const { done, value } = await reader.read(); 37 | if (done) break; 38 | const chunk = decoder.decode(value); 39 | const lines = chunk.split('\n').filter(line => line.trim() !== ''); 40 | // 流式响应数据: 41 | // {"id":"****","choices":[{"delta":{"content":"我是","function_call":null,"refusal":null,"role":null,"tool_calls":null}, 42 | // "finish_reason":null,"index":0,"logprobs":null}],"created":1735113344, 43 | // "model":"qwen-plus","object":"chat.completion.chunk","service_tier":null, 44 | // "system_fingerprint":null,"usage":null} 45 | for (const line of lines) { 46 | if (line.startsWith('data: ')) { 47 | const jsonLine = line.slice(6).trim(); // 移除 `data: ` 前缀 48 | if (jsonLine === '[DONE]') { 49 | // 响应:data: [DONE] ,则结束 50 | self.postMessage({ taskId, isComplete: true, result: currentText }); 51 | return; 52 | } 53 | try { 54 | const parsedLine = JSON.parse(jsonLine); 55 | const deltaContent = parsedLine?.choices?.[0]?.delta?.content; 56 | if (deltaContent) { 57 | currentText += deltaContent; 58 | self.postMessage({ taskId, isComplete: false, result: currentText }); 59 | } 60 | } catch (err) { 61 | self.postMessage({ taskId, result: '解析流数据时出错,请稍后重试' }); 62 | } 63 | } 64 | } 65 | } 66 | } catch (error) { 67 | self.postMessage({ taskId, result: '请求失败,请稍后重试' }); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/worker/workerPool.ts: -------------------------------------------------------------------------------- 1 | import type { DialogueHistory } from "../types/aiDialogue"; 2 | 3 | export class WorkerPool { 4 | private workers: Worker[] = []; // Worker 线程池 5 | private queue: { 6 | taskId: number; 7 | messages: DialogueHistory; 8 | userApiKey: string; 9 | model: string; 10 | API_URL: string; 11 | onResponse: (responseText: string, isComplete: boolean) => void; 12 | }[] = []; // 任务队列 13 | private activeTasks: Map = new Map(); // 正在执行的任务 14 | private nextTaskId = 1; // 任务 ID 计数器 15 | 16 | // 构建,初始化线程池 17 | constructor(workerCount: number) { 18 | for (let i = 0; i < workerCount; i++) { 19 | const worker = new Worker(new URL("./aiWorker.ts", import.meta.url), { type: "module" }); 20 | this.workers.push(worker); 21 | worker.onmessage = (event) => { 22 | const { taskId, result, isComplete } = event.data; 23 | const task = this.queue.find((t) => t.taskId === taskId); 24 | if (task) { 25 | task.onResponse(result, isComplete); 26 | } 27 | if (isComplete) { 28 | this.activeTasks.delete(taskId); 29 | this.workers.push(worker); 30 | this.processQueue(); 31 | } 32 | }; 33 | worker.onerror = (error) => { 34 | console.error("Worker 处理任务失败:", error); 35 | this.workers.push(worker); 36 | this.processQueue(); 37 | }; 38 | } 39 | } 40 | 41 | /** 42 | * 新增任务 43 | * @param messages 对话历史 44 | * @param userApiKey API Key 45 | * @param model 选择的模型 46 | * @param API_URL 请求 API 地址 47 | * @param onResponse 结果回调 48 | */ 49 | execute( 50 | messages: DialogueHistory, 51 | userApiKey: string, 52 | model: string, 53 | API_URL: string, 54 | onResponse: (responseText: string, isComplete: boolean) => void 55 | ): void { 56 | const taskId = this.nextTaskId++; 57 | this.queue.push({ taskId, messages, userApiKey, model, API_URL, onResponse }); 58 | this.processQueue(); 59 | } 60 | 61 | /** 62 | * 处理任务队列 63 | */ 64 | private processQueue() { 65 | if (this.queue.length > 0 && this.workers.length > 0) { 66 | const worker = this.workers.pop()!; 67 | const { taskId, messages, userApiKey, model, API_URL, onResponse } = this.queue.shift()!; 68 | this.activeTasks.set(taskId, worker); 69 | try { 70 | // postMessage自动克隆出现问题,这里手动克隆 messages 71 | const clonedMessages = JSON.parse(JSON.stringify(messages)); 72 | worker.postMessage({ taskId, messages: clonedMessages, userApiKey, model, API_URL }); 73 | console.log(`任务${taskId}分配给 Worker:${worker}`); 74 | worker.onmessage = (event) => { 75 | const { taskId, result, isComplete } = event.data; 76 | onResponse(result, isComplete); 77 | if (isComplete) { 78 | this.activeTasks.delete(taskId); 79 | this.workers.push(worker); 80 | this.processQueue(); 81 | } 82 | }; 83 | } catch (error) { 84 | onResponse("数据传输失败", true); 85 | this.workers.push(worker); 86 | this.processQueue(); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * 终止所有 Worker 93 | */ 94 | terminate() { 95 | this.workers.forEach((worker) => worker.terminate()); 96 | this.workers = []; 97 | this.activeTasks.clear(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | 6 | /* Linting */ 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedSideEffectImports": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2020", // 或者 "esnext"、"node16" 等 4 | "moduleResolution": "node", // 推荐使用 node 模块解析方式 5 | "target": "esnext", 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | } 12 | }, 13 | "files": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.app.json" 17 | }, 18 | { 19 | "path": "./tsconfig.node.json" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import path from 'path' 4 | // 引入svg需要的插件 5 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' 6 | 7 | export default defineConfig({ 8 | plugins: [vue(), 9 | // 跨域问题 10 | createSvgIconsPlugin({ 11 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], 12 | symbolId: 'icon-[dir]-[name]', 13 | }), 14 | ], 15 | resolve: { 16 | alias: { 17 | '@': path.resolve(__dirname, './src'), 18 | }, 19 | }, 20 | base: './', 21 | mode: 'development', 22 | worker: { 23 | format: 'es', 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async fetch(request, env) { 3 | const allowHeaders = { 4 | "Access-Control-Allow-Origin": "*", 5 | "Access-Control-Allow-Methods": "POST, OPTIONS", 6 | "Access-Control-Allow-Headers": "Authorization, Content-Type", 7 | }; 8 | 9 | if (request.method === "OPTIONS") { 10 | return new Response(null, { status: 204, headers: allowHeaders }); 11 | } 12 | 13 | if (request.method !== "POST") { 14 | return new Response("Method Not Allowed", { status: 405, headers: allowHeaders }); 15 | } 16 | 17 | try { 18 | const url = env.API_URL || "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; // 允许环境变量覆盖默认 API 19 | const headers = { 20 | "Authorization": request.headers.get("Authorization"), 21 | "Content-Type": "application/json", 22 | }; 23 | 24 | let body = await request.text(); 25 | let parsedBody = JSON.parse(body); 26 | parsedBody.stream = true; // 启用流式传输 27 | 28 | const response = await fetch(url, { 29 | method: "POST", 30 | headers, 31 | body: JSON.stringify(parsedBody), 32 | }); 33 | 34 | if (!response.ok) { 35 | return new Response(JSON.stringify({ error: "请求失败", status: response.status }), { 36 | status: response.status, 37 | headers: { ...allowHeaders, "Content-Type": "application/json" }, 38 | }); 39 | } 40 | 41 | return new Response(response.body, { 42 | status: 200, 43 | headers: { 44 | ...allowHeaders, 45 | "Content-Type": "text/event-stream", 46 | "Cache-Control": "no-cache", 47 | "Connection": "keep-alive", 48 | }, 49 | }); 50 | } catch (error) { 51 | return new Response(JSON.stringify({ error: "服务器内部错误", details: error.toString() }), { 52 | status: 500, 53 | headers: { ...allowHeaders, "Content-Type": "application/json" }, 54 | }); 55 | } 56 | }, 57 | }; 58 | --------------------------------------------------------------------------------