├── .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 |
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 |
81 |
82 |
83 |
Alipay
84 |
85 |
86 |
87 |
88 | ## License
89 |
90 | MIT © [Coderlei](./license)
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codecv
2 |
3 | 这是一款制作简历的工具,它可以将你编写的 `markdown` 简历转为 `PDF`,海量模板。
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
10 |
11 |
浏览历史
12 |
13 |
19 | {{ history.title }}
20 |
21 |
22 | {{ numFormat(history.hot) }}
23 |
24 |
25 |
26 |
27 |
28 |
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 |
16 | 聊天室
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/comment-reply-msg/crm.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 |
38 |
39 |
40 | 来自:
41 | {{ formatTimefromNow(notif.replyContent.createTime) }}发布的
42 |
43 |
51 |
{{ notif.replyUserInfo.nickName }}
52 |
53 | 评论内容:
54 | {{ notif.replyContent.content }}
55 |
56 |
.....
57 |
58 |
59 |
60 |
$emit('queryData', page)"
64 | :page-size="10"
65 | :total="total"
66 | />
67 |
68 |
69 |
70 |
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 |
14 |
59 |
60 |
61 |
91 |
--------------------------------------------------------------------------------
/src/components/contact.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/empty.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
{{ title }}
9 |
10 |
11 |
12 |
26 |
--------------------------------------------------------------------------------
/src/components/exportTotal.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | 累计导出 {{ total }} 份
16 |
17 |
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 |
10 |
11 |
热门面经
12 |
13 |
19 | {{ idx + 1 }}
20 | {{ article.title }}
21 |
22 | {{ numFormat(article.hot) }}
23 |
24 |
25 |
26 |
27 |
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 |
11 |
16 |
17 |
18 |
83 |
--------------------------------------------------------------------------------
/src/components/menu-bar/menu-bar/MenuBar.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
37 |
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 |
17 |
38 |
39 |
40 |
66 |
--------------------------------------------------------------------------------
/src/components/pwd-update/PWDUpdate.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
20 | ps: 请保存好密码 切勿外传
21 |
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 |
37 |
38 |
45 |
46 |
47 |
48 | {{ item.font_class }}
49 |
50 |
51 |
52 |
53 |
62 |
63 |
64 |
65 |
100 |
--------------------------------------------------------------------------------
/src/components/reward.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | 打赏
13 |
14 | PS:该项目作为免费的开源项目使用,
15 |
16 | 如果你觉得对你有帮助,可以请作者喝杯奶茶支持一下,非常感谢
17 |
18 |
19 |
20 |
21 |
微信收款码
22 |
23 |
24 |
25 |
支付宝收款码
26 |
27 |
28 |
29 |
30 |
65 |
--------------------------------------------------------------------------------
/src/components/themeToggle.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/toast-modal/toastModal.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
39 |
--------------------------------------------------------------------------------
/src/components/userInfo.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{{ userInfo?.nickName }}
15 |
16 | {{ formatTimefromNow(publishTime as string) }}发布
19 |
20 |
21 | {{ userInfo?.school }} - {{ userInfo?.graduation }}届
22 |
23 |
24 |
25 |
26 |
27 |
28 |
67 |
--------------------------------------------------------------------------------
/src/components/userTooltip.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ userInfo.nickName }}
13 |
14 |
15 |
16 | 性别:
17 | {{ userInfo.sex }}生
18 |
19 |
20 | 院校:
21 | {{ userInfo.school }} -
22 | {{ userInfo.graduation }}届
23 |
24 |
25 | 求职意向:
26 | {{ userInfo.professional }}
27 |
28 |
29 | 所在地区:
30 | {{ userInfo.origin }}
31 |
32 |
33 |
34 |
35 |
64 |
--------------------------------------------------------------------------------
/src/layout/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
63 |
64 |
65 |
132 |
--------------------------------------------------------------------------------
/src/layout/header/components/nav.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
13 | {{ navItem.name }}
14 | {{ navItem.name }}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/layout/header/components/navMoblie.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
24 |
25 |
26 |
38 |
--------------------------------------------------------------------------------
/src/layout/header/header.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
19 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
2 | 404 抱歉~您要找的页面没有找到
3 |
4 |
--------------------------------------------------------------------------------
/src/views/community/community.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
17 |
18 |
19 |
42 |
--------------------------------------------------------------------------------
/src/views/community/components/community-left/communityLeft.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
71 |
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 |
19 |
20 |
21 | 《关于社区暂时停止服务的公告》
22 |
23 | 各位兄弟姐妹,之前的服务器已经到期了,所以社区暂时就不提供了,但是数据都是在的,以后恢复正常之后账号都是可以正常使用的!简历服务也是可以正常使用的,别说我跑路了😅!有问题或者有更好的建议可以联系我的微信,也欢迎更多志同道合的朋友一起来维护这个项目!
24 |
25 |
26 |
37 |
38 |
39 |
40 |
41 |
42 | {{ numFormat(99999) }}
43 |
44 |
45 |
46 | {{ numFormat(99999) }}
47 |
48 |
49 |
50 | 浏览量 {{ numFormat(99999) }}
51 |
52 |
53 |
#社区公告
54 |
55 |
56 |
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 |
7 |
11 |
12 |
13 |
20 |
--------------------------------------------------------------------------------
/src/views/community/views/editor/communityEditor.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
返回
12 |
19 |
20 |
21 |
27 |
28 |
29 |
发布
30 |
返回
31 |
32 |
33 |
34 |
67 |
--------------------------------------------------------------------------------
/src/views/download/index.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
52 |
--------------------------------------------------------------------------------
/src/views/editor/components/editor/editorContainer.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | .
23 | .
24 | .
25 |
26 |
27 |
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 |
19 |
20 | editorStore.setMDContent(nv, resumeType)"
35 | />
36 |
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 |
15 |
19 |
28 |
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 |
19 | 请输入需要插入的列数
20 |
21 |
22 |
23 |
24 | 确定
25 |
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 |
10 | {{ title }}
11 |
12 |
13 |
14 |
15 |
16 |
17 | 确定
18 |
19 |
--------------------------------------------------------------------------------
/src/views/editor/components/editor/toolbar/components/tableInput.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | 插入表格行列数
21 |
22 |
23 |
24 |
25 |
26 |
27 | 确定
28 |
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 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | markdownModeInsertIcon(icon)" />
39 |
40 |
41 | markdownModeInsertMultiColumnsLayout(c)" />
42 |
43 |
44 | markdownModeInsertTable(c, r)" />
45 |
46 |
47 |
48 |
81 |
--------------------------------------------------------------------------------
/src/views/editor/components/editor/toolbar/richTool.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {{ heading.label }}
41 |
42 |
43 |
44 |
45 | insertIcon(icon, emit)" />
46 |
47 |
48 | insertLink(l,t, emit)" />
49 |
50 |
51 | insertMulticolumn(c, emit)" />
52 |
53 |
54 | InsertTable(c, r, emit)" />
55 |
56 |
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 |
24 |
49 |
50 |
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 |
9 |
44 |
45 |
46 |
74 |
--------------------------------------------------------------------------------
/src/views/editor/components/preview/render.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
$emit('html-convert', cnt)"
18 | @upload-avatar="path => $emit('upload-avatar', path)"
19 | />
20 |
21 |
22 |
30 |
31 |
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 |
17 |
38 |
39 |
70 |
--------------------------------------------------------------------------------
/src/views/home/components/header.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
28 |
29 |
30 |
44 |
--------------------------------------------------------------------------------
/src/views/home/components/presentation.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
17 |
24 |
25 |
26 |
27 |
35 |
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 |
15 |
19 |
20 |
21 |
44 |
--------------------------------------------------------------------------------
/src/views/template/components/resumeCard.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 | {{ theme.hot }}
17 |
18 |
19 |
20 |
21 | 使用模板
22 |
23 | {{ theme.name }}
24 |
25 |
26 |
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 |
6 |
7 |
历史版本
8 |
9 |
15 | {{ line.version }}
16 | {{ line.content }}
17 |
18 |
19 |
20 |
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 |
--------------------------------------------------------------------------------
32 | {{ formatTimefromNow(notif.commentContent.createTime) }}回复你: 35 | {{ notif.commentContent.content }} 36 |
37 |