├── .editorConfig ├── .env.production ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .shell ├── git-push.sh ├── gitee-push.sh └── test.sh ├── .vscode └── extensions.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── dist ├── css │ ├── aos-73168167.css │ ├── aos-73168167.css.gz │ ├── driver-4b398481.css │ ├── editor-30af71b7.css │ ├── element-plus-1386abe5.css │ ├── element-plus-1386abe5.css.gz │ ├── home-1da03570.css │ ├── index-eaa15cac.css │ ├── index-fb7690cc.css │ ├── nprogress-8b89e2e0.css │ ├── style-0d42effe.css │ ├── style-1a798512.css │ ├── style-29b41830.css │ ├── style-2b173bb8.css │ ├── style-335799a5.css │ ├── style-3484b480.css │ ├── style-3ad3a51e.css │ ├── style-3c8895da.css │ ├── style-3e737fc9.css │ ├── style-3ee738f2.css │ ├── style-411692ec.css │ ├── style-4203a5c6.css │ ├── style-49f1270e.css │ ├── style-4cff323d.css │ ├── style-5701618a.css │ ├── style-79ba219b.css │ ├── style-878fedaa.css │ ├── style-8b24a47e.css │ ├── style-8d29ed40.css │ ├── style-99277610.css │ ├── style-9c970f33.css │ ├── style-b4a2ad7e.css │ ├── style-b520db64.css │ ├── style-c7e73909.css │ ├── style-cba1d2d6.css │ ├── style-d57d18fe.css │ ├── style-de5a1fb2.css │ ├── style-e349495e.css │ ├── style-e571bc60.css │ ├── style-f269e66e.css │ ├── syntax-018b5d3c.css │ ├── syntax-018b5d3c.css.gz │ ├── template-035de545.css │ ├── typenet-48adb4ec.css │ └── update-7535b576.css ├── favicon.svg ├── index.html ├── jpeg │ └── qqgroup-ebb3dcc7.jpeg ├── jpg │ ├── alipay-5a066ea1.jpg │ ├── wechat-aac9c084.jpg │ └── wechat-d1899eda.jpg ├── js │ ├── @codemirror-4e89dd6b.js │ ├── @codemirror-4e89dd6b.js.gz │ ├── @ctrl-aa1b1e70.js │ ├── @ctrl-aa1b1e70.js.gz │ ├── @element-plus-a7a51df2.js │ ├── @element-plus-a7a51df2.js.gz │ ├── @floating-ui-c317a1d5.js │ ├── @lezer-868eaac8.js │ ├── @lezer-868eaac8.js.gz │ ├── @popperjs-535f1f87.js │ ├── @popperjs-535f1f87.js.gz │ ├── @vue-c6fcbc26.js │ ├── @vue-c6fcbc26.js.gz │ ├── @vueuse-63034ea9.js │ ├── @vueuse-63034ea9.js.gz │ ├── aos-80360ef4.js │ ├── aos-80360ef4.js.gz │ ├── async-validator-604317c1.js │ ├── async-validator-604317c1.js.gz │ ├── axios-93ecc7d0.js │ ├── axios-93ecc7d0.js.gz │ ├── codemirror-5e4ccb31.js │ ├── config-3bd082f3.js │ ├── config-3bd082f3.js.gz │ ├── crelt-8a41958c.js │ ├── dayjs-d3824421.js │ ├── dayjs-d3824421.js.gz │ ├── driver.js-dc4c3536.js │ ├── driver.js-dc4c3536.js.gz │ ├── editor-2e122ce7.js │ ├── editor-eb809c74.js │ ├── editor-eb809c74.js.gz │ ├── element-plus-37c3e502.js │ ├── element-plus-37c3e502.js.gz │ ├── escape-html-24561888.js │ ├── home-56403771.js │ ├── index-11794b13.js │ ├── index-c854a681.js │ ├── index-e5731265.js │ ├── index-f040c366.js │ ├── index-f040c366.js.gz │ ├── lodash-es-9d35530d.js │ ├── lodash-es-9d35530d.js.gz │ ├── lodash-unified-d151a101.js │ ├── markdown-transform-html-a1f02b0a.js │ ├── memoize-one-8443e73c.js │ ├── normalize-wheel-es-b3b926be.js │ ├── nprogress-6c9d9548.js │ ├── picture-verification-code-77c40e50.js │ ├── pinia-c946f11f.js │ ├── style-mod-b0eaf9b1.js │ ├── syntax-174ad51d.js │ ├── syntax-174ad51d.js.gz │ ├── template-6b029d61.js │ ├── typenet-5334c2a0.js │ ├── update-608b381d.js │ ├── vue-codemirror-d2f2654b.js │ ├── vue-d702f03a.js │ ├── vue-router-5174534a.js │ ├── vue-router-5174534a.js.gz │ └── w3c-keyname-2007ad7e.js ├── png │ └── wechat_group-ab9a254f.png └── svg │ ├── avataaars1-5c483e59.svg │ ├── avataaars2-6fc56c20.svg │ ├── avataaars3-2f506ef3.svg │ ├── avataaars4-e373fd76.svg │ ├── avataaars5-be0ba4f6.svg │ └── empty-88a93747.svg ├── docs ├── alipay.jpg ├── editor.webp ├── iconfont.webp ├── templates.webp ├── wechat.jpg └── wx-group.png ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.svg ├── src ├── App.vue ├── api │ ├── config.ts │ └── modules │ │ ├── comments.ts │ │ ├── community.ts │ │ ├── notification.ts │ │ ├── resume.ts │ │ ├── upload.ts │ │ └── user.ts ├── assets │ ├── global.scss │ ├── highlight.css │ ├── icon │ │ └── iconfont.json │ ├── img │ │ ├── qqgroup.jpeg │ │ ├── wechat.jpg │ │ └── wechat_group.png │ └── svg │ │ ├── avataaars1.svg │ │ ├── avataaars2.svg │ │ ├── avataaars3.svg │ │ ├── avataaars4.svg │ │ ├── avataaars5.svg │ │ └── empty.svg ├── common │ ├── global.ts │ ├── localstorage.ts │ ├── message.ts │ ├── nav │ │ ├── homeNav.ts │ │ ├── nav.ts │ │ └── outNav.ts │ └── tip.ts ├── components │ ├── browse-history │ │ ├── browseHistory.vue │ │ └── hook.ts │ ├── chat-room │ │ └── chat.vue │ ├── comment-reply-msg │ │ ├── crm.vue │ │ └── hook.ts │ ├── comments │ │ ├── comments.vue │ │ ├── hook.ts │ │ └── reply.vue │ ├── contact.vue │ ├── empty.vue │ ├── exportTotal.vue │ ├── hot-rank │ │ ├── hook.ts │ │ └── hotList.vue │ ├── logo.vue │ ├── menu-bar │ │ ├── menu-bar-item │ │ │ ├── hooks │ │ │ │ └── useScrollTop.ts │ │ │ └── menuBarItem.vue │ │ ├── menu-bar │ │ │ ├── MenuBar.vue │ │ │ └── hooks │ │ │ │ └── useMenuBarTitle.ts │ │ └── type.d.ts │ ├── navBar.vue │ ├── profile.vue │ ├── publish │ │ ├── hook.ts │ │ └── publish.vue │ ├── pwd-update │ │ ├── PWDUpdate.vue │ │ └── hook.ts │ ├── renderIcons.vue │ ├── reward.vue │ ├── themeToggle.vue │ ├── toast-modal │ │ └── toastModal.vue │ ├── userInfo.vue │ └── userTooltip.vue ├── layout │ ├── footer.vue │ ├── header │ │ ├── components │ │ │ ├── nav.vue │ │ │ ├── navMoblie.vue │ │ │ └── user.vue │ │ ├── header.vue │ │ └── hook.ts │ └── main.vue ├── main.ts ├── permission.ts ├── router │ ├── index.ts │ └── modules │ │ ├── community.ts.not │ │ ├── editor.ts │ │ ├── home.ts │ │ ├── recruit.ts.not │ │ ├── syntax.ts │ │ ├── template.ts │ │ └── update.ts ├── store │ ├── index.ts │ └── modules │ │ ├── editor.ts │ │ └── user.ts ├── templates │ ├── common.css │ ├── config.ts │ ├── iconfont.colors.css │ └── modules │ │ └── create │ │ └── style.scss ├── utils │ ├── date.ts │ ├── dom2md.ts │ ├── format.ts │ ├── index.ts │ ├── moduleCombine.ts │ └── uploader.ts ├── views │ ├── 404 │ │ └── index.vue │ ├── community │ │ ├── community.vue │ │ ├── components │ │ │ ├── community-left │ │ │ │ ├── communityLeft.vue │ │ │ │ ├── components │ │ │ │ │ ├── card │ │ │ │ │ │ ├── card.vue │ │ │ │ │ │ └── hook.ts │ │ │ │ │ └── notice │ │ │ │ │ │ └── notice.vue │ │ │ │ ├── constant.ts │ │ │ │ └── hook.ts │ │ │ └── community-right │ │ │ │ └── communityRight.vue │ │ └── views │ │ │ ├── detail │ │ │ ├── communityDetail.vue │ │ │ └── hook.ts │ │ │ └── editor │ │ │ ├── communityEditor.vue │ │ │ └── hook.ts │ ├── download │ │ └── index.vue │ ├── editor │ │ ├── components │ │ │ ├── editor │ │ │ │ ├── editorContainer.vue │ │ │ │ ├── hook.ts │ │ │ │ ├── md-editor │ │ │ │ │ ├── editor.vue │ │ │ │ │ ├── hook.ts │ │ │ │ │ └── md-editor.scss │ │ │ │ ├── rich-editor │ │ │ │ │ ├── editor.vue │ │ │ │ │ ├── hook.ts │ │ │ │ │ └── writable.scss │ │ │ │ └── toolbar │ │ │ │ │ ├── components │ │ │ │ │ ├── columnInput.vue │ │ │ │ │ ├── linkInput │ │ │ │ │ │ ├── hook.ts │ │ │ │ │ │ └── linkInput.vue │ │ │ │ │ └── tableInput.vue │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── hook.ts │ │ │ │ │ ├── mdTool.vue │ │ │ │ │ └── richTool.vue │ │ │ ├── guide │ │ │ │ ├── guide.ts │ │ │ │ └── popover.scss │ │ │ ├── header │ │ │ │ ├── header.vue │ │ │ │ ├── hook.ts │ │ │ │ └── nav.vue │ │ │ ├── preview │ │ │ │ └── render.vue │ │ │ └── tabbar │ │ │ │ ├── constant.ts │ │ │ │ ├── hook.ts │ │ │ │ └── tabbar.vue │ │ ├── editor.vue │ │ └── hook.ts │ ├── home │ │ ├── components │ │ │ ├── header.vue │ │ │ └── presentation.vue │ │ ├── home.vue │ │ └── hook.ts │ ├── recruit │ │ ├── README.md │ │ ├── hook.ts │ │ ├── recruit.vue │ │ └── recruits.ts │ ├── syntax │ │ ├── sources │ │ │ └── help.ts │ │ └── syntax.vue │ ├── template │ │ ├── components │ │ │ └── resumeCard.vue │ │ ├── constant.ts │ │ ├── hook.ts │ │ └── template.vue │ └── update │ │ ├── constant.ts │ │ └── update.vue └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── types └── type.d.ts └── vite.config.ts /.editorConfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_DROP_CONSOLE=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | node_modules 3 | dist 4 | src/assets/* 5 | build/* 6 | service 7 | **/*.scss 8 | **/*.css 9 | index.html 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | "vue/setup-compiler-macros": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:vue/vue3-essential", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "parser": "vue-eslint-parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module", 18 | "parser": "@typescript-eslint/parser", 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | }, 23 | "plugins": ["vue", "@typescript-eslint"], 24 | "rules": { 25 | "prettier/prettier": "error", 26 | "vue/no-v-html": "off", 27 | "vue/max-attributes-per-line": "off", 28 | "vue/multi-word-component-names": "off", 29 | "vue/no-multiple-template-root": "off", 30 | "vue/component-definition-name-casing": ["warn", "kebab-case"], 31 | "no-debugger": "off", 32 | "no-console": "off", 33 | "@typescript-eslint/no-explicit-any": ["off"] 34 | }, 35 | "globals": { 36 | "defineOptions": "writable" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | stats.html 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | node_modules 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 | .env 26 | src/templates/modules/[0-9]* 27 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | docs 4 | service -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "trailingComma": "none", 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "arrowParens": "avoid", 8 | "bracketSpacing": true, 9 | "endOfLine": "auto", 10 | "useTabs": false, 11 | "quoteProps": "as-needed", 12 | "jsxSingleQuote": false, 13 | "jsxBracketSameLine": false, 14 | "rangeStart": 0, 15 | "requirePragma": false, 16 | "insertPragma": false, 17 | "proseWrap": "preserve", 18 | "htmlWhitespaceSensitivity": "css" 19 | } -------------------------------------------------------------------------------- /.shell/git-push.sh: -------------------------------------------------------------------------------- 1 | echo "\033[32m <<<<<<<<< 正在拉取Github仓库远程代码... >>>>>>>>> \033[0m" 2 | git pull origin master 3 | 4 | echo "\033[32m <<<<<<<<< 正在添加文件... >>>>>>>>> \033[0m" 5 | git add . 6 | 7 | echo "\033[33m <<<<<<<<< 请填写备注信息(可为空: >>>>>>>>> \033[0m" 8 | read remarks 9 | if [ ! -n "$remarks" ] 10 | then 11 | remarks="deploy: 常规提交部署" 12 | fi 13 | 14 | git commit -m "$remarks" 15 | 16 | echo "\033[32m <<<<<<<<< 正在提交Github仓库代码... >>>>>>>>> \033[0m" 17 | git push origin master 18 | 19 | exit -------------------------------------------------------------------------------- /.shell/gitee-push.sh: -------------------------------------------------------------------------------- 1 | echo "\033[32m <<<<<<<<< 正在拉取Gitee仓库远程代码... >>>>>>>>> \033[0m" 2 | git pull gitee-origin master 3 | 4 | echo "\033[32m <<<<<<<<< 正在添加文件... >>>>>>>>> \033[0m" 5 | git add . 6 | 7 | echo "\033[33m <<<<<<<<< 请填写备注信息(可为空): >>>>>>>>> \033[0m" 8 | read remarks 9 | if [ ! -n "$remarks" ] 10 | then 11 | remarks="deploy: 常规提交部署" 12 | fi 13 | 14 | git commit -m "$remarks" 15 | 16 | echo "\033[32m <<<<<<<<< 正在提交Gitee仓库代码... >>>>>>>>> \033[0m" 17 | git push gitee-origin master 18 | 19 | exit -------------------------------------------------------------------------------- /.shell/test.sh: -------------------------------------------------------------------------------- 1 | git add . 2 | git commit -m 'test' 3 | git push test server-export 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar" 4 | ] 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.4.0 2 | 3 | `2023-04-23` 4 | 5 | - 支持两种编辑模式(内容模式 & markdown 模式),解决部分用户 markdown 语法门槛的问题. 内容模式目前处于试用阶段,可能出现一些问题(如果出现问题请转到 markdown 模式进行编辑后再重试). 6 | - 新增上边距调节器,你可以使用该工具调整简历中元素的上边距. 7 | - 调整 UI 布局. 8 | 9 | ## v1.3.4 10 | 11 | `2023-03-26` 12 | 13 | ### Feature 14 | 15 | - 新增简历模板 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use httpd:2.4-alpine image as the base image 2 | FROM httpd:2.4-alpine 3 | 4 | # Maintainer information 5 | MAINTAINER tanwenyang@aliyun.com 6 | 7 | # Copy the codecv file from the local directory to the /usr/local/apache2/htdocs/ directory inside the container 8 | COPY ./dist/ /usr/local/apache2/htdocs/ 9 | 10 | # Expose port 80 of the container and allow external access to this port 11 | EXPOSE 80 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | codecv © 2023 by coderlei 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 1. 授权许可 9 | 10 | 本许可协议(以下简称“协议”)允许任何个人或组织(以下统称“许可人”)根据以下条款和条件使用、修改和分发 "codecv" 作品(以下简称“作品”)。只要许可人遵循本协议的条款,他们可以自由使用和分发本作品。 11 | 12 | 2. 定义 13 | 14 | - “作品”指的是 "codecv",包括所有版本、派生作品和相关文件。 15 | - “许可人”指的是本协议下的任何使用、修改或分发 "codecv" 作品的个人或组织。 16 | - “非商业用途”指的是不以营利为目的,不涉及任何盈利、销售、广告或其他商业活动的使用。 17 | - “分发”包括复制、展示、传播、提供下载和传递 "codecv" 作品。 18 | 19 | 3. 使用和分发 20 | 21 | 3.1 非商业用途 22 | 许可人可以自由使用、修改和分发 "codecv" 作品,前提是他们不将作品用于商业用途。 23 | 24 | 3.2 相同方式共享 25 | 如果许可人创建了 "codecv" 作品的派生作品,他们必须将派生作品以相同的非商业用途许可协议分发,并在派生作品上明确指出本协议。 26 | 27 | 4. 署名 28 | 许可人可以自由使用、修改和分发 "codecv" 作品,前提是他们在作品中明确指出原作"coderlei",并提供指向本协议的链接。 29 | 30 | 5. 撤销许可 31 | 如果许可人未能遵守本协议的条款和条件,他们的权利将自动终止,并且不得再使用、修改或分发 "codecv" 作品。 32 | 33 | 6. 免责声明 34 | "codecv" 作品按“现状”提供,没有任何担保或保证。在适用法律允许的最大范围内,作者 "coderlei" 不对 "codecv" 作品产生的任何直接或间接损害承担任何责任。 35 | 36 | 7. 适用法律 37 | 本协议将根据中国法律的法律规定进行解释和执行。 38 | 39 | 8. 接受协议 40 | 使用、修改或分发 "codecv" 作品即表示许可人已阅读、理解并同意遵守本协议的所有条款和条件。 41 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # codecv 2 | 3 | This is a tool for creating resumes using `markdown`. It can convert your written `markdown` resume into a `PDF` format, supports multiple templates, and runs on love. 4 | 5 |
6 | 中文 | 7 | English 8 |
9 |
10 | 11 | [Online 1](https://codecv.top) [Online 2](https://codeleilei.gitee.io/markdown2pdf) 12 | 13 | > Declaration: This project is published on GitHub/Gitee, free and as an open source learning use, use spare time for continuous development, deployment please indicate the original author and the original warehouse address in a prominent place on the website, do not use for commercial purposes without the author's permission! 14 | 15 | ## 😄 Docker deploy 16 | 17 | You can directly run using the image I have already built. 18 | 19 | ```sh 20 | docker run -d -t -p 8080:80 --name codecv --restart=always docker.io/wenyang0/codecv:latest 21 | ``` 22 | 23 | Or, you can manually compile it yourself if you prefer. 24 | 25 | ```sh 26 | #clone the code 27 | git clone https://github.com/acmenlei/codecv.git 28 | 29 | #docker build 30 | cd codecv/ 31 | docker build -t codecv:v1 . 32 | 33 | #start server 34 | docker run -d -t -p 8080:80 --name codecv --restart=always codecv:v1 35 | ``` 36 | 37 | Finally, open your browser and access the service's address at http://serverIP:8080 38 | 39 | ## 🤩 Preview of the result 40 | 41 |

Resume template

42 | 43 | 模板 44 | 45 |

Resume editing and dark themes

46 | 47 | 编辑页 48 | 49 |

Built-in multiple vector ICONS

50 | 51 | 矢量图标 52 | 53 | ## ✊🏻 Features to be implemented 54 | 55 | [✓] Mobile device adaptation 56 | 57 | [✓] Improved content mode experience 58 | 59 | [✓] Template design (continuously updating... contributions to the repository templates are welcome) 60 | 61 | ## 🤔 Common issues 62 | 63 | [Please refer to the user guide for grammar-related questions.](https://codeleilei.gitee.io/markdown2pdf/#/syntax/helper) 64 | 65 | **Q**: Why export `PDF` after garbled code? 66 | 67 | **A**: It may be that the old font is cached, please click the reset resume content in the toolbar at the top of the preview to reset, of course, please ensure that you have saved the content before resetting. 68 | 69 | **Q**: Why does the export fail? 70 | 71 | **A**: At present, the service is deployed on the `Netlify Serverless` service, because it is a foreign server, access is easy to error, please try several times, of course, you can also use the local export `PDF` replacement. 72 | 73 | ## 🙏 Sponsor 74 | 75 | If you think this project is helpful to you and circumstances permit, you can give me a little support. In short, thank you very much for your support ~ 76 | 77 |
78 |
79 |

WeChat

80 | WeChat 81 |
82 |
83 |

Alipay

84 | Alipay 85 |
86 |
87 | 88 | ## License 89 | 90 | MIT © [Coderlei](./license) 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codecv 2 | 3 | 这是一款制作简历的工具,它可以将你编写的 `markdown` 简历转为 `PDF`,海量模板。 4 | 5 |
6 | 中文 | 7 | English 8 |
9 |
10 | 11 | [线上地址 1](https://codecv.top) [线上地址 2](https://codeleilei.gitee.io/markdown2pdf) 12 | 13 | > 声明:此项目发布于 GitHub/Gitee,免费且作为开源学习使用,使用业余时间进行持续开发,部署请在网站显眼位置注明原作者及原仓库地址,未经作者允许请勿用于商业用途! 14 | 15 | ## 😄 Docker 快速部署 16 | 17 | 你可以直接使用我已经构建好的镜像来运行 18 | 19 | ```sh 20 | docker run -d -t -p 8080:80 --name codecv --restart=always docker.io/wenyang0/codecv:latest 21 | ``` 22 | 23 | 或者,如果您愿意,也可以自己手动编译。 24 | 25 | ```sh 26 | #下载代码 27 | git clone https://github.com/acmenlei/codecv.git 28 | 29 | #docker 编译 30 | cd codecv/ 31 | docker build -t codecv:v1 . 32 | 33 | #启动服务 34 | docker run -d -t -p 8080:80 --name codecv --restart=always codecv:v1 35 | ``` 36 | 37 | 最后,打开你的浏览器访问服务的地址 http://serverIP:8080 即可(模板请自行编写与设计) 38 | 39 | 40 | 41 | ## 😄 在本地安装调试 42 | 43 | ```shell 44 | # 安装yarn包(有一个包需要使用yarn命令才能安装) 45 | npm i -g yarn 46 | 47 | #安装包 48 | yarn install 49 | 50 | #执行yarn install如果报错: yarn:无法加载文件 C\Users\talen\...\yarn.ps1 51 | # 打开Power Shell 52 | # 执行 set-ExecutionPolicy RemoteSigned 53 | set-ExecutionPolicy RemoteSigned 54 | #选择 A或者Y 解除脚本不信任 重新执行 yarn install 55 | 56 | #启动项目 57 | npm run dev 或 yarn run dev 58 | ``` 59 | 60 | ## 🤩 效果预览 61 | 62 |

简历模板

63 | 64 | 模板 65 | 66 |

简历编辑和暗黑主题

67 | 68 | 编辑页 69 | 70 |

内置多种矢量图标

71 | 72 | 矢量图标 73 | 74 | ## ✊🏻 待实现功能 75 | 76 | [✓] 移动端适配 77 | 78 | [✓] 内容模式体验优化 79 | 80 | [✓] 模板设计(持续更新... 欢迎为仓库贡献模板) 81 | 82 | ## 🤔 常见问题 83 | 84 | [语法问题请查看使用指南](https://codeleilei.gitee.io/markdown2pdf/#/syntax/helper) 85 | 86 | **Q**: 为什么导出 `PDF` 后乱码? 87 | 88 | **A**: 可能是缓存了旧的字体,请点击预览顶部工具栏中的重置简历内容进行重置,当然重置前请保证内容你已经保存 89 | 90 | **Q**: 为什么导出失败? 91 | 92 | **A**: 目前服务部署在 `netlify serverless` 服务上,因为是国外服务器,访问容易出错,请多尝试几遍,当然你也可以使用本地导出 `PDF` 替换 93 | 94 | ## 🙏 赞助 95 | 96 | 如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~ 97 | 98 |
99 |
100 |

WeChat

101 | 微信 102 |
103 |
104 |

Alipay

105 | 支付宝 106 |
107 |
108 | 109 | ## License 110 | 111 | MIT © [Coderlei](./license) 112 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /dist/css/aos-73168167.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/css/aos-73168167.css.gz -------------------------------------------------------------------------------- /dist/css/driver-4b398481.css: -------------------------------------------------------------------------------- 1 | .driver-active .driver-overlay,.driver-active *{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in .2s ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in .2s}.driver-popover{all:unset;box-sizing:border-box;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0;background-color:#fff}.driver-popover *{font-family:Helvetica Neue,Inter,ui-sans-serif,"Apple Color Emoji",Helvetica,Arial,sans-serif}.driver-popover-title{font:19px/normal sans-serif;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1;margin:0}.driver-popover-close-btn{all:unset;position:absolute;top:0;right:0;width:32px;height:28px;cursor:pointer;font-size:18px;font-weight:500;color:#d2d2d2;z-index:1;text-align:center;transition:color;transition-duration:.2s}.driver-popover-close-btn:hover,.driver-popover-close-btn:focus{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;font-weight:400;zoom:1}.driver-popover-footer{margin-top:15px;text-align:right;zoom:1;display:flex;align-items:center;justify-content:space-between}.driver-popover-progress-text{font-size:13px;font-weight:400;color:#727272;zoom:1}.driver-popover-footer button{all:unset;display:inline-block;box-sizing:border-box;padding:3px 7px;text-decoration:none;text-shadow:1px 1px 0 #fff;background-color:#fff;color:#2d2d2d;font:12px/normal sans-serif;cursor:pointer;outline:0;zoom:1;line-height:1.3;border:1px solid #ccc;border-radius:3px}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}:not(body):has(>.driver-active-element){overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}.driver-popover-footer button:hover,.driver-popover-footer button:focus{background-color:#f7f7f7}.driver-popover-navigation-btns{display:flex;flex-grow:1;justify-content:flex-end}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";position:absolute;border:5px solid #fff}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{left:100%;border-right-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-right{right:100%;border-left-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-top{top:100%;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.driver-popover-arrow-side-bottom{bottom:100%;border-left-color:transparent;border-top-color:transparent;border-right-color:transparent}.driver-popover-arrow-side-center{display:none}.driver-popover-arrow-side-left.driver-popover-arrow-align-start,.driver-popover-arrow-side-right.driver-popover-arrow-align-start{top:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-start,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start{left:15px}.driver-popover-arrow-align-end.driver-popover-arrow-side-left,.driver-popover-arrow-align-end.driver-popover-arrow-side-right{bottom:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-end,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end{right:15px}.driver-popover-arrow-side-left.driver-popover-arrow-align-center,.driver-popover-arrow-side-right.driver-popover-arrow-align-center{top:50%;margin-top:-5px}.driver-popover-arrow-side-top.driver-popover-arrow-align-center,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center{left:50%;margin-left:-5px}.driver-popover-arrow-none{display:none} 2 | -------------------------------------------------------------------------------- /dist/css/element-plus-1386abe5.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/css/element-plus-1386abe5.css.gz -------------------------------------------------------------------------------- /dist/css/index-fb7690cc.css: -------------------------------------------------------------------------------- 1 | .jufe[data-v-f11b4f13]{width:210mm;position:relative;min-height:295mm;z-index:1}.jufe[data-v-f11b4f13]:after{content:"";background:inherit;z-index:-2;position:fixed;top:0;left:0;width:120vw;height:120vh} 2 | -------------------------------------------------------------------------------- /dist/css/nprogress-8b89e2e0.css: -------------------------------------------------------------------------------- 1 | #nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}} 2 | -------------------------------------------------------------------------------- /dist/css/syntax-018b5d3c.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/css/syntax-018b5d3c.css.gz -------------------------------------------------------------------------------- /dist/css/template-035de545.css: -------------------------------------------------------------------------------- 1 | .resume-card[data-v-6174268f]{margin:5px 20px 80px 0;width:185px;height:240px;position:relative;text-align:center;transition:transform .4s;color:var(--font-color);cursor:pointer}.resume-card .template-hot[data-v-6174268f]{height:25px;background:var(--background);font-size:12px;top:-25px;position:absolute;text-align:left}.resume-card .template-hot i[data-v-6174268f]{color:#ff4500}.resume-card img[data-v-6174268f]{width:100%;height:100%;border-radius:5px}.resume-card .resume-card-mask[data-v-6174268f]{border-radius:5px;position:absolute;height:calc(100% + 25px);width:100%;top:0;left:0;display:none;background:rgba(0,0,0,.5)}.resume-card .resume-card-mask button[data-v-6174268f]{border-radius:3px;color:#fff;background:var(--theme)}.resume-card[data-v-6174268f]:hover{transform:translateY(10px)}.resume-card:hover .resume-card-mask[data-v-6174268f]{display:block}.resume-container[data-v-d057db7e]{max-width:var(--max-width);margin:20px auto}.resume-container .resume-notification[data-v-d057db7e]{padding-bottom:140px;position:sticky;top:80px;font-size:15px;line-height:28px}.resume-container .resume-notification strong[data-v-d057db7e]{display:inline-block;margin-bottom:10px;padding-bottom:5px;color:var(--theme)}.resume-container .resume-notification a[data-v-d057db7e]{color:#5e75eb}.resume-container .resume-hot-rank strong[data-v-d057db7e]{display:inline-block;color:var(--theme)}.resume-container .resume-hot-rank li[data-v-d057db7e]{font-size:14px;line-height:30px}.resume-container .resume-hot-rank li p[data-v-d057db7e]{max-width:135px}.resume-container .resume-hot-rank li sub[data-v-d057db7e]{font-weight:500;white-space:nowrap;color:#ff4500;text-align:right;flex-grow:1}.resume-container .resume-hot-rank li:nth-child(1) p span[data-v-d057db7e],.resume-container .resume-hot-rank li:nth-child(2) p span[data-v-d057db7e],.resume-container .resume-hot-rank li:nth-child(3) p span[data-v-d057db7e]{color:#ff4500}.resume-container .resume-left-container[data-v-d057db7e]{margin-right:20px}.resume-container .resume-left-container .resume-card-container[data-v-d057db7e]{display:grid;grid-template-columns:repeat(5,1fr)}.group[data-v-d057db7e]{align-items:center;gap:40px}@media screen and (max-width: 800px){.resume-right-container[data-v-d057db7e]{display:none}.resume-left-container[data-v-d057db7e]{margin-left:20px}} 2 | -------------------------------------------------------------------------------- /dist/css/typenet-48adb4ec.css: -------------------------------------------------------------------------------- 1 | .type-container{letter-spacing:2px;display:inline-block}@keyframes flicker-effect{0%{opacity:0}to{opacity:1}}.flicker{display:inline-block;animation:flicker-effect .3s alternate infinite} 2 | -------------------------------------------------------------------------------- /dist/css/update-7535b576.css: -------------------------------------------------------------------------------- 1 | .time-line[data-v-acd54dd6]{border-radius:10px;max-width:var(--max-width);margin:20px auto;background:var(--background);color:var(--font-color);padding:30px}.time-line h3[data-v-acd54dd6]{margin-bottom:20px} 2 | -------------------------------------------------------------------------------- /dist/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CodeCV简历 - 免费在线简历工具,5分钟打造你的金牌简历 6 | 7 | 11 | 12 | 13 | 17 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 36 | 56 | 57 | 58 |
59 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /dist/jpeg/qqgroup-ebb3dcc7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/jpeg/qqgroup-ebb3dcc7.jpeg -------------------------------------------------------------------------------- /dist/jpg/alipay-5a066ea1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/jpg/alipay-5a066ea1.jpg -------------------------------------------------------------------------------- /dist/jpg/wechat-aac9c084.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/jpg/wechat-aac9c084.jpg -------------------------------------------------------------------------------- /dist/jpg/wechat-d1899eda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/jpg/wechat-d1899eda.jpg -------------------------------------------------------------------------------- /dist/js/@codemirror-4e89dd6b.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@codemirror-4e89dd6b.js.gz -------------------------------------------------------------------------------- /dist/js/@ctrl-aa1b1e70.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@ctrl-aa1b1e70.js.gz -------------------------------------------------------------------------------- /dist/js/@element-plus-a7a51df2.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@element-plus-a7a51df2.js.gz -------------------------------------------------------------------------------- /dist/js/@floating-ui-c317a1d5.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/js/@lezer-868eaac8.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@lezer-868eaac8.js.gz -------------------------------------------------------------------------------- /dist/js/@popperjs-535f1f87.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@popperjs-535f1f87.js.gz -------------------------------------------------------------------------------- /dist/js/@vue-c6fcbc26.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@vue-c6fcbc26.js.gz -------------------------------------------------------------------------------- /dist/js/@vueuse-63034ea9.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/@vueuse-63034ea9.js.gz -------------------------------------------------------------------------------- /dist/js/aos-80360ef4.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/aos-80360ef4.js.gz -------------------------------------------------------------------------------- /dist/js/async-validator-604317c1.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/async-validator-604317c1.js.gz -------------------------------------------------------------------------------- /dist/js/axios-93ecc7d0.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/axios-93ecc7d0.js.gz -------------------------------------------------------------------------------- /dist/js/codemirror-5e4ccb31.js: -------------------------------------------------------------------------------- 1 | import{l as a,h as e,a as s,b as t,f as i,d as l,c as o,E as r,i as h,s as c,e as n,g as p,j as g,r as u,k as m,m as y,n as d,o as f,p as S,q as K,t as b,u as k,v,w,x,y as C}from"./@codemirror-4e89dd6b.js";const A=(()=>[a(),e(),s(),t(),i(),l(),o(),r.allowMultipleSelections.of(!0),h(),c(C,{fallback:!0}),n(),p(),g(),u(),m(),y(),d(),f.of([...S,...K,...b,...k,...v,...w,...x])])();export{A as b}; 2 | -------------------------------------------------------------------------------- /dist/js/config-3bd082f3.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/config-3bd082f3.js.gz -------------------------------------------------------------------------------- /dist/js/crelt-8a41958c.js: -------------------------------------------------------------------------------- 1 | function s(){var r=arguments[0];typeof r=="string"&&(r=document.createElement(r));var e=1,t=arguments[1];if(t&&typeof t=="object"&&t.nodeType==null&&!Array.isArray(t)){for(var n in t)if(Object.prototype.hasOwnProperty.call(t,n)){var o=t[n];typeof o=="string"?r.setAttribute(n,o):o!=null&&(r[n]=o)}e++}for(;e{!t.query.type||(m(String(t.query.type)),document.querySelector(".markdown-transform-html").innerHTML=o.nativeContent,o.resetNativeContent(),setTimeout(()=>{r(),n({name:String(t.query.type)}),window.print(),e.back()},100))}),_(()=>{localStorage.removeItem("download")}),(S,v)=>(c(),d("div",l))}});const z=f(y,[["__scopeId","data-v-f11b4f13"]]);export{z as default}; 2 | -------------------------------------------------------------------------------- /dist/js/index-f040c366.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/index-f040c366.js.gz -------------------------------------------------------------------------------- /dist/js/lodash-es-9d35530d.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/lodash-es-9d35530d.js.gz -------------------------------------------------------------------------------- /dist/js/lodash-unified-d151a101.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/js/memoize-one-8443e73c.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/js/normalize-wheel-es-b3b926be.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/js/picture-verification-code-77c40e50.js: -------------------------------------------------------------------------------- 1 | class g{constructor(...t){const[a=100,h=40]=t;this.width=a,this.height=h,this.size=16,this.code=[],this.canvas=document.createElement("canvas"),this.ctx=this.canvas.getContext("2d"),this.canvas.width=this.width,this.canvas.height=this.height,this.ctx.fillStyle=o(180,240),this.ctx.fillRect(0,0,this.width,this.height)}setBgColor(t){return this.bgColor=t,this}setBgImg(t){return this.bgImage=t,this}setWidth(t){return this.width=t,this}setHeight(t){return this.height=t,this}render(t){const{canvas:a,ctx:h}=this;h.clearRect(0,0,a.width,a.height),this.code=t?t.split(""):[],this.size=Math.min(16,this.height-14),this.width=Math.max(this.width,(this.size+5)*this.code.length),this.canvas.width=this.width,this.canvas.height=this.height,this.ctx.fillStyle=this.bgColor||o(180,240),this.ctx.fillRect(0,0,this.width,this.height),this.bgImage&&this.ctx.drawImage(this.bgImage,0,0,this.width,this.height),this.canvas.style.cursor="pointer",this.canvas.innerHTML="\u4F60\u7684\u6D4F\u89C8\u5668\u4E0D\u652F\u6301canvas",h.textBaseline="middle";const n=this.width/(this.code.length+1),s=this.height/2;return this.code.forEach((r,l)=>{const c=n*(l+.5);h.font=e(this.height/2,this.height)+"px SimHei",h.fillStyle=o(50,160),h.shadowOffsetX=e(-3,3),h.shadowOffsetY=e(-3,3),h.shadowBlur=e(-3,3),h.shadowColor="rgba(0, 0, 0, 0.3)",h.translate(c,s),h.fillText(r,0,0),h.translate(-c,-s)}),this.canvas.toDataURL("image/jpeg",1)}}function e(i,t){return Math.floor(Math.random()*(t-i)+i)}function o(i,t){return"rgb("+e(i,t)+", "+e(i,t)+", "+e(i,t)+")"}function d(i){i=i||4;const t=[],a=[];let h=[];for(let s=65;s<91;s++)t.push(String.fromCharCode(s));for(let s=97;s<123;s++)a.push(String.fromCharCode(s));h=new Array(10).fill("").map((s,r)=>`${r}`);const n=[...h,...t,...a];return new Array(i).fill("").map(()=>n[e(0,n.length-1)]).join("")}export{d as s,g as t}; 2 | -------------------------------------------------------------------------------- /dist/js/pinia-c946f11f.js: -------------------------------------------------------------------------------- 1 | import{ae as H,r as J,ad as k,g as Y,y as Z,w as G,J as $,i as L,aq as q,af as A,d as T,e as tt,n as et,K as st,h as nt}from"./@vue-c6fcbc26.js";var ct=!1;/*! 2 | * pinia v2.0.34 3 | * (c) 2023 Eduardo San Martin Morote 4 | * @license MIT 5 | */let B;const R=t=>B=t,D=Symbol();function C(t){return t&&typeof t=="object"&&Object.prototype.toString.call(t)==="[object Object]"&&typeof t.toJSON!="function"}var I;(function(t){t.direct="direct",t.patchObject="patch object",t.patchFunction="patch function"})(I||(I={}));function it(){const t=H(!0),c=t.run(()=>J({}));let s=[],e=[];const r=k({install(u){R(r),r._a=u,u.provide(D,r),u.config.globalProperties.$pinia=r,e.forEach(f=>s.push(f)),e=[]},use(u){return!this._a&&!ct?e.push(u):s.push(u),this},_p:s,_a:null,_e:t,_s:new Map,state:c});return r}const K=()=>{};function V(t,c,s,e=K){t.push(c);const r=()=>{const u=t.indexOf(c);u>-1&&(t.splice(u,1),e())};return!s&&T()&&tt(r),r}function g(t,...c){t.slice().forEach(s=>{s(...c)})}function x(t,c){t instanceof Map&&c instanceof Map&&c.forEach((s,e)=>t.set(e,s)),t instanceof Set&&c instanceof Set&&c.forEach(t.add,t);for(const s in c){if(!c.hasOwnProperty(s))continue;const e=c[s],r=t[s];C(r)&&C(e)&&t.hasOwnProperty(s)&&!L(e)&&!q(e)?t[s]=x(r,e):t[s]=e}return t}const ot=Symbol();function rt(t){return!C(t)||!t.hasOwnProperty(ot)}const{assign:y}=Object;function ut(t){return!!(L(t)&&t.effect)}function at(t,c,s,e){const{state:r,actions:u,getters:f}=c,a=s.state.value[t];let j;function b(){a||(s.state.value[t]=r?r():{});const v=st(s.state.value[t]);return y(v,u,Object.keys(f||{}).reduce((d,m)=>(d[m]=k(nt(()=>{R(s);const _=s._s.get(t);return f[m].call(_,_)})),d),{}))}return j=N(t,b,c,s,e,!0),j}function N(t,c,s={},e,r,u){let f;const a=y({actions:{}},s),j={deep:!0};let b,v,d=k([]),m=k([]),_;const p=e.state.value[t];!u&&!p&&(e.state.value[t]={}),J({});let O;function F(o){let n;b=v=!1,typeof o=="function"?(o(e.state.value[t]),n={type:I.patchFunction,storeId:t,events:_}):(x(e.state.value[t],o),n={type:I.patchObject,payload:o,storeId:t,events:_});const h=O=Symbol();et().then(()=>{O===h&&(b=!0)}),v=!0,g(d,n,e.state.value[t])}const W=u?function(){const{state:n}=s,h=n?n():{};this.$patch(S=>{y(S,h)})}:K;function z(){f.stop(),d=[],m=[],e._s.delete(t)}function M(o,n){return function(){R(e);const h=Array.from(arguments),S=[],w=[];function U(i){S.push(i)}function X(i){w.push(i)}g(m,{args:h,name:o,store:l,after:U,onError:X});let E;try{E=n.apply(this&&this.$id===t?this:l,h)}catch(i){throw g(w,i),i}return E instanceof Promise?E.then(i=>(g(S,i),i)).catch(i=>(g(w,i),Promise.reject(i))):(g(S,E),E)}}const Q={_p:e,$id:t,$onAction:V.bind(null,m),$patch:F,$reset:W,$subscribe(o,n={}){const h=V(d,o,n.detached,()=>S()),S=f.run(()=>G(()=>e.state.value[t],w=>{(n.flush==="sync"?v:b)&&o({storeId:t,type:I.direct,events:_},w)},y({},j,n)));return h},$dispose:z},l=$(Q);e._s.set(t,l);const P=e._e.run(()=>(f=H(),f.run(()=>c())));for(const o in P){const n=P[o];if(L(n)&&!ut(n)||q(n))u||(p&&rt(n)&&(L(n)?n.value=p[o]:x(n,p[o])),e.state.value[t][o]=n);else if(typeof n=="function"){const h=M(o,n);P[o]=h,a.actions[o]=n}}return y(l,P),y(A(l),P),Object.defineProperty(l,"$state",{get:()=>e.state.value[t],set:o=>{F(n=>{y(n,o)})}}),e._p.forEach(o=>{y(l,f.run(()=>o({store:l,app:e._a,pinia:e,options:a})))}),p&&u&&s.hydrate&&s.hydrate(l.$state,p),b=!0,v=!0,l}function lt(t,c,s){let e,r;const u=typeof c=="function";typeof t=="string"?(e=t,r=u?s:c):(r=t,e=t.id);function f(a,j){const b=Y();return a=a||b&&Z(D,null),a&&R(a),a=B,a._s.has(e)||(u?N(e,c,r,a):at(e,r,a)),a._s.get(e)}return f.$id=e,f}export{it as c,lt as d}; 6 | -------------------------------------------------------------------------------- /dist/js/style-mod-b0eaf9b1.js: -------------------------------------------------------------------------------- 1 | const y="\u037C",g=typeof Symbol>"u"?"__"+y:Symbol.for(y),m=typeof Symbol>"u"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),w=typeof globalThis<"u"?globalThis:typeof window<"u"?window:{};class x{constructor(e,l){this.rules=[];let{finish:u}=l||{};function n(t){return/^@/.test(t)?[t]:t.split(/,\s*/)}function s(t,i,h,T){let f=[],r=/^@(\w+)\b/.exec(t[0]),c=r&&r[1]=="keyframes";if(r&&i==null)return h.push(t[0]+";");for(let o in i){let a=i[o];if(/&/.test(o))s(o.split(/,\s*/).map(d=>t.map(S=>d.replace(/&/,S))).reduce((d,S)=>d.concat(S)),a,h);else if(a&&typeof a=="object"){if(!r)throw new RangeError("The value of a property ("+o+") should be a primitive value.");s(n(o),a,f,c)}else a!=null&&f.push(o.replace(/_.*/,"").replace(/[A-Z]/g,d=>"-"+d.toLowerCase())+": "+a+";")}(f.length||c)&&h.push((u&&!r&&!T?t.map(u):t).join(", ")+" {"+f.join(" ")+"}")}for(let t in e)s(n(t),e[t],this.rules)}getRules(){return this.rules.join(` 2 | `)}static newName(){let e=w[g]||1;return w[g]=e+1,y+e.toString(36)}static mount(e,l){(e[m]||new C(e)).mount(Array.isArray(l)?l:[l])}}let p=null;class C{constructor(e){if(!e.head&&e.adoptedStyleSheets&&typeof CSSStyleSheet<"u"){if(p)return e.adoptedStyleSheets=[p.sheet,...e.adoptedStyleSheets],e[m]=p;this.sheet=new CSSStyleSheet,e.adoptedStyleSheets=[this.sheet,...e.adoptedStyleSheets],p=this}else{this.styleTag=(e.ownerDocument||e).createElement("style");let l=e.head||e;l.insertBefore(this.styleTag,l.firstChild)}this.modules=[],e[m]=this}mount(e){let l=this.sheet,u=0,n=0;for(let s=0;s-1&&(this.modules.splice(i,1),n--,i=-1),i==-1){if(this.modules.splice(n++,0,t),l)for(let h=0;he in s?y(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var a=(s,e,t)=>(m(s,typeof e!="symbol"?e+"":e,t),t);function p(s){return new Promise(e=>setTimeout(e,s))}function C(s){return document.createTextNode(s)}function b(s,e){const t=document.createElement("span");return t.textContent=s,t.style.cssText=e,t}function N(){return document.createElement("br")}const d="INSERT",l="REMOVE",u="MOVE";function k(s){var t;const e=document.createElement("div");e.textContent="|",e.className="flicker",(t=s.typeContainer)==null||t.appendChild(e)}function T(s,e){const t=e.querySelector(".flicker");e.insertBefore(s,t)}function E(s,e){const t=e.querySelector(".flicker");e.insertBefore(t,s)}function w(s){var t;const e=s.typeContainer=document.createElement("div");e.className="type-container",e.style.cssText=s.options.style||"",(t=s.root)==null||t.appendChild(s.typeContainer)}function h(s){const e=Array.from(s.childNodes),t=[];for(const r of e)(r.nodeType===3||r.nodeType==1&&r.className!="flicker")&&t.push(r);return t}function o(s,e,t,r){return new Promise(n=>{setTimeout(()=>{switch(r){case d:T(e,s);break;case l:s.removeChild(e);break;case u:E(e,s);break}n(1)},t)})}const P={speed:100};class x{constructor(e,t=P){a(this,"typeContainer",document.body);a(this,"root");a(this,"callbacks",[]);a(this,"cursorPosition",0);this.el=e,this.options=t,this.root=document.querySelector(e),this.root}type(e,t){return this.callbacks.push(async()=>{for(let r=0,n=e.length;r{const r=h(this.typeContainer);let n=Math.min(e,this.cursorPosition);for(;n--;){const c=r[--this.cursorPosition],i=(t==null?void 0:t.speed)||this.options.speed;await o(this.typeContainer,c,i,l)}}),this}move(e=1,t){return this.callbacks.push(async()=>{const r=h(this.typeContainer),n=e>0?"forward":"backward",c={forward:{actualCharactersLength:Math.min(e,r.length-this.cursorPosition),add:1},backward:{actualCharactersLength:Math.min(-e,this.cursorPosition),add:-1}};for(;c[n].actualCharactersLength--;){const i=r[this.cursorPosition+=c[n].add],f=(t==null?void 0:t.speed)||this.options.speed;await o(this.typeContainer,i,f,u)}}),this}sleep(e){return this.callbacks.push(async()=>await p(e)),this}line(){return this.callbacks.push(async()=>{const e=N();this.typeContainer.insertBefore(e,this.typeContainer.childNodes[this.cursorPosition++])}),this}async start(){w(this),k(this);for(const e of this.callbacks)await e.apply(this)}}export{x as T}; 2 | -------------------------------------------------------------------------------- /dist/js/vue-d702f03a.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/js/vue-router-5174534a.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/js/vue-router-5174534a.js.gz -------------------------------------------------------------------------------- /dist/js/w3c-keyname-2007ad7e.js: -------------------------------------------------------------------------------- 1 | var o={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},t={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},n=typeof navigator<"u"&&/Chrome\/(\d+)/.exec(navigator.userAgent);typeof navigator<"u"&&/Gecko\/\d+/.test(navigator.userAgent);var d=typeof navigator<"u"&&/Mac/.test(navigator.platform),g=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),s=d||n&&+n[1]<57;for(var r=0;r<10;r++)o[48+r]=o[96+r]=String(r);for(var r=1;r<=24;r++)o[r+111]="F"+r;for(var r=65;r<=90;r++)o[r]=String.fromCharCode(r+32),t[r]=String.fromCharCode(r);for(var i in o)t.hasOwnProperty(i)||(t[i]=o[i]);function y(a){var f=s&&(a.ctrlKey||a.altKey||a.metaKey)||g&&a.shiftKey&&a.key&&a.key.length==1||a.key=="Unidentified",e=!f&&a.key||(a.shiftKey?t:o)[a.keyCode]||a.key||"Unidentified";return e=="Esc"&&(e="Escape"),e=="Del"&&(e="Delete"),e=="Left"&&(e="ArrowLeft"),e=="Up"&&(e="ArrowUp"),e=="Right"&&(e="ArrowRight"),e=="Down"&&(e="ArrowDown"),e}export{o as b,y as k,t as s}; 2 | -------------------------------------------------------------------------------- /dist/png/wechat_group-ab9a254f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/dist/png/wechat_group-ab9a254f.png -------------------------------------------------------------------------------- /docs/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/docs/alipay.jpg -------------------------------------------------------------------------------- /docs/editor.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/docs/editor.webp -------------------------------------------------------------------------------- /docs/iconfont.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/docs/iconfont.webp -------------------------------------------------------------------------------- /docs/templates.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/docs/templates.webp -------------------------------------------------------------------------------- /docs/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/docs/wechat.jpg -------------------------------------------------------------------------------- /docs/wx-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/docs/wx-group.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CodeCV简历 - 免费在线简历工具,5分钟打造你的金牌简历 6 | 7 | 11 | 12 | 13 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codecv", 3 | "private": true, 4 | "version": "1.4.3", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview", 10 | "push": "sh ./.shell/git-push.sh", 11 | "test": "sh ./.shell/test.sh", 12 | "gitee-push": "sh ./.shell/gitee-push.sh", 13 | "lint": "eslint src --ext .vue,.js,.ts,.jsx,.tsx --fix", 14 | "formater": "prettier --config .prettierrc --write ./**/**/*.{js,css,vue}" 15 | }, 16 | "dependencies": { 17 | "@codemirror/lang-css": "^6.1.1", 18 | "@codemirror/lang-markdown": "^6.0.4", 19 | "@codemirror/theme-one-dark": "^6.1.0", 20 | "@textbus/editor": "^3.0.0-alpha.32", 21 | "@vueuse/core": "^9.13.0", 22 | "ali-oss": "^6.17.1", 23 | "aos": "^2.3.4", 24 | "axios": "^1.2.0", 25 | "codemirror": "^6.0.1", 26 | "dayjs": "^1.11.6", 27 | "driver.js": "^1.2.1", 28 | "element-plus": "^2.2.19", 29 | "html2canvas": "^1.4.1", 30 | "jspdf": "^2.5.1", 31 | "markdown-transform-html": "^1.7.2", 32 | "nprogress": "^0.2.0", 33 | "picture-verification-code": "^1.1.0", 34 | "pinia": "^2.0.27", 35 | "socket.io-client": "^4.5.4", 36 | "typenet": "^1.0.3", 37 | "vue": "^3.2.41", 38 | "vue-codemirror": "^6.1.1", 39 | "vue-router": "^4.1.6", 40 | "vue3-emoji-picker": "^1.1.7" 41 | }, 42 | "devDependencies": { 43 | "@types/ali-oss": "^6.16.6", 44 | "@types/aos": "^3.0.4", 45 | "@types/node": "^18.11.7", 46 | "@types/nprogress": "^0.2.0", 47 | "@typescript-eslint/eslint-plugin": "^5.58.0", 48 | "@typescript-eslint/parser": "^5.58.0", 49 | "@vitejs/plugin-vue": "^3.2.0", 50 | "eslint": "^8.38.0", 51 | "eslint-config-prettier": "^8.8.0", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "eslint-plugin-vue": "^9.11.0", 54 | "prettier": "^2.8.7", 55 | "rollup-plugin-external-globals": "^0.8.0", 56 | "rollup-plugin-visualizer": "^5.9.2", 57 | "sass": "^1.55.0", 58 | "typescript": "^4.6.4", 59 | "unplugin-auto-import": "^0.11.4", 60 | "unplugin-vue-components": "^0.22.9", 61 | "vite": "^3.2.7", 62 | "vite-plugin-compression": "^0.5.1", 63 | "vite-plugin-eslint": "^1.8.1", 64 | "vite-plugin-imagemin": "^0.6.1", 65 | "vue-eslint-parser": "^9.1.1", 66 | "vue-tsc": "^1.0.9" 67 | }, 68 | "keywords": [ 69 | "markdown-resume", 70 | "markdown-pdf", 71 | "markdown-to-pdf", 72 | "markdown2pdf", 73 | "markdown-resume-pdf" 74 | ], 75 | "resolutions": { 76 | "bin-wrapper": "npm:bin-wrapper-china" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 5 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 22 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 15 | 26 | -------------------------------------------------------------------------------- /src/api/config.ts: -------------------------------------------------------------------------------- 1 | import axios, { ResponseType } from 'axios' 2 | 3 | import { Tip } from '@/common/tip' 4 | import { errorMessage } from '@/common/message' 5 | 6 | const service = axios.create({ 7 | baseURL: import.meta.env.VITE_BASE_URL as string, 8 | timeout: 5000, 9 | withCredentials: true, 10 | responseType: 'json' 11 | }) 12 | // 请求拦截 统一配置 13 | service.interceptors.request.use( 14 | config => { 15 | // showLoading() 16 | if (config.url === '/fileUpload/upload') { 17 | ;(config as any).headers['Content-Type'] = 'multipart/form-data' 18 | } 19 | return config 20 | }, 21 | err => { 22 | // hideLoading() 23 | errorMessage(err) 24 | return Promise.reject(new Error(err)) 25 | } 26 | ) 27 | // 统一在此处解构一层data 28 | service.interceptors.response.use( 29 | data => { 30 | return data.data 31 | }, 32 | err => { 33 | // hideLoading() 34 | errorMessage(err) 35 | return Promise.reject(new Error(err)) 36 | } 37 | ) 38 | 39 | // get method 40 | export function get(url: string, params: any = {}) { 41 | return new Promise((resolved, rejected) => { 42 | service 43 | .get(url, params) 44 | .then( 45 | resp => { 46 | resolved(resp) 47 | }, 48 | err => { 49 | errorMessage(Tip.NETWORK_ERROR) 50 | rejected(err) 51 | } 52 | ) 53 | .catch(err => { 54 | // 弹出错误提示 55 | rejected(err) 56 | errorMessage(Tip.NETWORK_ERROR) 57 | }) 58 | }) 59 | } 60 | // post method 61 | export function post(url: string, data: any = {}, type?: ResponseType) { 62 | return new Promise((resolved, rejected) => { 63 | service 64 | .post(url, data, { responseType: type || 'json' }) 65 | .then( 66 | resp => { 67 | resolved(resp) 68 | }, 69 | err => { 70 | errorMessage(Tip.NETWORK_ERROR) 71 | rejected(err) 72 | } 73 | ) 74 | .catch(err => { 75 | // 弹出错误提示 76 | errorMessage(Tip.NETWORK_ERROR) 77 | rejected(err) 78 | }) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/api/modules/comments.ts: -------------------------------------------------------------------------------- 1 | import { type IPublishComment, IPublishCommentReply } from '@@types/type' 2 | import { post } from '../config' 3 | 4 | export function publishComment(data: IPublishComment) { 5 | return post('/communityComment/publish', data) 6 | } 7 | 8 | export function publishCommentReply(data: IPublishCommentReply) { 9 | return post('/communityComment/reply', data) 10 | } 11 | 12 | export function removeComment(data: { commentId: number; articleId: number; level: number }) { 13 | return post('/communityComment/remove', data) 14 | } 15 | 16 | export function queryCommunityArticleCommentsById(data: { 17 | articleId: number 18 | pageSize: number 19 | pageNum: number 20 | }) { 21 | return post('/communityComment/queryCommentsByArticleId', data) 22 | } 23 | 24 | export function queryCommentPosition(data: { 25 | commentId: number 26 | pageSize: number 27 | articleId: number 28 | }) { 29 | return post('/communityComment/queryCommentPosition', data) 30 | } 31 | -------------------------------------------------------------------------------- /src/api/modules/community.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ICommunityArticle, 3 | ICommunityArticleUpdate, 4 | ICommunityCondition, 5 | ICommunityLike 6 | } from '@@types/type' 7 | import { post } from '../config' 8 | 9 | export function publishCommunity(data: ICommunityArticle) { 10 | return post('/community/publish', data) 11 | } 12 | 13 | export function updateCommunity(data: ICommunityArticleUpdate) { 14 | return post('/community/update', data) 15 | } 16 | 17 | export function removeCommunity(data: { articleId: number }) { 18 | return post('/community/remove', data) 19 | } 20 | 21 | export function queryCommunity(data: ICommunityCondition) { 22 | return post('/community/list', data) 23 | } 24 | 25 | export function queryCommunityHotRank(data: { 26 | start?: string 27 | end?: string 28 | requireCount: number 29 | }) { 30 | return post('/community/queryCommunityHotRank', data) 31 | } 32 | 33 | export function likeArticle(data: ICommunityLike) { 34 | return post('/community/like', data) 35 | } 36 | 37 | export function queryCommunityArticleById(data: { articleId: number }) { 38 | return post('/community/queryArticleById', data) 39 | } 40 | -------------------------------------------------------------------------------- /src/api/modules/notification.ts: -------------------------------------------------------------------------------- 1 | import { post } from '../config' 2 | 3 | export function queryNotification(data: { pageSize: number; pageNum: number; uid: number }) { 4 | return post('/notification/list', data) 5 | } 6 | 7 | export function updateNotificationState(data: { commentId: number }) { 8 | return post('/notification/read', data) 9 | } 10 | -------------------------------------------------------------------------------- /src/api/modules/resume.ts: -------------------------------------------------------------------------------- 1 | export interface IResumeConfig { 2 | content: string 3 | style: string 4 | link: string 5 | name: string 6 | type?: number 7 | } 8 | 9 | const UPSTASH_BASE_URL = import.meta.env.VITE_UPSTASH_BASE_URL as string 10 | 11 | export async function resumeExport(data: IResumeConfig) { 12 | const res = await fetch(import.meta.env.VITE_EXPORT_URL as string, { 13 | method: 'POST', 14 | body: JSON.stringify(data) 15 | // headers: { 16 | // 'Content-Type': 'application/json' 17 | // } 18 | }) 19 | return await res.json() 20 | } 21 | 22 | export function getExportCount() { 23 | return new Promise((resolve, reject) => { 24 | fetch(`${UPSTASH_BASE_URL}/get/count`, { 25 | headers: { 26 | Authorization: import.meta.env.VITE_UPSTASH_GET_TOKEN as string 27 | } 28 | }) 29 | .then(response => response.json()) 30 | .then(data => resolve(data.result)) 31 | .catch(reject) 32 | }) 33 | } 34 | 35 | export async function setExportCount() { 36 | let count: string 37 | try { 38 | count = (await getExportCount()) as string 39 | } catch { 40 | return Promise.resolve('获取失败...') 41 | } 42 | return new Promise(resolve => { 43 | fetch(`${UPSTASH_BASE_URL}/set/count/${parseInt(count) + 1}`, { 44 | headers: { 45 | Authorization: import.meta.env.VITE_UPSTASH_SET_TOKEN as string 46 | } 47 | }) 48 | .then(response => response.json()) 49 | .then(data => resolve(data.result)) 50 | .catch(resolve) 51 | }) 52 | } 53 | 54 | export async function getTemplateCondition() { 55 | const res = await fetch(`${UPSTASH_BASE_URL}/get/templateData`, { 56 | headers: { 57 | Authorization: import.meta.env.VITE_UPSTASH_GET_TOKEN as string 58 | } 59 | }) 60 | return await res.json() 61 | } 62 | 63 | export async function setTemplateCondition(params: { name: string }) { 64 | let data, 65 | templateData: { [key: string]: string } = {} 66 | try { 67 | data = await getTemplateCondition() 68 | } catch { 69 | return Promise.resolve({ msg: '获取模板数据失败...', result: null }) 70 | } 71 | if (data.result) { 72 | templateData = JSON.parse(data.result) 73 | } 74 | templateData[`t${params.name}`] = String(+(templateData[`t${params.name}`] || 0) + 1) 75 | const res = await fetch(`${UPSTASH_BASE_URL}/set/templateData`, { 76 | method: 'POST', 77 | body: JSON.stringify(templateData), 78 | headers: { 79 | Authorization: import.meta.env.VITE_UPSTASH_SET_TOKEN as string 80 | } 81 | }) 82 | return await res.json() 83 | } 84 | // 获取 Gitee 仓库 star 数量 85 | export function queryGiteeRepoStars() { 86 | return new Promise(resolve => { 87 | fetch(import.meta.env.VITE_GITEE_API_URL as string) 88 | .then(res => res.json()) 89 | .then(data => { 90 | // 获取仓库 star 数量 91 | resolve(data) 92 | }) 93 | .catch(() => resolve([])) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /src/api/modules/upload.ts: -------------------------------------------------------------------------------- 1 | import { post } from '../config' 2 | 3 | export function fileUpload(data: FormData) { 4 | return post('/fileUpload/upload', data) 5 | } 6 | 7 | export function fileMerge(data: { name: string; length: number }) { 8 | return post('/fileUpload/merge', data) 9 | } 10 | 11 | export function getToken() { 12 | return post('/fileUpload/getToken') 13 | } 14 | -------------------------------------------------------------------------------- /src/api/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { IUser, IUserInfo } from '@@types/type' 2 | import { post } from '../config' 3 | 4 | export function login(data: IUser) { 5 | return post('/user/login', data) 6 | } 7 | 8 | export function registerUser(data: IUser) { 9 | return post('/user/register', data) 10 | } 11 | 12 | export function updateUserInfo(data: IUserInfo) { 13 | return post('/user/update', data) 14 | } 15 | 16 | export function logout(data: { username: string }) { 17 | return post('/user/logout', data) 18 | } 19 | 20 | export function verify(data: { token: string; username: string }) { 21 | return post('/user/verify', data) 22 | } 23 | 24 | export function queryUserInfoById(data: { uid: number }) { 25 | return post('/user/queryUserById', data) 26 | } 27 | 28 | export function pwdUpdate(data: { nPassword: string; oPassword: string; username: string }) { 29 | return post('/user/pwdUpdate', data) 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/img/qqgroup.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/src/assets/img/qqgroup.jpeg -------------------------------------------------------------------------------- /src/assets/img/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/src/assets/img/wechat.jpg -------------------------------------------------------------------------------- /src/assets/img/wechat_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/src/assets/img/wechat_group.png -------------------------------------------------------------------------------- /src/common/global.ts: -------------------------------------------------------------------------------- 1 | import useUserStore from '@/store/modules/user' 2 | import { useDark, useToggle } from '@vueuse/core' 3 | import { ref, watchEffect } from 'vue' 4 | 5 | export function isLogin() { 6 | const { loginState } = useUserStore() 7 | return loginState.logined 8 | } 9 | 10 | export function useThemeConfig() { 11 | const isDark = useDark() 12 | const toggleTheme = useToggle(isDark) 13 | // #3f9eff 14 | watchEffect(() => { 15 | const theme = isDark.value ? '#5745c8' : '#ff7449', 16 | background = isDark.value ? '#282c34' : '#ffffff', 17 | fontColor = isDark.value ? '#eeeeee' : '#1e293b', 18 | strongColor = isDark.value ? '#ab3fb2' : '#f24672', 19 | toolbarBg = isDark.value ? '#282c34' : '#222222', 20 | bodyBackground = isDark.value ? '#1e2633' : '#f3f5f7', 21 | writableFontColor = isDark.value ? '#d1d1d1' : '#545a69', 22 | linearBGC = isDark.value ? background : '#fbe9db' 23 | 24 | document.body.style.setProperty('--theme', theme) 25 | document.body.style.setProperty('--background', background) 26 | document.body.style.setProperty('--font-color', fontColor) 27 | document.body.style.setProperty('--strong-color', strongColor) 28 | document.body.style.setProperty('--toolbar-bg', toolbarBg) 29 | document.body.style.setProperty('--body-background', bodyBackground) 30 | document.body.style.setProperty('--el-color-primary', theme) 31 | document.body.style.setProperty('--writable-font-color', writableFontColor) 32 | document.body.style.setProperty('--linear-background', linearBGC) 33 | }) 34 | 35 | return { 36 | toggleTheme, 37 | isDark 38 | } 39 | } 40 | 41 | export function useSwitch(initState?: boolean) { 42 | const open = ref(initState ?? false) 43 | 44 | function toggle() { 45 | open.value = !open.value 46 | } 47 | function setTure() { 48 | open.value = true 49 | } 50 | function setFalse() { 51 | open.value = false 52 | } 53 | return { 54 | open, 55 | toggle, 56 | setTure, 57 | setFalse 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/common/localstorage.ts: -------------------------------------------------------------------------------- 1 | type LocalStorageValue = { value: T; expires: number } 2 | 3 | export function setLocalStorage(key: string, value: unknown, expires: number = 1000 * 60 * 60 * 3) { 4 | const result: LocalStorageValue = { 5 | value, 6 | expires: Date.now() + expires 7 | } 8 | localStorage.setItem(key, JSON.stringify(result)) 9 | return true 10 | } 11 | 12 | export function getLocalStorage(key: string) { 13 | const currentTime = Date.now() 14 | 15 | const value = localStorage.getItem(key) 16 | if (!value) { 17 | return false 18 | } 19 | const result: LocalStorageValue = JSON.parse(value) 20 | 21 | // 如果过期了就删掉 22 | if (result.expires < currentTime) { 23 | localStorage.removeItem(key) 24 | return false 25 | } 26 | return result.value 27 | } 28 | 29 | export function removeLocalStorage(key: string) { 30 | if (!getLocalStorage(key)) { 31 | return false 32 | } 33 | localStorage.removeItem(key) 34 | return true 35 | } 36 | -------------------------------------------------------------------------------- /src/common/message.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | import 'element-plus/es/components/message/style/css' 3 | import { h } from 'vue' 4 | 5 | export function successMessage(message: string) { 6 | ElMessage({ 7 | showClose: true, 8 | message, 9 | type: 'success' 10 | }) 11 | } 12 | 13 | export function warningMessage(message: string) { 14 | ElMessage({ 15 | showClose: true, 16 | message, 17 | type: 'warning' 18 | }) 19 | } 20 | 21 | export function errorMessage(message: string) { 22 | ElMessage({ 23 | showClose: true, 24 | message, 25 | type: 'error' 26 | }) 27 | } 28 | 29 | export function showMessageVN(message: string, strong: string) { 30 | ElMessage({ 31 | message: h('p', null, [ 32 | h('span', null, message), 33 | h('strong', { style: 'color: teal; margin: 0 5px' }, strong) 34 | ]), 35 | offset: 60 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/common/nav/homeNav.ts: -------------------------------------------------------------------------------- 1 | const homeNav = [ 2 | { 3 | name: '简历制作', 4 | path: '/template', 5 | tooltip: false 6 | }, 7 | // { 8 | // name: '求职社区', 9 | // path: '/community', 10 | // tooltip: false 11 | // }, 12 | { 13 | name: '语法助手', 14 | path: '/syntax/helper', 15 | tooltip: false 16 | }, 17 | { 18 | name: '更新记录', 19 | path: '/update/line', 20 | tooltip: false 21 | } 22 | ] 23 | 24 | const homeOutNav = [ 25 | { 26 | name: 'GitHub', 27 | path: 'https://github.com/acmenlei/markdown-resume-to-pdf', 28 | icon: 'iconfont icon-github' 29 | }, 30 | { 31 | name: 'Gitee', 32 | path: 'https://gitee.com/codeleilei/markdown2pdf', 33 | icon: 'iconfont icon-gitee', 34 | color: '#d90013' 35 | } 36 | ] 37 | export { homeNav, homeOutNav } 38 | -------------------------------------------------------------------------------- /src/common/nav/nav.ts: -------------------------------------------------------------------------------- 1 | const nav = [ 2 | { 3 | name: '导入/导出', 4 | multiple: true, 5 | children: ['导入MD', '导出MD', '导出图片'] 6 | }, 7 | { 8 | name: '简历模板', 9 | path: '/template', 10 | tooltip: false 11 | }, 12 | 13 | { 14 | name: '语法助手', 15 | path: '/syntax/helper', 16 | tooltip: false 17 | } 18 | ] 19 | 20 | export default nav 21 | -------------------------------------------------------------------------------- /src/common/nav/outNav.ts: -------------------------------------------------------------------------------- 1 | const outNav = [ 2 | { 3 | name: '简历制作', 4 | path: '/template', 5 | tooltip: false 6 | }, 7 | // { 8 | // name: '求职社区', 9 | // path: '/community', 10 | // tooltip: false 11 | // }, 12 | { 13 | name: '语法助手', 14 | path: '/syntax/helper', 15 | tooltip: false 16 | }, 17 | // { 18 | // name: '岗位推荐', 19 | // path: '/recruit', 20 | // tooltip: false 21 | // }, 22 | { 23 | name: '更新内容', 24 | path: '/update/line', 25 | tooltip: false 26 | } 27 | ] 28 | 29 | export default outNav 30 | -------------------------------------------------------------------------------- /src/common/tip.ts: -------------------------------------------------------------------------------- 1 | export enum Tip { 2 | NETWORK_ERROR = '网络发生了一点小故障,请检查网络问题再来试试吧~', 3 | BE_INCOMPLATE = '请输入完整的账户信息', 4 | VERIFY_CODE_INVAILED = '验证码错误,请重新尝试' 5 | } 6 | -------------------------------------------------------------------------------- /src/components/browse-history/browseHistory.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /src/components/browse-history/hook.ts: -------------------------------------------------------------------------------- 1 | import { getLocalStorage, setLocalStorage } from '@/common/localstorage' 2 | import { onActivated, ref } from 'vue' 3 | import { useRouter } from 'vue-router' 4 | import { IArticle } from '@@types/type' 5 | 6 | export function useBrowseHistory() { 7 | const BROWSE_HISTORY = '__BROWSE_HISTORY__', 8 | max = 10, 9 | data = ref([]) 10 | const router = useRouter() 11 | 12 | function setBrowseHistory(article: IArticle) { 13 | const history = getBrowseHistory() 14 | if (history.length >= max) { 15 | history.pop() 16 | } 17 | history.unshift(article) 18 | setLocalStorage(BROWSE_HISTORY, history, 60 * 60 * 1000 * 24 * 365) 19 | } 20 | 21 | function getBrowseHistory() { 22 | return (getLocalStorage(BROWSE_HISTORY) || []) as IArticle[] 23 | } 24 | 25 | function setData(historys: IArticle[]) { 26 | data.value = historys 27 | } 28 | 29 | function useDetail(articleId: number) { 30 | router.push(`/community/detail?articleId=${articleId}`) 31 | } 32 | 33 | onActivated(() => { 34 | setData(getBrowseHistory() || []) 35 | }) 36 | return { 37 | data, 38 | useDetail, 39 | setBrowseHistory, 40 | getBrowseHistory 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/chat-room/chat.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/comment-reply-msg/crm.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 71 | 72 | 125 | -------------------------------------------------------------------------------- /src/components/comment-reply-msg/hook.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, watch } from 'vue' 2 | import { ref } from 'vue' 3 | import useUserStore from '@/store/modules/user' 4 | import { queryNotification, updateNotificationState } from '@/api/modules/notification' 5 | import { useRouter } from 'vue-router' 6 | import { errorMessage } from '@/common/message' 7 | import { type INotificationList, IResponse } from '@@types/type' 8 | 9 | export function useNotificationList(toggleMessageModal: () => void) { 10 | const { userInfo } = useUserStore(), 11 | router = useRouter(), 12 | commentTotal = ref(0), 13 | total = ref(0) 14 | const conditions = ref({ pageNum: 1, pageSize: 10, uid: 0 }) 15 | const data = ref([]) 16 | 17 | async function queryData() { 18 | conditions.value.uid = userInfo.uid 19 | const res = (await queryNotification(conditions.value)) as IResponse 20 | if (res.code == 200) { 21 | data.value = res.data as INotificationList[] 22 | total.value = res.total as number 23 | commentTotal.value = (res as any).commentTotal as number 24 | } else { 25 | errorMessage(res.msg) 26 | } 27 | } 28 | async function readNotification({ 29 | commentId, 30 | articleId, 31 | read, 32 | posterCommentId 33 | }: INotificationList) { 34 | router.replace({ path: '/community/detail', query: { articleId, posterCommentId } }) 35 | toggleMessageModal() 36 | if (read != 1) { 37 | const res = (await updateNotificationState({ commentId })) as IResponse 38 | if (res.code == 200) queryData() 39 | } 40 | } 41 | function pageNumChange(page: number) { 42 | conditions.value.pageNum = page 43 | queryData() 44 | } 45 | onMounted(() => { 46 | if (userInfo.uid != 0) queryData() 47 | }) 48 | watch( 49 | () => userInfo.uid, 50 | () => { 51 | if (userInfo.uid != 0) queryData() 52 | } 53 | ) 54 | return { 55 | data, 56 | total, 57 | commentTotal, 58 | readNotification, 59 | pageNumChange 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/comments/hook.ts: -------------------------------------------------------------------------------- 1 | import { isLogin } from '@/common/global' 2 | import { errorMessage } from '@/common/message' 3 | import { successMessage } from '@/common/message' 4 | import { removeComment } from '@/api/modules/comments' 5 | import { calcOffsetTop, scrollTo } from '@/utils' 6 | import useUserStore from '@/store/modules/user' 7 | import { nextTick, Ref, ref, watch } from 'vue' 8 | import { type IResponse } from '@@types/type' 9 | 10 | // 回复所需要的操作 11 | export function useReply(emits: any) { 12 | const { userInfo } = useUserStore() 13 | const currenId = ref(-1) 14 | let preId = -1 15 | 16 | function reply(commentId: number) { 17 | if (preId === commentId) { 18 | currenId.value = -1 19 | preId = -1 20 | return 21 | } 22 | preId = commentId 23 | currenId.value = commentId 24 | } 25 | 26 | async function remove(commentId: number, articleId: number, level: number) { 27 | if (!isLogin()) { 28 | errorMessage('请先登录!') 29 | window.location.reload() 30 | return 31 | } 32 | const rest: IResponse = (await removeComment({ 33 | commentId, 34 | articleId, 35 | level 36 | })) as IResponse 37 | if (rest.code == 200) { 38 | successMessage(rest.msg) 39 | emits('reQueryComments') 40 | return 41 | } 42 | errorMessage(rest.msg) 43 | } 44 | 45 | return { 46 | userInfo, 47 | reply, 48 | remove, 49 | currenId 50 | } 51 | } 52 | // 展示更多 53 | export function useShowMore(count: number) { 54 | const more = ref(count > 1) 55 | 56 | function setMore() { 57 | more.value = false 58 | } 59 | return { 60 | more, 61 | setMore 62 | } 63 | } 64 | // 获取当前评论的具体页数和位置 65 | export function useCommentPosition(position: Ref) { 66 | const comments = ref() 67 | // 点击通知后进行评论定位 68 | watch( 69 | () => position.value, 70 | () => { 71 | try { 72 | nextTick(() => { 73 | const targetComment = comments.value.children[position.value] 74 | scrollTo(calcOffsetTop(targetComment) - 65) 75 | targetComment.classList.add('notice') 76 | setTimeout(() => { 77 | targetComment.classList.remove('notice') 78 | }, 1000) 79 | }) 80 | } catch (e) { 81 | console.log(e) 82 | errorMessage('出了点错,请刷新后重新尝试~') 83 | } 84 | } 85 | ) 86 | return { 87 | comments 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/components/comments/reply.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 60 | 61 | 91 | -------------------------------------------------------------------------------- /src/components/contact.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/components/empty.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 26 | -------------------------------------------------------------------------------- /src/components/exportTotal.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/components/hot-rank/hook.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router' 2 | import { errorMessage } from '@/common/message' 3 | import { queryCommunityHotRank } from '@/api/modules/community' 4 | import { onMounted, ref } from 'vue' 5 | import { IArticle, IResponse } from '@@types/type' 6 | 7 | export function useHotRank() { 8 | const hotList = ref([]), 9 | router = useRouter() 10 | 11 | async function queryHotRankList() { 12 | const hotRankList = (await queryCommunityHotRank({ requireCount: 10 })) as IResponse 13 | if (hotRankList.code === 200) { 14 | hotList.value = hotRankList.data as IArticle[] 15 | return 16 | } 17 | errorMessage(hotRankList.msg) 18 | } 19 | 20 | function useDetail(articleId: number) { 21 | router.push(`/community/detail?articleId=${articleId}`) 22 | } 23 | // 请求一次就行 24 | onMounted(() => queryHotRankList()) 25 | 26 | return { 27 | useDetail, 28 | hotList 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/hot-rank/hotList.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 51 | -------------------------------------------------------------------------------- /src/components/menu-bar/menu-bar-item/hooks/useScrollTop.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, ref } from 'vue' 2 | import { useDebounceFn } from '@vueuse/core' 3 | 4 | export default function useScrollTop() { 5 | const scrollTop = ref(0) 6 | const cb = useDebounceFn(function () { 7 | scrollTop.value = (document.documentElement || document.body).scrollTop 8 | }, 50) 9 | 10 | onMounted(() => { 11 | document.addEventListener('scroll', cb) 12 | }) 13 | 14 | onUnmounted(() => { 15 | document.removeEventListener('scroll', cb) 16 | }) 17 | return scrollTop 18 | } 19 | -------------------------------------------------------------------------------- /src/components/menu-bar/menu-bar-item/menuBarItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 83 | -------------------------------------------------------------------------------- /src/components/menu-bar/menu-bar/MenuBar.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 38 | 39 | 62 | -------------------------------------------------------------------------------- /src/components/menu-bar/menu-bar/hooks/useMenuBarTitle.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, onActivated, ref } from 'vue' 2 | import { IMenuBarItem } from '../../type' 3 | 4 | // let levels = { h1: 1, h2: 1, h3: 1, h4: 1, h5: 1,h6: 1} 5 | export default function useMenuBarTitleConfigura(root: string) { 6 | const oMenuBarTitleData = ref>([]) 7 | 8 | nextTick(() => { 9 | oMenuBarTitleData.value = genMenuBarData(root) 10 | }) 11 | onActivated(() => { 12 | oMenuBarTitleData.value = genMenuBarData(root) 13 | }) 14 | 15 | return oMenuBarTitleData 16 | } 17 | 18 | function genMenuBarData(root: string): Array { 19 | const oMenuBarTitleData: Array = [] 20 | dfs(oMenuBarTitleData, document.querySelector(root) as HTMLElement, 1) 21 | genMenunItemGap(oMenuBarTitleData) 22 | return oMenuBarTitleData 23 | } 24 | 25 | function dfs(menus: Array, node: HTMLElement, level: number) { 26 | // 是一个标题节点 27 | const tagName = node?.tagName.toLowerCase() 28 | if (node?.nodeType == 1 && tagName[0] === 'h') { 29 | const oMenuItem = { offsetMax: 0 } as IMenuBarItem 30 | //创建子元素 31 | oMenuItem.title = node.textContent + '' 32 | oMenuItem.level = +tagName[1] 33 | oMenuItem.offset = getMenuItemOffset(node) 34 | oMenuItem.target = node 35 | 36 | menus.push(oMenuItem) 37 | } else { 38 | if (!node || node.nodeType != 1) { 39 | return 40 | } 41 | const childrens = Array.from(node.children) 42 | 43 | for (const child of childrens) { 44 | dfs(menus, child as HTMLElement, level + 1) 45 | } 46 | } 47 | } 48 | 49 | function getMenuItemOffset(node: HTMLElement) { 50 | let height = node?.offsetTop, 51 | parent = node.offsetParent as HTMLElement 52 | while (parent !== null) { 53 | height += parent.offsetTop 54 | parent = parent.offsetParent as HTMLElement 55 | } 56 | return height 57 | } 58 | 59 | function genMenunItemGap(data: Array) { 60 | for (let i = 0, n = data.length; i < n; i++) { 61 | if (i + 1 < n) { 62 | data[i].offsetMax = data[i + 1].offset 63 | } else { 64 | data[i].offsetMax = Infinity 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/menu-bar/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface IMenuBarItem { 2 | level: number 3 | title: string 4 | offset: number 5 | offsetMax: number 6 | target: HTMLElement 7 | } 8 | -------------------------------------------------------------------------------- /src/components/navBar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | 66 | -------------------------------------------------------------------------------- /src/components/pwd-update/PWDUpdate.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /src/components/pwd-update/hook.ts: -------------------------------------------------------------------------------- 1 | import useUserStore from '@/store/modules/user' 2 | import { errorMessage, warningMessage } from '@/common/message' 3 | import VerificationCode, { createCode } from 'picture-verification-code' 4 | import { onMounted, ref } from 'vue' 5 | import { pwdUpdate } from '@/api/modules/user' 6 | import { IResponse } from '@@types/type' 7 | 8 | export function useSubmit(emits: any) { 9 | const form = ref({ nPassword: '', oPassword: '', verify: '' }) 10 | const imgSrc = ref('') 11 | const codeInstance = new VerificationCode() 12 | let verifyCode = '' 13 | // 提交修改 14 | async function submit() { 15 | if (form.value.nPassword.trim() === '' || form.value.oPassword.trim() === '') { 16 | return errorMessage('信息请填写完整!') 17 | } 18 | if (form.value.verify.trim().toLowerCase() != verifyCode.toLowerCase()) { 19 | return errorMessage('验证码不正确,请重新尝试!') 20 | } 21 | const { userInfo } = useUserStore(), 22 | username = userInfo.username 23 | const { code, msg } = (await pwdUpdate({ 24 | username, 25 | nPassword: form.value.nPassword, 26 | oPassword: form.value.oPassword 27 | })) as IResponse 28 | if (code == 200) { 29 | warningMessage(msg) 30 | location.reload() 31 | return 32 | } 33 | errorMessage(msg) 34 | } 35 | function genCode() { 36 | verifyCode = createCode() 37 | imgSrc.value = codeInstance.render(verifyCode) 38 | } 39 | function cancel() { 40 | genCode() 41 | emits('cancel') 42 | } 43 | onMounted(genCode) 44 | return { 45 | form, 46 | imgSrc, 47 | genCode, 48 | cancel, 49 | submit 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/renderIcons.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 64 | 65 | 100 | -------------------------------------------------------------------------------- /src/components/reward.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 29 | 30 | 65 | -------------------------------------------------------------------------------- /src/components/themeToggle.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | -------------------------------------------------------------------------------- /src/components/toast-modal/toastModal.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 15 | 39 | -------------------------------------------------------------------------------- /src/components/userInfo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 28 | 67 | -------------------------------------------------------------------------------- /src/components/userTooltip.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 64 | -------------------------------------------------------------------------------- /src/layout/footer.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 132 | -------------------------------------------------------------------------------- /src/layout/header/components/nav.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /src/layout/header/components/navMoblie.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /src/layout/header/header.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /src/layout/header/hook.ts: -------------------------------------------------------------------------------- 1 | import { successMessage } from '@/common/message' 2 | import { Router } from 'vue-router' 3 | import { onMounted, reactive, ref } from 'vue' 4 | 5 | import useUserStore, { TOKEN, USERNAME } from '@/store/modules/user' 6 | import { getLocalStorage } from '@/common/localstorage' 7 | import { errorMessage } from '@/common/message' 8 | import { updateUserInfo } from '@/api/modules/user' 9 | import { IResponse } from '@@types/type' 10 | 11 | export function useUpdateInfoModel() { 12 | const infoModel = ref(false) 13 | function setInfoModel() { 14 | infoModel.value = !infoModel.value 15 | } 16 | return { 17 | infoModel, 18 | setInfoModel 19 | } 20 | } 21 | 22 | export const userForm = reactive({ 23 | uid: 0, 24 | nickName: '', 25 | username: '', 26 | sex: '', 27 | professional: '', 28 | graduation: '', 29 | school: '', 30 | avatar: '', 31 | origin: '' 32 | }) 33 | export function useUpdateInfo(toggle: () => void) { 34 | async function updateInfo() { 35 | const { userInfo, setUserInfo } = useUserStore() 36 | // 格式化时间 只需要年份 37 | userForm.graduation = String(new Date(userForm.graduation).getFullYear()) 38 | const data = (await updateUserInfo(userForm)) as IResponse 39 | if (data.code == 200) { 40 | toggle() 41 | successMessage(data.msg) 42 | setUserInfo(userInfo, userForm) 43 | } else { 44 | errorMessage(data.msg) 45 | } 46 | } 47 | return { 48 | updateInfo 49 | } 50 | } 51 | 52 | export function useUserLogin() { 53 | const user = reactive({ username: '', password: '', verify: '' }) 54 | const { login, logout, verifyLoginState } = useUserStore() 55 | 56 | onMounted(() => { 57 | const token = getLocalStorage(TOKEN), 58 | username = getLocalStorage(USERNAME) 59 | token && username && verifyLoginState(token as string, username as string) 60 | }) 61 | 62 | return { 63 | user, 64 | login, 65 | logout 66 | } 67 | } 68 | 69 | export function useNavigator(router: Router, path: string) { 70 | const { loginState, loginModelToggle } = useUserStore() 71 | if (!loginState.logined) { 72 | loginModelToggle() 73 | return 74 | } 75 | router.push(path) 76 | } 77 | 78 | export function useRegister() { 79 | const model = ref(false), 80 | registerUser = reactive({ username: '', password: '', verify: '' }) 81 | const { genVerify } = useUserStore() 82 | 83 | function toggleModel() { 84 | model.value = !model.value 85 | genVerify() 86 | } 87 | return { 88 | model, 89 | registerUser, 90 | toggleModel 91 | } 92 | } 93 | 94 | export function useMessage() { 95 | const messageModal = ref(false), 96 | tab = ref(0) 97 | 98 | function toggleMessageModal() { 99 | messageModal.value = !messageModal.value 100 | } 101 | 102 | function msgTabChange(idx: number) { 103 | tab.value = idx 104 | } 105 | 106 | return { 107 | tab, 108 | messageModal, 109 | msgTabChange, 110 | toggleMessageModal 111 | } 112 | } 113 | // 修改密码 114 | export function useUpdatePWDModel() { 115 | const PWDModel = ref(false) 116 | function setPWDModel() { 117 | PWDModel.value = !PWDModel.value 118 | } 119 | return { 120 | PWDModel, 121 | setPWDModel 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/layout/main.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './permission' 4 | import '@/assets/global.scss' 5 | import 'element-plus/theme-chalk/dark/css-vars.css' 6 | import pinia from '@/store' 7 | 8 | createApp(App).use(router).use(pinia).mount('#app') 9 | -------------------------------------------------------------------------------- /src/permission.ts: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import { getLocalStorage } from '@/common/localstorage' 3 | import { TOKEN } from '@/store/modules/user' 4 | import useUserStore from '@/store/modules/user' 5 | import nprogress from 'nprogress' 6 | import 'nprogress/nprogress.css' 7 | 8 | nprogress.configure({ easing: 'ease', speed: 300 }) 9 | 10 | const whiteList = ['/download'] 11 | 12 | router.beforeEach((to, from, next) => { 13 | if (!whiteList.includes(to.path)) { 14 | nprogress.start() 15 | } 16 | const token = getLocalStorage(TOKEN) 17 | if (['/community/editor'].includes(to.path)) { 18 | if (!token) { 19 | const { loginModelToggle } = useUserStore() 20 | next({ ...from }) 21 | loginModelToggle() // 需要登录 22 | return 23 | } 24 | } 25 | next() 26 | }) 27 | 28 | router.afterEach(() => { 29 | nprogress.done() 30 | }) 31 | 32 | export default router 33 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | /* 统一导入路由 */ 4 | const routeFiles = import.meta.glob('./modules/*.ts', { eager: true }) 5 | export const routeConfiguras: RouteRecordRaw[] = [] 6 | 7 | Object.keys(routeFiles).forEach(routeModule => { 8 | ;(routeFiles[routeModule] as any).default && 9 | routeConfiguras.push((routeFiles[routeModule] as any).default) 10 | }) 11 | 12 | const routes: RouteRecordRaw[] = [ 13 | { 14 | path: '/', 15 | redirect: '/home' 16 | }, 17 | { 18 | path: '/download', 19 | name: 'download', 20 | component: () => import('@/views/download/index.vue') 21 | }, 22 | { 23 | path: '/:pathMatch(.*)*', 24 | name: 'NotFound', 25 | component: () => import('@/views/404/index.vue') 26 | } 27 | ] 28 | 29 | const topInitList = ['/community/detail', '/syntax/helper', '/update/line', '/home', '/editor'] 30 | 31 | const router = createRouter({ 32 | routes: routeConfiguras.concat(routes), 33 | history: createWebHashHistory(), 34 | scrollBehavior: (to, from, savePos) => { 35 | if (topInitList.includes(to.path)) return { top: 0 /* behavior: 'smooth' */ } 36 | if (savePos) return savePos 37 | } 38 | }) 39 | 40 | export default router 41 | -------------------------------------------------------------------------------- /src/router/modules/community.ts.not: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'community-root', 5 | path: '/community', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/community', 10 | name: 'community', 11 | component: () => import('@/views/community/community.vue') 12 | }, 13 | { 14 | path: '/community/editor', 15 | name: 'communityEditor', 16 | component: () => import('@/views/community/views/editor/communityEditor.vue') 17 | }, 18 | { 19 | path: '/community/detail', 20 | name: 'communityDetail', 21 | component: () => import('@/views/community/views/detail/communityDetail.vue') 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/router/modules/editor.ts: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'editor', 5 | path: '/editor', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/editor', 10 | name: 'editor', 11 | component: () => import('@/views/editor/editor.vue') 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'home', 5 | path: '/home', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/home', 10 | name: 'home', 11 | component: () => import('@/views/home/home.vue') 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/router/modules/recruit.ts.not: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'recruit', 5 | path: '/recruit', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/recruit', 10 | name: 'recruit', 11 | component: () => import('@/views/recruit/recruit.vue') 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/router/modules/syntax.ts: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'syntax', 5 | path: '/syntax', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/syntax/helper', 10 | name: 'syntaxHelper', 11 | component: () => import('@/views/syntax/syntax.vue') 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/router/modules/template.ts: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'template', 5 | path: '/template', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/template', 10 | name: 'template', 11 | component: () => import('@/views/template/template.vue') 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/router/modules/update.ts: -------------------------------------------------------------------------------- 1 | import Layout from '@/layout/main.vue' 2 | 3 | export default { 4 | name: 'update', 5 | path: '/update', 6 | component: Layout, 7 | children: [ 8 | { 9 | path: '/update/line', 10 | name: 'updateLine', 11 | component: () => import('@/views/update/update.vue') 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const pinia = createPinia() 4 | 5 | export default pinia 6 | -------------------------------------------------------------------------------- /src/store/modules/editor.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { nextTick } from 'vue' 3 | 4 | import pinia from '@/store' 5 | import { getLocalStorage, setLocalStorage } from '@/common/localstorage' 6 | import { showMessageVN } from '@/common/message' 7 | import { templates } from '@/templates/config' 8 | import { ensureEmptyPreWhiteSpace } from '@/views/editor/components/tabbar/hook' 9 | 10 | const MARKDOWN_CONTENT = 'markdown-content' 11 | const WRITABLE = 'writable' 12 | 13 | export const getCurrentTypeContent = (type: string): string => { 14 | for (const template of templates.value) { 15 | if (type === template.type) { 16 | return template.content 17 | } 18 | } 19 | return '' 20 | } 21 | 22 | const useEditorStore = defineStore('editorStore', { 23 | state: () => ({ 24 | MDContent: '', 25 | nativeContent: '', 26 | writable: Boolean(getLocalStorage(WRITABLE)) || false 27 | }), 28 | actions: { 29 | // 初始化编辑器内容(默认为Markdown模式) 30 | initMDContent(resumeType: string) { 31 | const cacheKey = MARKDOWN_CONTENT + '-' + resumeType 32 | this.MDContent = getLocalStorage(cacheKey) 33 | ? (getLocalStorage(cacheKey) as string) 34 | : getCurrentTypeContent(resumeType) 35 | }, 36 | setMDContent(nv: string, resumeType: string) { 37 | this.MDContent = nv 38 | // 处理之后的操作 39 | if (!nv) return 40 | setLocalStorage(`${MARKDOWN_CONTENT}-${resumeType}`, nv) 41 | }, 42 | // 切换编辑模式 43 | setWritableMode(originHTML: HTMLElement) { 44 | this.writable = !this.writable 45 | setLocalStorage(WRITABLE, this.writable) 46 | showMessageVN('您已切换至', this.writable ? '内容模式' : 'Markdown模式') 47 | if (this.writable) { 48 | nextTick(() => { 49 | originHTML = originHTML || (document.querySelector('.reference-dom') as HTMLElement) 50 | originHTML = originHTML.cloneNode(true) 51 | const DOMTree = document.querySelector('.writable-edit-mode') as HTMLElement 52 | ensureEmptyPreWhiteSpace(originHTML) 53 | DOMTree && (DOMTree.innerHTML = originHTML.innerHTML) 54 | }) 55 | } 56 | }, 57 | setNativeContent(content: string) { 58 | this.nativeContent = content 59 | }, 60 | resetNativeContent() { 61 | this.nativeContent = '' 62 | } 63 | } 64 | }) 65 | 66 | export default () => useEditorStore(pinia) 67 | -------------------------------------------------------------------------------- /src/templates/config.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | const initialCVState: Map = new Map() 4 | 5 | // 创作模板的默认配置 6 | initialCVState.set('create', ['#333', '#333', '', '25']) 7 | 8 | type Module = { 9 | default: SubModule 10 | } 11 | 12 | type SubModule = { 13 | type: string 14 | id: number 15 | name: string 16 | font?: string 17 | lineHeight?: number 18 | content: string 19 | primaryColor: string 20 | primaryBackground: string 21 | img: string 22 | hot?: number | string 23 | } 24 | export type TemplateType = SubModule 25 | 26 | export const templates = ref([]) 27 | 28 | const moduleEntries = Object.entries(import.meta.glob('./modules/*/index.ts', { eager: true })) 29 | 30 | for (const [path, curModule] of moduleEntries) { 31 | const content = (curModule as Module).default 32 | content.id = Math.ceil(Math.random() * 1000000000) 33 | content.type = path.split('/')[2] 34 | templates.value.push(content) 35 | initialCVState.set(content.type, [ 36 | content.primaryColor, 37 | content.primaryBackground, 38 | content.font || '', 39 | String(content.lineHeight || 25) 40 | ]) 41 | } 42 | 43 | const match = (module: SubModule) => +(module.type.match(/^\d+/) as RegExpMatchArray)[0] 44 | templates.value.sort((a, b) => match(b) - match(a)) 45 | 46 | export function getPrimaryBGColor(type: string) { 47 | return (initialCVState.get(type) as string[])[1] 48 | } 49 | 50 | export function getPrimaryColor(type: string) { 51 | return (initialCVState.get(type) as string[])[0] 52 | } 53 | 54 | export function getFontFamily(type: string) { 55 | return (initialCVState.get(type) as string[])[2] 56 | } 57 | -------------------------------------------------------------------------------- /src/templates/modules/create/style.scss: -------------------------------------------------------------------------------- 1 | @import url('../../common.css'); 2 | 3 | .markdown-transform-html { 4 | background: #fff; 5 | font-size: 16px; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' 3 | import relativeTime from 'dayjs/plugin/relativeTime' 4 | 5 | dayjs.extend(relativeTime) 6 | dayjs.locale('zh-cn') 7 | 8 | export function formatTime(time: string | number) { 9 | return dayjs(time).format('YYYY-MM-DD HH:mm:ss') 10 | } 11 | // 以当前时间为基准返回如:几秒前..几分钟前...字样 12 | export function formatTimefromNow(time: string | number) { 13 | return dayjs(time).fromNow() 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function numFormat(num: number): string | number { 2 | if (num >= 1000) { 3 | return (num / 1000).toFixed(1) + 'k' 4 | } 5 | return num 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/moduleCombine.ts: -------------------------------------------------------------------------------- 1 | import { markdownToHTML } from 'markdown-transform-html' 2 | 3 | // 简历模块拆分 将每个子模块内容进行整合 4 | function moduleCombine(DOMStr: string) { 5 | const fragment = document.createElement('div') 6 | fragment.innerHTML = DOMStr 7 | const hasMainLayout = fragment.querySelector('.main-layout') 8 | const searchStart = hasMainLayout || fragment 9 | const nodes = Array.from(searchStart.childNodes) as HTMLElement[] 10 | let container = null, 11 | // eslint-disable-next-line prefer-const 12 | result = document.createElement('div') 13 | 14 | for (const node of nodes) { 15 | if (node.nodeType === Node.TEXT_NODE) continue 16 | if (node.tagName.toLocaleLowerCase() === 'h2') { 17 | if (container) { 18 | result.appendChild(container) 19 | } 20 | container = document.createElement('div') 21 | container.className = 'resume-module' 22 | container.appendChild(node) 23 | } else { 24 | container ? container.appendChild(node) : result.appendChild(node) 25 | } 26 | } 27 | // 最后的也添加 28 | container && result.appendChild(container) 29 | if (hasMainLayout) { 30 | searchStart.parentNode?.replaceChild(result, searchStart) 31 | result.className = 'main-layout' 32 | result = fragment 33 | } 34 | return result 35 | } 36 | 37 | export function convertDOM(DOMStr: string) { 38 | return moduleCombine(markdownToHTML(DOMStr)) 39 | } 40 | -------------------------------------------------------------------------------- /src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/community/community.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /src/views/community/components/community-left/communityLeft.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 72 | 73 | 87 | -------------------------------------------------------------------------------- /src/views/community/components/community-left/components/card/hook.ts: -------------------------------------------------------------------------------- 1 | import { warningMessage } from '@/common/message' 2 | import { errorMessage, successMessage } from '@/common/message' 3 | import { isLogin } from '@/common/global' 4 | import useUserStore from '@/store/modules/user' 5 | import { useRouter } from 'vue-router' 6 | import { likeArticle, removeCommunity } from '@/api/modules/community' 7 | import { ref, Ref, watchEffect } from 'vue' 8 | import { useBrowseHistory } from '@/components/browse-history/hook' 9 | import { IArticle, IResponse } from '@@types/type' 10 | 11 | export function useOperator(articleId: Ref, emits: any, hasClick: Ref) { 12 | const router = useRouter() 13 | 14 | async function useLike() { 15 | if (!isLogin()) { 16 | errorMessage('请先登录!') 17 | return 18 | } 19 | if (hasClick.value) { 20 | warningMessage('你已经赞过了,不用重复点~') 21 | return 22 | } 23 | const { userInfo } = useUserStore() 24 | await likeArticle({ userId: userInfo.uid, articleId: articleId.value }) 25 | emits('reQueryList', userInfo.uid) 26 | } 27 | 28 | function useDetail(article: IArticle) { 29 | const { setBrowseHistory } = useBrowseHistory() 30 | setBrowseHistory(article) 31 | router.push(`/community/detail?articleId=${articleId.value}`) 32 | } 33 | async function useRemove() { 34 | const res: IResponse = (await removeCommunity({ 35 | articleId: articleId.value 36 | })) as IResponse 37 | if (res.code == 200) { 38 | successMessage(res.msg) 39 | emits('remove') 40 | } 41 | } 42 | 43 | function useEditor() { 44 | router.push(`/community/editor?articleId=${articleId.value}`) 45 | } 46 | 47 | return { 48 | useLike, 49 | useDetail, 50 | useRemove, 51 | useEditor 52 | } 53 | } 54 | 55 | export function useCovers(mainContent: Ref) { 56 | const covers = ref([]) 57 | 58 | watchEffect(() => { 59 | const tmpCovers: string[] = [] 60 | mainContent.value.replace(/]*src=['"]([^'"]+)[^>]*>/gi, ($, $1) => { 61 | tmpCovers.length < 3 && tmpCovers.push($1) 62 | return $1 63 | }) 64 | covers.value = tmpCovers 65 | }) 66 | return { 67 | covers 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/views/community/components/community-left/components/notice/notice.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 57 | 58 | 116 | -------------------------------------------------------------------------------- /src/views/community/components/community-left/constant.ts: -------------------------------------------------------------------------------- 1 | export const tabs = ['推荐', '最新', '我的'] 2 | export const professionals = [ 3 | 'Java后端', 4 | 'Go', 5 | 'python', 6 | 'C++', 7 | '数据库', 8 | 'web前端', 9 | '大数据', 10 | '算法工程师', 11 | '数据分析', 12 | '技术运营', 13 | '测试开发', 14 | 'UI设计', 15 | '网络安全', 16 | '运维', 17 | '材料工程', 18 | '嵌入式开发', 19 | '移动通信', 20 | '区块链', 21 | '土木工程师', 22 | '芯片研发', 23 | '软件研发', 24 | '公务员' 25 | ] 26 | -------------------------------------------------------------------------------- /src/views/community/components/community-left/hook.ts: -------------------------------------------------------------------------------- 1 | import { isLogin } from '@/common/global' 2 | import useUserStore from '@/store/modules/user' 3 | import { errorMessage } from '@/common/message' 4 | import { queryCommunity } from '@/api/modules/community' 5 | import { reactive, ref } from 'vue' 6 | import { tabs } from './constant' 7 | import { useThrottleFn } from '@vueuse/core' 8 | import { IArticle, ICommunityCondition, IResponse } from '@@types/type' 9 | 10 | export function useTab(conditions: ICommunityCondition, conditionQuery: () => void) { 11 | const tab = ref(tabs[0]) 12 | function toggleTab(idx: number) { 13 | tab.value = tabs[idx] 14 | conditions.tab = idx 15 | // 切换就要重新计算pageNum了 16 | conditions.pageNum = 1 17 | conditionQuery() 18 | } 19 | return { 20 | tab, 21 | toggleTab 22 | } 23 | } 24 | 25 | export function useData() { 26 | const { userInfo, loginModelToggle } = useUserStore() 27 | const data = ref([]), 28 | loading = ref(false), 29 | noMore = ref(false) 30 | const conditions = reactive({ 31 | pageNum: 1, 32 | pageSize: 10, 33 | keyword: '', 34 | professional: '', 35 | tab: 0, 36 | uid: userInfo.uid 37 | }) 38 | // 无限滚动 39 | async function queryList() { 40 | if (noMore.value) { 41 | return 42 | } 43 | loading.value = true 44 | conditions.pageNum += 1 45 | const res = (await queryCommunity(conditions)) as IResponse 46 | if (res.code != 200) { 47 | return errorMessage(res.msg) 48 | } 49 | loading.value = false 50 | data.value.push(...(res.data)) 51 | if ((res.data as IArticle[]).length < conditions.pageSize) { 52 | noMore.value = true 53 | } 54 | } 55 | // 条件查询 56 | async function conditionQuery() { 57 | if (conditions.tab == 2) { 58 | if (!isLogin()) { 59 | errorMessage('请先登录再查看。') 60 | loginModelToggle() 61 | return 62 | } 63 | conditions.uid = userInfo.uid // 只看我自己的 64 | } 65 | loading.value = true 66 | const res = (await queryCommunity(conditions)) as IResponse 67 | if (res.code != 200) { 68 | return errorMessage(res.msg) 69 | } 70 | loading.value = false 71 | data.value = res.data as IArticle[] 72 | if (data.value.length < conditions.pageSize) { 73 | noMore.value = true 74 | } 75 | } 76 | // 点击专业锚点查询 77 | function queryProfessional(professional: string) { 78 | if (professional != conditions.professional) { 79 | conditions.professional = professional 80 | conditionQuery() 81 | } 82 | } 83 | // 删除文章 84 | function removeArticle(idx: number) { 85 | data.value.splice(idx, 1) 86 | } 87 | // 重置子查询 88 | function resetSub() { 89 | conditions.pageNum = 1 90 | conditions.keyword = '' 91 | conditions.professional = '' 92 | conditionQuery() 93 | } 94 | // 搜索 95 | function searchSub() { 96 | conditions.pageNum = 1 97 | conditionQuery() 98 | } 99 | return { 100 | data, 101 | loading, 102 | noMore, 103 | conditions, 104 | resetSub: useThrottleFn(resetSub, 1000), 105 | searchSub: useThrottleFn(searchSub, 1000), 106 | queryList, 107 | queryProfessional, 108 | removeArticle, 109 | conditionQuery 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/views/community/components/community-right/communityRight.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /src/views/community/views/editor/communityEditor.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 67 | -------------------------------------------------------------------------------- /src/views/download/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 33 | 34 | 52 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/editorContainer.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 29 | 64 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/hook.ts: -------------------------------------------------------------------------------- 1 | import useEditorStore from '@/store/modules/editor' 2 | import { Ref, computed, nextTick, onActivated, onDeactivated, ref, watchEffect } from 'vue' 3 | import { linkFlag, selectIcon } from './toolbar/hook' 4 | import { clickedTarget } from '../../hook' 5 | import { setClickedLinkText, setClickedLinkURL } from './toolbar/components/linkInput/hook' 6 | import { getPickerFile } from '@/utils/uploader' 7 | import { queryDOM } from '@/utils' 8 | 9 | export function reactiveWritable(resumeType: string) { 10 | const editorStore = useEditorStore() 11 | editorStore.initMDContent(resumeType) 12 | const writable = computed(() => editorStore.writable) 13 | return { 14 | writable 15 | } 16 | } 17 | 18 | // 左右移动伸缩布局 19 | export function useMoveLayout() { 20 | const left = ref(550) 21 | let flag = false 22 | 23 | function move(event: MouseEvent) { 24 | if (!flag) return 25 | left.value = event.clientX - 15 26 | } 27 | 28 | function down() { 29 | document.body.classList.add('no-select') 30 | flag = true 31 | } 32 | 33 | function up() { 34 | document.body.classList.remove('no-select') 35 | flag = false 36 | } 37 | 38 | onActivated(() => { 39 | window.addEventListener('mouseup', up) 40 | window.addEventListener('mousemove', move) 41 | }) 42 | 43 | onDeactivated(() => { 44 | window.removeEventListener('mouseup', up) 45 | window.removeEventListener('mousemove', move) 46 | }) 47 | return { left, down, top } 48 | } 49 | 50 | export function injectWritableModeAvatarEvent( 51 | writable: Ref, 52 | setAvatar: (path: string) => void 53 | ) { 54 | watchEffect(() => { 55 | if (!writable.value) return 56 | nextTick(() => { 57 | const node = queryDOM('.writable-edit-mode') as HTMLElement 58 | setTimeout(() => { 59 | const avatar = node.querySelector('img[alt*="个人头像"]') 60 | if (avatar) { 61 | avatar.addEventListener('click', async function () { 62 | const file = await getPickerFile({ 63 | multiple: false, 64 | accept: 'image/png, image/jpeg,image/jpg, ' 65 | }) 66 | const reader = new FileReader() 67 | reader.readAsDataURL(file) // 暂时使用base64方案 68 | reader.onload = function (event) { 69 | setAvatar(event.target?.result as string) 70 | } 71 | }) 72 | } 73 | 74 | injectWritableModeClickedReplace(node) 75 | }) 76 | }) 77 | }) 78 | } 79 | 80 | export function injectWritableModeClickedReplace(parentNode: HTMLElement) { 81 | parentNode.addEventListener('click', (event: Event) => { 82 | const target = event.target as HTMLElement, 83 | className = target.className, 84 | tagName = target.tagName.toLocaleLowerCase() 85 | if (className.includes('iconfont')) { 86 | selectIcon.value = !selectIcon.value 87 | clickedTarget.value = target 88 | } else if (tagName === 'a') { 89 | linkFlag.value = !linkFlag.value 90 | setClickedLinkText(target) 91 | setClickedLinkURL(target) 92 | clickedTarget.value = target 93 | } 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/md-editor/editor.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/md-editor/hook.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmenlei/codecv/c06cb36458182b8ea5c1b31a098cb86883a7a5f1/src/views/editor/components/editor/md-editor/hook.ts -------------------------------------------------------------------------------- /src/views/editor/components/editor/md-editor/md-editor.scss: -------------------------------------------------------------------------------- 1 | .cm-editor { 2 | font-size: 14px; 3 | &.cm-focused { 4 | outline: none; 5 | } 6 | } 7 | 8 | .cm-scroller { 9 | overflow-y: scroll; 10 | overflow-x: hidden; 11 | padding-bottom: 30px; 12 | 13 | .cm-gutters + .cm-content[contenteditable='true'] { 14 | margin: 0; 15 | } 16 | 17 | .cm-line { 18 | line-height: inherit; 19 | } 20 | } 21 | 22 | .ͼ1 .cm-scroller { 23 | font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; 24 | line-height: 20px; 25 | } 26 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/rich-editor/editor.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/rich-editor/hook.ts: -------------------------------------------------------------------------------- 1 | import useEditorStore from '@/store/modules/editor' 2 | import { queryDOM } from '@/utils' 3 | import { resumeDOMStruct2Markdown } from '@/utils/dom2md' 4 | import { nextTick, onActivated, ref } from 'vue' 5 | // 使用编辑模式 6 | export function useToggleEditorMode(resumeType: string) { 7 | const editorStore = useEditorStore(), 8 | DOMTree = ref() 9 | 10 | function ObserverContent() { 11 | const content = resumeDOMStruct2Markdown({ 12 | node: DOMTree.value as Node, 13 | latest: true, 14 | uid: 0, 15 | whiteSpace: 0, 16 | parent: DOMTree.value?.parentElement 17 | }) 18 | editorStore.setMDContent(content, resumeType) 19 | } 20 | 21 | onActivated(() => { 22 | if (editorStore.writable) { 23 | nextTick(() => { 24 | ;(DOMTree.value as HTMLElement).innerHTML = (( 25 | queryDOM('.reference-dom') 26 | )).innerHTML 27 | }) 28 | } 29 | }) 30 | return { 31 | editorStore, 32 | DOMTree, 33 | ObserverContent 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/rich-editor/writable.scss: -------------------------------------------------------------------------------- 1 | .writable-edit-mode { 2 | background: var(--background); 3 | color: var(--writable-font-color); 4 | line-height: 22px; 5 | font-size: 14px; 6 | 7 | .flex-layout { 8 | display: flex; 9 | align-items: center; 10 | margin-top: 5px; 11 | .flex-layout-item { 12 | margin-right: 10px; 13 | flex: 1; 14 | } 15 | } 16 | // 只有是两列布局的时候才起作用 17 | & > .flex-layout { 18 | align-items: flex-start; 19 | } 20 | ul, 21 | ol { 22 | padding-left: 20px; 23 | } 24 | li, 25 | p { 26 | margin-top: 5px; 27 | } 28 | p[breakLayout]:empty::before { 29 | content: '请输入内容...'; 30 | color: #999; 31 | } 32 | h1, 33 | h2, 34 | h3, 35 | h4, 36 | h5, 37 | h6 { 38 | margin-top: 10px; 39 | } 40 | h1 { 41 | font-size: 20px; 42 | } 43 | h2 { 44 | font-size: 17px; 45 | } 46 | h3 { 47 | font-size: 15px; 48 | } 49 | h4 { 50 | font-size: 14px; 51 | } 52 | h5 { 53 | font-size: 13px; 54 | } 55 | h6 { 56 | font-size: 12px; 57 | } 58 | 59 | i.iconfont { 60 | margin-right: 8px; 61 | &:hover { 62 | opacity: 0.5; 63 | cursor: pointer; 64 | } 65 | } 66 | 67 | a { 68 | color: var(--super-link); 69 | &:hover { 70 | opacity: 0.5; 71 | cursor: pointer; 72 | } 73 | } 74 | .head-layout { 75 | border-radius: 10px; 76 | position: relative; 77 | } 78 | img[alt*='个人头像'] { 79 | width: 24mm; 80 | height: 28mm; 81 | border-radius: 5px; 82 | margin: 0 10px; 83 | object-fit: cover; 84 | cursor: pointer; 85 | &:hover { 86 | opacity: 0.8; 87 | } 88 | } 89 | img { 90 | display: inline-block; 91 | max-width: 100%; 92 | cursor: pointer; 93 | &:hover { 94 | opacity: 0.8; 95 | } 96 | } 97 | 98 | table { 99 | width: 100%; 100 | text-align: center; 101 | tbody td:nth-child(1) { 102 | border-left: 1px solid #eee; 103 | } 104 | thead { 105 | color: #f8f8f8; 106 | background-color: #b2c9d6; 107 | } 108 | thead th, 109 | tbody td { 110 | padding: 10px; 111 | } 112 | tbody td { 113 | border-right: 1px solid #eee; 114 | border-bottom: 1px solid #eee; 115 | } 116 | } 117 | 118 | code.single-code { 119 | color: var(--writableFontColor); 120 | padding: 3px 10px; 121 | background: transparent; 122 | position: relative; 123 | border-radius: 5px; 124 | 125 | &::after { 126 | content: ''; 127 | position: absolute; 128 | width: 100%; 129 | height: 100%; 130 | left: 0; 131 | top: 0; 132 | border-radius: 5px; 133 | background: #ccc; 134 | opacity: 0.2; 135 | } 136 | } 137 | 138 | blockquote { 139 | position: relative; 140 | margin: 0; 141 | font-size: 14px; 142 | padding: 8px 15px; 143 | margin: 12px 0; 144 | &::after, 145 | &::before { 146 | content: ''; 147 | position: absolute; 148 | top: 0; 149 | left: 0; 150 | height: 100%; 151 | } 152 | &::after { 153 | width: 100%; 154 | opacity: 0.1; 155 | background: #b2c9d6; 156 | } 157 | &::before { 158 | position: absolute; 159 | width: 5px; 160 | background: #b2c9d6; 161 | opacity: 0.3; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/components/columnInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/components/linkInput/hook.ts: -------------------------------------------------------------------------------- 1 | import { warningMessage } from '@/common/message' 2 | // import { clickedTarget } from '@/views/editor/hook' 3 | import { ref } from 'vue' 4 | 5 | export const link = ref('') 6 | export const linkText = ref('') 7 | 8 | export function reset() { 9 | link.value = '' 10 | linkText.value = '' 11 | // clickedTarget.value = null 12 | } 13 | 14 | // 内容模式:点击超链接的时候设置弹出框的链接信息 15 | export function setClickedLinkURL(target: HTMLElement) { 16 | link.value = target.getAttribute('href') || '' 17 | } 18 | export function setClickedLinkText(target: HTMLElement) { 19 | linkText.value = target.textContent || '' 20 | } 21 | 22 | export function useLinkInput(emit: any) { 23 | function confirm() { 24 | if (!link.value || !linkText.value) { 25 | warningMessage('请填写完整!') 26 | return 27 | } 28 | emit('confirm', link.value, linkText.value) 29 | reset() 30 | } 31 | 32 | return { 33 | confirm, 34 | link, 35 | linkText 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/components/linkInput/linkInput.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/components/tableInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/constants.ts: -------------------------------------------------------------------------------- 1 | export const toolbarConfig = [ 2 | { 3 | icon: 'bold', 4 | command: 'bold', 5 | tip: '加粗文本' 6 | }, 7 | { 8 | icon: 'italic', 9 | command: 'italic', 10 | tip: '斜体文本' 11 | }, 12 | { 13 | icon: 'link', 14 | command: 'insertLink', 15 | tip: '添加链接/替换选中文本' 16 | }, 17 | { 18 | icon: 'unorderedlist', 19 | command: 'insertUnorderedList', 20 | tip: '无序列表' 21 | }, 22 | { 23 | icon: 'orderedlist', 24 | command: 'insertOrderedList', 25 | tip: '有序列表' 26 | }, 27 | { 28 | icon: 'emoji', 29 | command: 'insertIcon', 30 | tip: '插入图标' 31 | }, 32 | { 33 | icon: 'info', 34 | command: 'insertUserInfo', 35 | tip: '插入个人信息布局' 36 | }, 37 | { 38 | icon: 'columns', 39 | command: 'multiColumns', 40 | tip: '插入多列布局' 41 | }, 42 | { 43 | icon: 'table', 44 | command: 'insertTable', 45 | tip: '插入表格' 46 | }, 47 | { 48 | icon: 'code', 49 | command: 'insertCode', 50 | tip: '插入技能点' 51 | }, 52 | { 53 | icon: 'reply', 54 | command: 'breakLayout', 55 | tip: '跳出布局在新行编写' 56 | }, 57 | { 58 | icon: 'write', 59 | command: 'toMarkdownMode', 60 | tip: '切换至Markdown模式' 61 | } 62 | ] 63 | 64 | export const headings = [ 65 | { label: '普通文本', value: '普通文本' }, 66 | { label: '一级标题', value: 'h1' }, 67 | { label: '二级标题', value: 'h2' }, 68 | { label: '三级标题', value: 'h3' }, 69 | { label: '四级标题', value: 'h4' }, 70 | { label: '五级标题', value: 'h5' }, 71 | { label: '六级标题', value: 'h6' } 72 | ] 73 | 74 | export const markdownModeToolbarConfig = [ 75 | { 76 | icon: 'bold', 77 | command: 'insertBold', 78 | tip: '加粗' 79 | }, 80 | { 81 | icon: 'italic', 82 | command: 'insertItalic', 83 | tip: '斜体' 84 | }, 85 | { 86 | icon: 'unorderedlist', 87 | command: 'insertUnorderedlist', 88 | tip: '无序列表' 89 | }, 90 | { 91 | icon: 'orderedlist', 92 | command: 'insertOrderedlist', 93 | tip: '有序列表' 94 | }, 95 | { 96 | icon: 'link', 97 | command: 'insertLink', 98 | tip: '链接' 99 | }, 100 | { 101 | icon: 'image', 102 | command: 'insertAvatar', 103 | tip: '插入证件照格式' 104 | }, 105 | { 106 | icon: 'emoji', 107 | command: 'insertIcon', 108 | tip: '插入图标' 109 | }, 110 | { 111 | icon: 'info', 112 | command: 'insertHeadLayout', 113 | tip: '插入个人信息布局' 114 | }, 115 | { 116 | icon: 'practice', 117 | command: 'insertMainLayout', 118 | tip: '插入主体内容布局' 119 | }, 120 | { 121 | icon: 'columns', 122 | command: 'insertMultiColumns', 123 | tip: '插入多列布局' 124 | }, 125 | { 126 | icon: 'table', 127 | command: 'insertTable', 128 | tip: '插入表格' 129 | }, 130 | { 131 | icon: 'write', 132 | command: 'toContentMode', 133 | tip: '切换至内容模式' 134 | } 135 | ] 136 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/mdTool.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | 48 | 81 | -------------------------------------------------------------------------------- /src/views/editor/components/editor/toolbar/richTool.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 57 | 58 | 96 | -------------------------------------------------------------------------------- /src/views/editor/components/guide/guide.ts: -------------------------------------------------------------------------------- 1 | import { driver } from 'driver.js' 2 | import 'driver.js/dist/driver.css' 3 | import './popover.scss' 4 | import { getLocalStorage, setLocalStorage } from '@/common/localstorage' 5 | 6 | const driverObj = driver({ 7 | popoverClass: 'popover-container', 8 | showProgress: true, 9 | nextBtnText: '下一步', 10 | prevBtnText: '上一步', 11 | doneBtnText: '开始使用', 12 | steps: [ 13 | { 14 | element: '.editor-toolbar', 15 | popover: { 16 | title: '创作工具栏', 17 | description: '你可以使用该工具栏快速编写简历排版' 18 | } 19 | }, 20 | { 21 | element: '.icon-write', 22 | popover: { 23 | title: '编辑模式切换', 24 | description: 25 | '现支持两种模式,你可以使用 markdown富文本 的方式来编写,不用担心切换后数据丢失,因为它们之间的数据是同步的~' 26 | } 27 | }, 28 | { 29 | element: '.operator-level2', 30 | popover: { 31 | title: '简历工具栏', 32 | description: 33 | '你可以通过这些工具来调整你想要看到的简历效果,接下来我将给你介绍一下每一个工具的使用' 34 | } 35 | }, 36 | { 37 | element: '.icon-adjust', 38 | popover: { 39 | title: '调节元素边距', 40 | description: 41 | '如果你对简历中某个元素的排版并不满意,你可以通过该功能对指定元素的上下边距进行调整' 42 | } 43 | }, 44 | { 45 | element: '.icon-zhengjian', 46 | popover: { 47 | title: '证件照', 48 | description: '此功能为上传证件照' 49 | } 50 | }, 51 | 52 | { 53 | element: '.icon-diy', 54 | popover: { 55 | title: '自定义CSS', 56 | description: 57 | '如果你有能力编写CSS,那么你可以在此处编辑CSS来调整简历效果,注意,CSS都需要写在.jufe类下确保生效' 58 | } 59 | }, 60 | { 61 | element: '.font-color-picker', 62 | popover: { title: '自定义字体颜色', description: '简历的颜色可以由你自己自由控制' } 63 | }, 64 | { 65 | element: '.main-color-picker', 66 | popover: { title: '自定义主色调', description: '同样,主色调也可以自由调整' } 67 | }, 68 | { 69 | element: '.icon-refresh', 70 | popover: { 71 | title: '重置内容', 72 | description: '如果你想清空所有改动回到最初的样子,请使用该功能,该操作不可逆!' 73 | } 74 | }, 75 | { 76 | element: '.follow-roll', 77 | popover: { 78 | title: '跟随滚动', 79 | description: '同时要滚动左右两个容器太麻烦了?把这个打开吧!' 80 | } 81 | }, 82 | { 83 | element: '.font-select', 84 | popover: { 85 | title: '设置字体', 86 | description: '你可以根据自己喜好选择字体效果~' 87 | } 88 | }, 89 | { 90 | element: '.el-dropdown-link', 91 | popover: { 92 | title: '导出简历内容', 93 | description: 94 | '如果你想保存你的简历内容,请在此处导出MD文件,想继续编写时导入即可' 95 | } 96 | }, 97 | { 98 | element: '.use-guide', 99 | popover: { 100 | title: '使用引导', 101 | description: '如果你想再次查看使用指引,请点击这里' 102 | } 103 | } 104 | ] 105 | }) 106 | 107 | export const startGuide = function () { 108 | const guideStatus = getLocalStorage('resume-make-guide') 109 | if (guideStatus) return 110 | setTimeout(driverObj.drive) 111 | setLocalStorage('resume-make-guide', true, 1000 * 3600 * 24 * 180) 112 | } 113 | export const refreshGuide = driverObj.drive 114 | -------------------------------------------------------------------------------- /src/views/editor/components/guide/popover.scss: -------------------------------------------------------------------------------- 1 | .popover-container { 2 | background: var(--background); 3 | color: var(--writable-font-color); 4 | 5 | .driver-popover-title { 6 | color: var(--font-color); 7 | font-size: 14px; 8 | } 9 | .driver-popover-description { 10 | font-size: 13px; 11 | } 12 | .driver-popover-close-btn { 13 | color: var(--writable-font-color); 14 | } 15 | 16 | .driver-popover-prev-btn, 17 | .driver-popover-next-btn { 18 | text-shadow: none; 19 | border: none; 20 | } 21 | .driver-popover-next-btn { 22 | background: var(--theme); 23 | color: #f6f8f8; 24 | &:hover { 25 | background: var(--theme); 26 | color: #f6f8f8; 27 | opacity: 0.5; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/views/editor/components/header/header.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 51 | 52 | 105 | -------------------------------------------------------------------------------- /src/views/editor/components/header/hook.ts: -------------------------------------------------------------------------------- 1 | import { onActivated, ref } from 'vue' 2 | 3 | export function useFile(emit: any) { 4 | const fileName = ref('') 5 | const exportFile = (type: string) => { 6 | document.title = fileName.value 7 | emit(`download-${type}` as any, fileName.value) 8 | } 9 | 10 | const importFile = (event: any) => { 11 | emit('import-md', event?.target?.files[0]) 12 | } 13 | 14 | onActivated(() => (fileName.value = document.title)) 15 | return { 16 | fileName, 17 | exportFile, 18 | importFile 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/views/editor/components/header/nav.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 45 | 46 | 74 | -------------------------------------------------------------------------------- /src/views/editor/components/preview/render.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 50 | -------------------------------------------------------------------------------- /src/views/editor/components/tabbar/constant.ts: -------------------------------------------------------------------------------- 1 | export const marks = { 2 | 0: '0%', 3 | 10: '10%', 4 | 20: '20%', 5 | 30: '30%', 6 | 40: '40%', 7 | 50: '50%', 8 | 60: '60%', 9 | 70: '70%', 10 | 80: '80%', 11 | 90: '90%', 12 | 100: '100%' 13 | } 14 | -------------------------------------------------------------------------------- /src/views/editor/editor.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | 39 | 70 | -------------------------------------------------------------------------------- /src/views/home/components/header.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /src/views/home/components/presentation.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | 134 | -------------------------------------------------------------------------------- /src/views/recruit/README.md: -------------------------------------------------------------------------------- 1 | # 岗位添加方式 2 | 3 | ## 悉知 4 | 5 | > `warning`: 岗位需要是真实在招人的,刷`KPI`的被发现将加入黑名单册 6 | 7 | > `info`: 需要新增岗位的可以按照如下格式在 `recruit.ts` 文件中进行添加后提交 `PR` 或者联系作者本人添加 8 | 9 | - `logo`: 公司 logo(不是必须的,有的话可以加上,增加辨识度) 10 | - `type`: 招聘面向哪些人群 11 | - `job`: 招聘的岗位 12 | - `corporation`: 招聘公司名称 13 | - `tags`: 岗位或公司相关标签 14 | - `endTime`: 截止时间 15 | - `educational_required`: 学历要求 16 | - `remark`: 其他信息 17 | - `external_link`: 投递方式(可以给官方链接也可以提供微信联系方式) 18 | 19 | ## 岗位例子 20 | 21 | ```js 22 | { 23 | logo: 'https://gw.alicdn.com/imgextra/i3/O1CN0175GaEE1WFD2QbMmw2_!!6000000002758-2-tps-200-53.png', 24 | type: ['应届生', '1-3年经验'], 25 | job: '前端开发工程师', 26 | corporation: '飞猪旅行', 27 | tags: ['福利待遇好', '面试简单'], 28 | endTime: '尽快投递', 29 | educational_required: ['统招本科', '不强制要求92', '优秀可特批'], 30 | remark: '领导很好,本人亲试。', 31 | external_link: 'https://alibaba.com/feizhu' 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /src/views/recruit/hook.ts: -------------------------------------------------------------------------------- 1 | import { ref, watchEffect } from 'vue' 2 | import { recruits } from './recruits' 3 | 4 | export const EducationalRequiredOptions = [ 5 | '985/211本科', 6 | '不强制要求92', 7 | '优秀可特批', 8 | '统招本科', 9 | '专升本', 10 | '专科' 11 | ] 12 | export const WorkAndResetTimeOptions = [ 13 | '996', 14 | '855', 15 | '965', 16 | '1075', 17 | 'WLB', 18 | '下午茶', 19 | '餐补', 20 | '房补', 21 | '五险一金', 22 | '六险一金', 23 | '福利待遇好', 24 | '面试简单', 25 | '看中基础', 26 | '创业公司', 27 | '互联网大厂', 28 | '不打卡', 29 | '弹性上班' 30 | ] 31 | export const WorkEXPOptions = ['应届生', '一年以内', '1-3年经验', '3-5年经验', '5-10年经验'] 32 | export interface IExternalContact { 33 | app: string 34 | contact: string 35 | } 36 | export interface IRecruitData { 37 | logo?: string 38 | job: string 39 | type: string[] 40 | corporation: string 41 | tags: string[] 42 | endTime: string 43 | educational_required: string[] 44 | remark: string 45 | external_link: string | IExternalContact 46 | } 47 | export function useData() { 48 | const form = ref({ 49 | pageNum: 1, 50 | pageSize: 8, 51 | keyword: '', 52 | job: '', 53 | type: '', 54 | educational_required: '', 55 | icu: '' 56 | }) 57 | const data = ref([]) 58 | function query() { 59 | let curRecruits: IRecruitData[] = recruits 60 | const params = form.value 61 | if (params.educational_required) { 62 | curRecruits = curRecruits.filter(recruit => 63 | recruit.educational_required.includes(params.educational_required) 64 | ) 65 | } 66 | if (params.keyword) { 67 | curRecruits = curRecruits.filter( 68 | recruit => 69 | recruit.corporation.includes(params.keyword) || recruit.remark.includes(params.keyword) 70 | ) 71 | } 72 | if (params.job) { 73 | curRecruits = curRecruits.filter(recruit => recruit.job.includes(params.job)) 74 | } 75 | if (params.type) { 76 | curRecruits = curRecruits.filter(recruit => recruit.type.includes(params.type)) 77 | } 78 | if (params.icu) { 79 | curRecruits = curRecruits.filter(recruit => recruit.tags.includes(params.icu)) 80 | } 81 | const start = (params.pageNum - 1) * params.pageSize 82 | data.value = curRecruits.slice(start, start + params.pageSize) 83 | } 84 | 85 | function pageNumChange(page: number) { 86 | form.value.pageNum = page 87 | query() 88 | } 89 | 90 | function reset() { 91 | form.value = { 92 | pageNum: 1, 93 | pageSize: 8, 94 | keyword: '', 95 | job: '', 96 | type: '', 97 | educational_required: '', 98 | icu: '' 99 | } 100 | } 101 | 102 | watchEffect(query) 103 | return { 104 | data, 105 | form, 106 | query, 107 | reset, 108 | pageNumChange 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/views/recruit/recruits.ts: -------------------------------------------------------------------------------- 1 | import { IRecruitData } from './hook' 2 | 3 | export const recruits: IRecruitData[] = [ 4 | { 5 | logo: 'https://gw.alicdn.com/imgextra/i3/O1CN0175GaEE1WFD2QbMmw2_!!6000000002758-2-tps-200-53.png', 6 | type: ['应届生', '1-3年经验'], 7 | job: '前端工程师', 8 | corporation: '飞猪旅行(阿里集团旗下)', 9 | tags: ['福利待遇好', '面试简单', '不打卡'], 10 | endTime: '尽快投递', 11 | educational_required: ['统招本科', '不强制要求92'], 12 | remark: '团队氛围很好速冲', 13 | external_link: 14 | 'https://www.zhipin.com/job_detail/a871e43e3d756d851XN_09m-EltV.html?ka=personal_submitted_job_a871e43e3d756d851XN_09m-EltV' 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /src/views/syntax/syntax.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | 21 | 44 | -------------------------------------------------------------------------------- /src/views/template/components/resumeCard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 82 | -------------------------------------------------------------------------------- /src/views/template/constant.ts: -------------------------------------------------------------------------------- 1 | export const templateCategory = [ 2 | '全部', 3 | '校招', 4 | '社招', 5 | '英文', 6 | 'Geek', 7 | '运营', 8 | '商务', 9 | '设计', 10 | '互联网', 11 | '简约', 12 | '暗黑', 13 | '通用', 14 | '事业单位', 15 | '研究生复试' 16 | ] 17 | -------------------------------------------------------------------------------- /src/views/template/hook.ts: -------------------------------------------------------------------------------- 1 | import { templates, type TemplateType } from '@/templates/config' 2 | import { onMounted, Ref, ref } from 'vue' 3 | import { templateCategory } from './constant' 4 | import { getTemplateCondition } from '@/api/modules/resume' 5 | import { errorMessage } from '@/common/message' 6 | import { getLocalStorage, setLocalStorage } from '@/common/localstorage' 7 | 8 | export function useCategory() { 9 | const category = ref('全部') 10 | const data: Ref = ref([...templates.value]) 11 | 12 | function queryCategory(idx: number) { 13 | category.value = templateCategory[idx] 14 | if (category.value === '全部') { 15 | data.value = [...templates.value] 16 | return 17 | } 18 | data.value = templates.value.filter(template => template.name.includes(category.value)) 19 | } 20 | 21 | return { 22 | queryCategory, 23 | category, 24 | data 25 | } 26 | } 27 | 28 | export function useTemplateData() { 29 | const ranks = ref([]) 30 | async function templateCondition() { 31 | const _templateData = await getTemplateCondition() 32 | if (!_templateData.result) { 33 | errorMessage(_templateData.msg) 34 | return 35 | } 36 | const templateData = JSON.parse(_templateData.result) 37 | templates.value.forEach(template => (template.hot = templateData[`t${template.type}`])) 38 | ranks.value = [...templates.value] 39 | .sort((a, b) => (b.hot as number) - (a.hot as number)) 40 | .slice(0, 10) 41 | } 42 | onMounted(() => templateCondition()) 43 | 44 | return { 45 | templateCondition, 46 | ranks 47 | } 48 | } 49 | 50 | export function useNotification() { 51 | const flag = ref(false) 52 | 53 | function close() { 54 | flag.value = false 55 | setLocalStorage('notification', 'read', 1000 * 60 * 60 * 24 * 1) 56 | } 57 | onMounted(() => { 58 | if (getLocalStorage('notification') !== 'read') { 59 | flag.value = true 60 | } 61 | }) 62 | return { 63 | flag, 64 | close 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/views/update/constant.ts: -------------------------------------------------------------------------------- 1 | export const timeLine = [ 2 | { 3 | content: ` 4 | 纯前端导出体验并不好,暂时使用服务端导出PDF的方式替换前端导出方案,解决导出会出现的内容截断问题 5 | `, 6 | timestamp: '2023-07-15', 7 | version: '1.4.4' 8 | }, 9 | { 10 | content: ` 11 | 新增高质量岗位模块 12 | `, 13 | timestamp: '2023-07-10', 14 | version: '1.4.3' 15 | }, 16 | { 17 | content: ` 18 | 1. 内容模式工具栏新增链接按钮. 19 | 2. 新增暗黑模板 20 | `, 21 | timestamp: '2023-05-02', 22 | version: '1.4.2' 23 | }, 24 | { 25 | content: ` 26 | 1. 内容模式添加工具栏,支持使用UI交互来编写简历内容格式. 27 | `, 28 | timestamp: '2023-04-26', 29 | version: '1.4.1' 30 | }, 31 | { 32 | content: ` 33 | 1. 支持两种编辑模式(内容模式 & markdown模式),解决部分用户markdown语法门槛的问题,但是还是建议使用markdown编辑模式,内容模式目前处于试用阶段,可能出现一些问题(如果出现问题请转到markdown模式进行编辑后再重试). 34 | 2. 新增上边距调节器,你可以使用该工具调整简历中元素的上边距. 35 | 3. 调整 UI布局. 36 | `, 37 | timestamp: '2023-04-23', 38 | version: '1.4.0' 39 | }, 40 | { 41 | content: 42 | '新增UI设计师模板、编辑器中删除线语法、一键重置简历内容到初始状态(不可逆,操作前请注意!)功能,修复已有BUG。', 43 | timestamp: '2023-03-13', 44 | version: '1.3.4' 45 | }, 46 | { 47 | content: 48 | '新增多种模板:商务、社招类,上传证件照、自定义主色调、字体颜色功能,并优化自动一页功能,由于每种模板的间距都不太一样,个别模板或许会有微小的误差,可手动进行调整(手动增删部分文字即可).', 49 | timestamp: '2022-11-19', 50 | version: '1.3.3' 51 | }, 52 | { 53 | content: 54 | '新增字体选择、加载动画、文件导入导出、编辑器模式替换输入框,优化简历操作栏排版,修复简历中图片显示的问题.', 55 | timestamp: '2022-11-06', 56 | version: '1.3.2' 57 | }, 58 | { 59 | content: '新增语法小助手,你可以去语法小助手学习编写简历可能会用到的一些特殊语法.', 60 | timestamp: '2022-11-03', 61 | version: '1.3.1' 62 | }, 63 | { 64 | content: 65 | '新增实时分页显示、优化导出PDF的方式,提供了原生和动态计算两种导出方式,可以根据自己的偏好选择.', 66 | timestamp: '2022-11-02', 67 | version: '1.3.0' 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /src/views/update/update.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | declare module 'vue3-emoji-picker' { 10 | import picker from 'vue3-emoji-picker' 11 | export default picker 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": false, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "paths": { 16 | "@/*": ["./src/*"], 17 | "@@types/*": ["./types/*"] 18 | }, 19 | "types": ["element-plus/global"] 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 22 | "references": [ 23 | { 24 | "path": "./tsconfig.node.json" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /types/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | username: string 3 | password: string 4 | verify: string 5 | } 6 | 7 | export interface IResponse { 8 | code: number 9 | msg: string 10 | data?: T 11 | total?: number 12 | commentsTotal?: number 13 | } 14 | 15 | export interface ICommunityArticle { 16 | title: string 17 | content: string 18 | professional: string 19 | authorId: number 20 | } 21 | 22 | export interface ICommunityArticleUpdate { 23 | title: string 24 | content: string 25 | professional: string 26 | introduce: string 27 | articleId: number 28 | } 29 | 30 | export interface ICommunityCondition { 31 | pageNum: number 32 | pageSize: number 33 | tab: number 34 | uid: number 35 | professional: string 36 | keyword: string 37 | } 38 | 39 | export interface ICommunityLike { 40 | articleId: number 41 | userId: number 42 | } 43 | 44 | export interface IUserInfo { 45 | uid: number 46 | nickName: string 47 | username: string 48 | sex: string 49 | professional: string 50 | graduation: string 51 | school: string 52 | avatar: string 53 | origin: string 54 | createTime?: string 55 | updateTime?: string 56 | } 57 | 58 | export interface IArticle { 59 | title: string 60 | content: string 61 | professional: string 62 | authorId: number 63 | likes: number[] 64 | commentTotal: number 65 | hot: number 66 | createTime: string 67 | updateTime: string 68 | articleId: number 69 | introduce: string 70 | authorInfo: IUserInfo 71 | } 72 | 73 | export interface ICommentReply { 74 | commentId: number 75 | articleId: number 76 | authorId: number 77 | authorInfo: IUserInfo 78 | content: string 79 | images: string 80 | level: number 81 | createTime: string 82 | posterCommentId: number 83 | replyNickName: string 84 | } 85 | 86 | export interface IComment { 87 | commentId: number 88 | articleId: number 89 | content: string 90 | images: string 91 | authorId: number 92 | authorInfo: IUserInfo 93 | level: number 94 | createTime: string 95 | children: ICommentReply[] 96 | } 97 | 98 | export interface IPublishComment { 99 | content: string 100 | articleId: number 101 | level: number 102 | authorId: number 103 | replyArticleAuthorId: number 104 | } 105 | 106 | export interface IPublishCommentReply { 107 | content: string 108 | articleId: number 109 | level: number 110 | authorId: number 111 | posterCommentId: number 112 | replyAuthorId: number 113 | replyArticleAuthorId: number 114 | } 115 | 116 | export interface IArticleDetail { 117 | title: string 118 | content: string 119 | professional: string 120 | authorId: number 121 | like: number 122 | hot: number 123 | createTime: string 124 | updateTime: string 125 | articleId: number 126 | introduce: string 127 | authorInfo: IUserInfo 128 | comments: IComment[] 129 | } 130 | 131 | export interface INotificationList { 132 | read: number 133 | articleId: number 134 | commentId: number 135 | replyCommentId: number 136 | posterCommentId: number 137 | commentContent: { content: string; createTime: string } 138 | commentUserInfo: IUserInfo 139 | replyContent: { content?: string; title?: string; createTime: string } 140 | replyUserInfo: IUserInfo 141 | } 142 | 143 | export interface ICommentPosition { 144 | pageNum: number 145 | position: number 146 | data: IComment[] 147 | } 148 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { resolve } from 'path' 3 | import AutoImport from 'unplugin-auto-import/vite' 4 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { defineConfig, loadEnv } from 'vite' 7 | import viteCompression from 'vite-plugin-compression' 8 | import eslint from 'vite-plugin-eslint' 9 | import { visualizer } from 'rollup-plugin-visualizer' 10 | // import externalGlobals from 'rollup-plugin-external-globals' 11 | import viteImagemin from 'vite-plugin-imagemin' 12 | 13 | // const globals = externalGlobals({ 14 | // jspdf: 'jspdf.jsPDF', 15 | // axios: 'axios' 16 | // html2canvas: 'html2canvas' 17 | // }) 18 | 19 | const viteCompressionPlugin = viteCompression({ 20 | disable: false, 21 | threshold: 10240 // 如果体积大于阈值,将被压缩,单位为b,体积过小时请不要压缩,以免适得其反 22 | }) 23 | 24 | const viteImageminPlugin = viteImagemin({ 25 | gifsicle: { 26 | optimizationLevel: 7, 27 | interlaced: false 28 | }, 29 | optipng: { 30 | optimizationLevel: 7 31 | }, 32 | mozjpeg: { 33 | quality: 20 34 | }, 35 | pngquant: { 36 | quality: [0.8, 0.9], 37 | speed: 4 38 | }, 39 | svgo: { 40 | plugins: [ 41 | { 42 | name: 'removeViewBox' 43 | }, 44 | { 45 | name: 'removeEmptyAttrs', 46 | active: false 47 | } 48 | ] 49 | } 50 | }) 51 | 52 | export default ({ mode }) => { 53 | const env = loadEnv(mode, process.cwd()) 54 | return defineConfig({ 55 | plugins: [ 56 | vue(), 57 | AutoImport({ 58 | resolvers: [ElementPlusResolver()] 59 | }), 60 | Components({ 61 | resolvers: [ElementPlusResolver()] 62 | }), 63 | eslint({ lintOnStart: true, cache: false }) // 打包以及启动项目开启eslint检查 64 | ], 65 | resolve: { 66 | alias: { 67 | '@': resolve(__dirname, 'src'), 68 | '@@types': resolve(__dirname, 'types') 69 | } 70 | }, 71 | esbuild: { 72 | drop: env?.VITE_DROP_CONSOLE === 'true' ? ['console', 'debugger'] : [] 73 | }, 74 | base: './', 75 | build: { 76 | rollupOptions: { 77 | // external: ['jspdf', 'axios', 'html2canvas'], 78 | // external: ['axios'], 79 | plugins: [ 80 | /* globals, */ viteCompressionPlugin, 81 | viteImageminPlugin, 82 | visualizer({ open: true }) 83 | ], 84 | output: { 85 | chunkFileNames: 'js/[name]-[hash].js', 86 | entryFileNames: 'js/[name]-[hash].js', 87 | assetFileNames: '[ext]/[name]-[hash].[ext]', 88 | manualChunks(id) { 89 | if (id.includes('node_modules')) { 90 | return id.toString().split('node_modules/')[1].split('/')[0].toString() 91 | } 92 | } 93 | } 94 | } 95 | } 96 | }) 97 | } 98 | --------------------------------------------------------------------------------