├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── deploy.yml │ └── mirror.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── gatsby-config.js ├── gatsby-node.js ├── i18n.config.js ├── i18n.formatter.js ├── package.json ├── pnpm-lock.yaml ├── scripts └── i18n-pick.js ├── src ├── components │ ├── .DS_Store │ ├── Avatar │ │ ├── index.less │ │ └── index.tsx │ ├── Drawer │ │ ├── ConfigTheme │ │ │ └── index.tsx │ │ ├── Templates │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── FormCreator │ │ ├── ColorPicker │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── index.tsx │ ├── LangSwitcher │ │ ├── index.less │ │ └── index.tsx │ ├── Resume │ │ ├── Template1 │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Template2 │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Template3 │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── index.tsx │ ├── index.less │ ├── index.tsx │ └── types.ts ├── data │ ├── constant.ts │ └── resume.ts ├── helpers │ ├── contant.tsx │ ├── copy-to-board.ts │ ├── customAssign.ts │ ├── detect-device.ts │ ├── export-to-local.ts │ ├── fetch-resume.ts │ ├── location.ts │ ├── store-to-local.ts │ └── template.ts ├── hooks │ ├── useModeSwitcher │ │ ├── index.less │ │ └── index.tsx │ └── useThrottle.ts ├── i18n │ ├── index.ts │ ├── language.ts │ ├── locales │ │ └── en-US.json │ └── types.ts ├── layout │ ├── footer.less │ ├── footer.tsx │ ├── header.less │ └── header.tsx └── pages │ ├── index.less │ └── index.tsx ├── static ├── favicon.ico ├── images │ ├── love.png │ ├── personal-skill.png │ ├── skill.png │ └── work-experience.png └── resume.svg └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript", "@babel/react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | [ 6 | "import", 7 | { 8 | "libraryName": "antd", 9 | "style": "less" 10 | }, 11 | "antd" 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/node_modules/** 3 | **/server.js 4 | **/webpack.config*.js 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | }, 7 | extends: 'eslint:recommended', 8 | parserOptions: { 9 | ecmaFeatures: { 10 | experimentalObjectRestSpread: true, 11 | jsx: true, 12 | }, 13 | sourceType: 'module', 14 | }, 15 | plugins: ['react'], 16 | rules: { 17 | 'linebreak-style': ['error', 'unix'], 18 | quotes: ['error', 'single'], 19 | semi: ['error', 'always'], 20 | 'no-unused-vars': 'off', // 关闭规则 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # Set a branch name to trigger deployment 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 12 17 | 18 | - run: npm install 19 | - run: npm run build 20 | 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_dir: ./public 26 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 Sync to Gitee Mirror 2 | 3 | on: [page_build, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: 🔁 Sync to Gitee 10 | uses: wearerequired/git-mirror-action@master 11 | env: 12 | # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY 13 | SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} 14 | with: 15 | # 注意替换为你的 GitHub 源仓库地址 16 | source-repo: git@github.com:visiky/resume.git 17 | # 注意替换为你的 Gitee 目标仓库地址 18 | destination-repo: git@gitee.com:visiky/resume.git 19 | 20 | - name: ✅ Build Gitee Pages 21 | uses: yanglbme/gitee-pages-action@master 22 | with: 23 | # 注意替换为你的 Gitee 用户名 24 | gitee-username: visiky 25 | # 注意在 Settings->Secrets 配置 GITEE_PASSWORD 26 | gitee-password: ${{ secrets.GITEE_PASSWORD }} 27 | # 参数默认是 master,若是其他分支,需要指定 28 | branch: gh-pages 29 | # 注意替换为你的 Gitee 仓库 30 | gitee-repo: visiky/resume 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Coverage directory used by tools like istanbul 7 | coverage 8 | 9 | # Dependency directories 10 | node_modules 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | .DS_Store 16 | 17 | # vscode editor 18 | .vscode 19 | 20 | # webstorm editor 21 | .idea 22 | 23 | # gatsby files 24 | .cache/ 25 | public 26 | 27 | # package lock file 28 | package-lock.json 29 | 30 | # temp 31 | **temp** 32 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": true, 4 | "semi": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kasmine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🧾 Resume Generator 2 | 3 | 在线简历生成器。无须 fork 仓库,即可在线预览、编辑和下载 PDF 简历。✨ [在线编辑](https://visiky.github.io/resume) 4 | 5 | 内置 3 套模板,支持**自定义主题颜色**、**自定义模块标题**、**国际化(中/英)** 等. 6 | 7 | |默认模板| 简易模板| 简易模板2(适用于多页)| 8 | | -------------------------------- | --------------------------------------------------|----------------------- | 9 | | || | 10 | |[Live Demo](https://visiky.github.io/resume?user=visiky) |[Live Demo](https://visiky.github.io/resume?user=visiky&template=template2)|[Live Demo](https://visiky.github.io/resume?user=visiky&template=template3) | 11 | 12 | ## 如何使用(How to use) 13 | 14 | **方式 1:** 15 | 16 | 在线编辑 -> 导出配置 -> 存储“简历信息”在个人 github special 仓库下(例如: [visiky/visiky](https://github.com/visiky/visiky/blob/master/resume.json)) 17 | 18 | **方式 2:** 19 | 20 | 直接创建一个 `resume.json` 文件在自己的 special 仓库下 (内容参考: [visiky/visiky](https://github.com/visiky/visiky/blob/master/resume.json)). 21 | 22 | **最后** 23 | 24 | 访问 https://visiky.github.io/resume?user={user}&branch={branch} 25 | 26 | 参数说明: 27 | 28 | | 参数 | 描述 | 默认值 | 29 | | ------ | ------------- | ------------ | 30 | | user | github 用户名 | 必选 | 31 | | template | 模板 | 默认: template1 | 32 | | branch | 分支名 | 默认: master | 33 | | mode | 模式 | 备注: 默认为‘只读’模式,设置为: `mode=edit` 即可进入编辑模式 | 34 | | lang | 语言 | 默认: zh-CN | 35 | 36 | ## 本地开发(Local develop) 37 | 38 | ```bash 39 | # pnpm required, to see: https://pnpm.io/installation 40 | # Install dependencies 41 | pnpm install 42 | # Then, start 43 | npm start 44 | ``` 45 | 46 | ## ✨ Recommendation 47 | 48 | - [resumemaker](https://www.resumemaker.online/es.php) 49 | - [Geek Resume - Pure Markdown, an online resume editor for developer.](https://www.jijian.press/) 50 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pathPrefix: '/resume', 3 | siteMetadata: { 4 | title: 'Resume Generator', 5 | githubUrl: 'https://github.com/visiky/resume.git', 6 | author: 'visiky', 7 | contact: 'https://github.com/visiky', 8 | }, 9 | flags: { 10 | DEV_SSR: false, 11 | }, 12 | plugins: [ 13 | { 14 | // https://developers.google.com/analytics/devguides/collection/gtagjs?hl=zh_CN 15 | resolve: `gatsby-plugin-google-gtag`, 16 | options: { 17 | // The property ID; the tracking code won't be generated without it 18 | trackingIds: ['G-2K3PH6MKBG'], 19 | }, 20 | }, 21 | { 22 | resolve: 'gatsby-plugin-antd', 23 | options: { 24 | style: true, 25 | }, 26 | }, 27 | { 28 | resolve: 'gatsby-plugin-less', 29 | options: { 30 | strictMath: true, 31 | lessOptions: { 32 | javascriptEnabled: true, 33 | modifyVars: { 34 | 'font-family': 'roboto-regular, Arial', 35 | 'primary-color': '#2f5785', 36 | }, 37 | }, 38 | }, 39 | }, 40 | // this (optional) plugin enables Progressive Web App + Offline functionality 41 | // To learn more, visit: https://gatsby.dev/offline 42 | // `gatsby-plugin-offline`, 43 | 'gatsby-plugin-pnpm', 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | exports.onCreateWebpackConfig = ({ actions, loaders, stage, getConfig }) => { 4 | const config = getConfig(); 5 | 6 | if (config.resolve) { 7 | config.resolve.alias = { 8 | ...config.resolve.alias, 9 | '@': path.resolve(__dirname, 'src'), 10 | }; 11 | } else { 12 | config.resolve = { 13 | alias: { '@': path.resolve(__dirname, 'src') }, 14 | }; 15 | } 16 | 17 | // This will completely replace the webpack config with the modified object. 18 | actions.replaceWebpackConfig(config); 19 | 20 | if (stage === 'build-html' || stage === 'develop-html') { 21 | actions.setWebpackConfig({ 22 | module: { 23 | rules: [ 24 | { 25 | test: /bad-module/, 26 | use: loaders.null(), 27 | }, 28 | ], 29 | }, 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /i18n.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | exclude: ['**/i18n/**', '**/data/**'], 3 | }; 4 | -------------------------------------------------------------------------------- /i18n.formatter.js: -------------------------------------------------------------------------------- 1 | exports.format = function (msgs) { 2 | const results = {}; 3 | for (const [id, msg] of Object.entries(msgs)) { 4 | // results[id] = { 5 | // string: msg.defaultMessage, 6 | // comment: msg.description, 7 | // }; 8 | results[id] = msg.defaultMessage || msg.description || id; 9 | } 10 | return results; 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resume", 3 | "version": "1.1.5", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.6.2", 7 | "antd": "^4.16.6", 8 | "array-move": "^3.0.1", 9 | "classnames": "^2.3.1", 10 | "cross-fetch": "^3.1.4", 11 | "gatsby-plugin-google-gtag": "^4.23.0", 12 | "json-url": "^3.0.0", 13 | "lodash-es": "^4.17.21", 14 | "query-string": "^7.1.1", 15 | "react": "^17.0.1", 16 | "react-color": "^2.19.3", 17 | "react-dnd": "^14.0.2", 18 | "react-dnd-html5-backend": "^14.0.0", 19 | "react-dom": "^17.0.1", 20 | "react-helmet": "^6.1.0", 21 | "react-intl": "^6.2.10", 22 | "react-svg": "^14.1.6" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.21.0", 26 | "@babel/plugin-proposal-class-properties": "^7.18.6", 27 | "@babel/plugin-proposal-decorators": "^7.21.0", 28 | "@babel/preset-env": "^7.20.2", 29 | "@babel/preset-react": "^7.18.6", 30 | "@babel/preset-typescript": "^7.21.0", 31 | "@formatjs/cli": "^6.0.4", 32 | "@types/lodash-es": "^4.17.6", 33 | "@types/react": "^17.0.3", 34 | "babel-plugin-import": "^1.13.3", 35 | "cross-env": "^7.0.3", 36 | "gatsby": "^2.32.13", 37 | "gatsby-plugin-antd": "^2.2.0", 38 | "gatsby-plugin-less": "^5.2.0", 39 | "gatsby-plugin-pnpm": "^1.2.10", 40 | "gh-pages": "^3.1.0", 41 | "glob": "^9.2.1", 42 | "husky": "^7.0.4", 43 | "less": "^4.1.0", 44 | "lint-staged": "^12.3.5", 45 | "prettier": "2.2.1", 46 | "rimraf": "^2.5.4", 47 | "ts-loader": "^8.0.18", 48 | "tslib": "^2.4.0", 49 | "typescript": "^4.2.3" 50 | }, 51 | "scripts": { 52 | "start": "cross-env NODE_DEV=development gatsby clean && gatsby develop", 53 | "build": "gatsby build --prefix-paths", 54 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 55 | "clean": "gatsby clean", 56 | "deploy": "npm run clean && gatsby build --prefix-paths && gh-pages -d public", 57 | "prepare": "husky install", 58 | "lint-staged": "lint-staged", 59 | "i18n-pick": "node ./scripts/i18n-pick.js", 60 | "extract": "formatjs extract 'src/**/*.ts*' --ignore '**/locales/**' --format i18n.formatter.js --out-file i18n.temp.json --flatten" 61 | }, 62 | "lint-staged": { 63 | "*.{ts,tsx}": [ 64 | "prettier --write" 65 | ] 66 | }, 67 | "repository": { 68 | "type": "git", 69 | "url": "git@github.com:visiky/resume.git" 70 | }, 71 | "keywords": [ 72 | "resume" 73 | ], 74 | "author": "visiky <736929286@qq.com>", 75 | "license": "MIT", 76 | "bugs": { 77 | "url": "https://github.com/visiky/resume/issues" 78 | }, 79 | "homepage": "https://github.com/visiky/resume#readme" 80 | } 81 | -------------------------------------------------------------------------------- /scripts/i18n-pick.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const fs = require('fs'); 3 | const { transformFileAsync } = require('@babel/core'); 4 | const config = require('../i18n.config.js'); 5 | 6 | const wordSet = new Set(); 7 | 8 | /** unicode cjk 中日韩文 范围 */ 9 | const DOUBLE_BYTE_REGEX = /[\u4E00-\u9FFF]/; 10 | function detectChinese(text, path) { 11 | if (DOUBLE_BYTE_REGEX.test(text)) { 12 | wordSet.add(text.trim().replace(/[\r\n]/g, '')); 13 | } 14 | } 15 | 16 | function scan({ types: t }) { 17 | return { 18 | visitor: { 19 | // 匹配: const text = '中文文案'; new Person('小红') 20 | StringLiteral(path) { 21 | const { node } = path; 22 | detectChinese(node.value, path); 23 | }, 24 | // 匹配: jsx 文本 & 属性 25 | JSXText(path) { 26 | detectChinese(path.node.value); 27 | }, 28 | }, 29 | }; 30 | } 31 | 32 | const { exclude = [] } = config; 33 | 34 | function run(path) { 35 | glob(`${path}/**/*.{js,jsx,ts,tsx}`, { 36 | ignore: exclude.concat('node_modules/**'), 37 | }) 38 | .then(files => { 39 | Promise.all( 40 | files.map(filename => { 41 | // todo 可以匹配一些规则,直接返回 42 | 43 | return transformFileAsync(filename, { 44 | plugins: [ 45 | // 装饰器插件 46 | ['@babel/plugin-proposal-decorators', { legacy: true }], 47 | scan, 48 | ], 49 | presets: [ 50 | [ 51 | '@babel/preset-typescript', 52 | // 强制开启 jsx 解析,否则尖括号可能会被识别为 typescript 的类型断言。如 `var foo = bar;` 53 | { isTSX: true, allExtensions: true }, 54 | ], 55 | ['@babel/preset-env', { targets: 'chrome > 58' }], 56 | ], 57 | }); 58 | }) 59 | ).then(() => { 60 | const langPath = 'src/i18n/locales/en-US.json'; 61 | fs.readFile(langPath, (err, d) => { 62 | if (err) return console.error(err); 63 | 64 | const content = JSON.parse(d.toString()); 65 | const actual = Object.assign( 66 | {}, 67 | Object.fromEntries(wordSet.entries()), 68 | content 69 | ); 70 | 71 | fs.writeFileSync( 72 | langPath, 73 | JSON.stringify(actual, null, 2), 74 | err => {} 75 | ); 76 | }); 77 | }); 78 | }) 79 | .catch(err => { 80 | console.error(err); 81 | }); 82 | } 83 | 84 | run('./src'); 85 | -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visiky/resume/2eb87e51ccdb63f0565d3d6935bd1b92fa2dbc00/src/components/.DS_Store -------------------------------------------------------------------------------- /src/components/Avatar/index.less: -------------------------------------------------------------------------------- 1 | .avatar { 2 | position: relative; 3 | 4 | .btn-upload { 5 | position: absolute; 6 | top: 50%; 7 | left: 50%; 8 | transform: translate(-50%, -50%); 9 | 10 | .ant-upload-list-item-info { 11 | display: none; 12 | } 13 | 14 | .ant-upload-list.ant-upload-list-picture-card { 15 | width: 82px; 16 | height: 82px; 17 | } 18 | 19 | .ant-upload-select.ant-upload-select-picture-card { 20 | height: calc(100% + 4px); 21 | width: calc(100% + 4px); 22 | margin-top: -2px; 23 | border-radius: 50%; 24 | } 25 | .ant-upload-list-picture-card-container { 26 | height: 100%; 27 | width: 100%; 28 | } 29 | 30 | .ant-upload-list-picture .ant-upload-list-item, 31 | .ant-upload-list-picture-card .ant-upload-list-item { 32 | border: none; 33 | } 34 | } 35 | 36 | &:hover { 37 | .btn-upload { 38 | display: block !important; 39 | } 40 | } 41 | 42 | .avatar-upload-tip { 43 | height: 100%; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | } 49 | 50 | @media print { 51 | .avatar.avatar-hidden { 52 | display: none!important; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Upload, Avatar as AntdAvatar } from 'antd'; 3 | import './index.less'; 4 | 5 | export const Avatar = ({ 6 | avatarSrc, 7 | className, 8 | shape = 'circle', 9 | size = 'default', 10 | }) => { 11 | return ( 12 |
13 | {avatarSrc ? ( 14 | // @ts-ignore 15 | 21 | ) : ( 22 | 头像地址为空 23 | )} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/Drawer/ConfigTheme/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl'; 2 | import React, { useEffect } from 'react'; 3 | import { ColorPicker } from '../../FormCreator/ColorPicker'; 4 | import type { ThemeConfig } from '../../types'; 5 | 6 | type Props = ThemeConfig & { 7 | onChange: (v: Partial) => void; 8 | }; 9 | 10 | const FormItemStyle = { 11 | display: 'flex', 12 | alignItems: 'center', 13 | minWidth: '100px', 14 | }; 15 | 16 | export const ConfigTheme: React.FC = props => { 17 | useEffect(() => { 18 | let $style = document.getElementById('dynamic'); 19 | if (!$style) { 20 | $style = document.createElement('style'); 21 | $style.setAttribute('id', 'dynamic'); 22 | document.head.insertBefore($style, null); 23 | } 24 | const styles = ` 25 | :root { 26 | --primary-color: ${props.color}; 27 | --tag-color: ${props.tagColor}; 28 | } 29 | `; 30 | $style.innerHTML = styles; 31 | }, [props.color, props.tagColor]); 32 | 33 | return ( 34 |
41 |
42 | 43 | 44 | 45 | props.onChange({ color: v })} 48 | /> 49 |
50 |
51 | 52 | 53 | 54 | props.onChange({ tagColor: v })} 57 | /> 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/Drawer/Templates/index.less: -------------------------------------------------------------------------------- 1 | .templates { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | grid-row-gap: 24px; 5 | margin: 24px 0; 6 | 7 | .template-item { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | 12 | .template-id { 13 | font-size: 14px; 14 | color: rgba(0, 0, 0, 0.65); 15 | } 16 | .template-description { 17 | font-size: 12px; 18 | color: rgba(0, 0, 0, 0.45); 19 | } 20 | 21 | svg.template { 22 | width: 160px; 23 | border-radius: 4px; 24 | border: 1px solid #efefef; 25 | } 26 | 27 | &.selected svg.template { 28 | border: 1px solid #b3b3b3; 29 | } 30 | 31 | &:hover { 32 | cursor: pointer; 33 | } 34 | &.disabled:hover { 35 | cursor: not-allowed; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Drawer/Templates/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactSVG } from 'react-svg'; 3 | import cx from 'classnames'; 4 | import { useIntl } from 'react-intl'; 5 | import './index.less'; 6 | 7 | type Props = { 8 | template: string; 9 | onChange: (v: string) => void; 10 | }; 11 | 12 | const TEMPLATES = [ 13 | { 14 | url: 'https://gw.alipayobjects.com/zos/antfincdn/GLDkiGBSPl/moban1.svg', 15 | id: 'template1', 16 | description: '默认模板(适用于单页)', 17 | }, 18 | { 19 | url: 'https://gw.alipayobjects.com/zos/antfincdn/RGxVcJ2O3q/moban2.svg', 20 | id: 'template2', 21 | description: '简易模板', 22 | }, 23 | { 24 | url: 'https://gw.alipayobjects.com/zos/antfincdn/Kn2jUKcBme/moban2.svg', 25 | id: 'template3', 26 | description: '简易模板(适用于多页)', 27 | disabled: false, 28 | }, 29 | ]; 30 | 31 | export const Templates: React.FC = props => { 32 | const intl = useIntl(); 33 | 34 | return ( 35 |
36 | {TEMPLATES.map(item => { 37 | return ( 38 |
!item.disabled && props.onChange(item.id)} 45 | > 46 | { 49 | svg.setAttribute('class', 'template'); 50 | }} 51 | /> 52 | {item.id} 53 | 54 | {intl.formatMessage({ id: item.description })} 55 | 56 |
57 | ); 58 | })} 59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/Drawer/index.less: -------------------------------------------------------------------------------- 1 | .module-list { 2 | .module-item { 3 | > div:first-child { 4 | &:hover { 5 | cursor: pointer; 6 | } 7 | .item-icon { 8 | margin-right: 8px; 9 | } 10 | .item-name { 11 | font-family: roboto-medium; 12 | } 13 | } 14 | } 15 | 16 | .ant-collapse > .ant-collapse-item > .ant-collapse-header { 17 | padding: 4px 0; 18 | width: auto; 19 | .ant-collapse-header-text{ 20 | display: flex; 21 | } 22 | } 23 | 24 | .ant-collapse-ghost 25 | > .ant-collapse-item 26 | > .ant-collapse-content 27 | > .ant-collapse-content-box { 28 | padding: 4px 0; 29 | } 30 | 31 | // radio.group 复写 32 | .ant-radio-button-wrapper { 33 | border: none !important; 34 | } 35 | .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before { 36 | background: none !important; 37 | } 38 | // radio.group 复写 39 | 40 | .no-content-panel { 41 | .ant-collapse-content-box { 42 | display: none; 43 | } 44 | } 45 | 46 | .list-value-item { 47 | margin-left: 20px; 48 | 49 | > div:not(.btn-append) { 50 | width: 100%; 51 | padding: 4px 0; 52 | background: rgba(0, 0, 0, 0.04); 53 | border-radius: 4px; 54 | color: rgba(0, 0, 0, 0.65); 55 | font-size: 12px; 56 | overflow: hidden; 57 | white-space: nowrap; 58 | text-overflow: ellipsis; 59 | 60 | margin: 8px 0; 61 | padding: 4px 12px; 62 | } 63 | } 64 | 65 | .btn-append { 66 | color: rgba(0, 0, 0, 0.45); 67 | text-align: center; 68 | font-size: 12px; 69 | padding: 8px 0; 70 | 71 | &:hover { 72 | background: rgba(0, 0, 0, 0.05); 73 | } 74 | } 75 | 76 | .list-value-item { 77 | > div { 78 | position: relative; 79 | 80 | .anticon-delete { 81 | position: absolute; 82 | right: 0; 83 | top: 0; 84 | background: #fff; 85 | padding: 7.5px 8px; 86 | border-radius: 2px; 87 | display: none; 88 | } 89 | 90 | &:hover .anticon-delete { 91 | display: inline-block; 92 | } 93 | } 94 | } 95 | } 96 | 97 | // drawer 复写 98 | .ant-drawer-header, .ant-drawer-body { 99 | padding: 12px 24px; 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useMemo } from 'react'; 2 | import { 3 | Drawer as AntdDrawer, 4 | Button, 5 | Collapse, 6 | Modal, 7 | Radio, 8 | Popover, 9 | Input, 10 | } from 'antd'; 11 | import { DeleteFilled, InfoCircleFilled } from '@ant-design/icons'; 12 | import { DndProvider, useDrag, useDrop } from 'react-dnd'; 13 | import { HTML5Backend } from 'react-dnd-html5-backend'; 14 | import _ from 'lodash-es'; 15 | import arrayMove from 'array-move'; 16 | import { FormCreator } from '../FormCreator'; 17 | import { getDefaultTitleNameMap } from '@/data/constant'; 18 | import { FormattedMessage, useIntl } from 'react-intl'; 19 | import { MODULES, CONTENT_OF_MODULE } from '@/helpers/contant'; 20 | import type { ResumeConfig, ThemeConfig } from '../types'; 21 | import { ConfigTheme } from './ConfigTheme'; 22 | import { Templates } from './Templates'; 23 | import './index.less'; 24 | import useThrottle from '@/hooks/useThrottle'; 25 | 26 | const { Panel } = Collapse; 27 | 28 | type Props = { 29 | value: ResumeConfig; 30 | onValueChange: (v: Partial) => void; 31 | theme: ThemeConfig; 32 | onThemeChange: (v: Partial) => void; 33 | template: string; 34 | onTemplateChange: (v: string) => void; 35 | 36 | style?: object; 37 | }; 38 | 39 | const type = 'DragableBodyRow'; 40 | 41 | const DragableRow = ({ index, moveRow, ...restProps }) => { 42 | const ref = useRef(); 43 | const [{ isOver, dropClassName }, drop] = useDrop({ 44 | accept: type, 45 | collect: monitor => { 46 | // @ts-ignore 47 | const { index: dragIndex } = monitor.getItem() || {}; 48 | if (dragIndex === index) { 49 | return {}; 50 | } 51 | return { 52 | isOver: monitor.isOver(), 53 | dropClassName: 54 | dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', 55 | }; 56 | }, 57 | drop: item => { 58 | // @ts-ignore 59 | moveRow(item.index, index); 60 | }, 61 | }); 62 | const [, drag] = useDrag({ 63 | type, 64 | item: { index }, 65 | collect: monitor => ({ 66 | isDragging: monitor.isDragging(), 67 | }), 68 | }); 69 | drop(drag(ref)); 70 | 71 | return ( 72 |
78 | ); 79 | }; 80 | 81 | /** 82 | * @description 简历配置区 83 | */ 84 | export const Drawer: React.FC = props => { 85 | const intl = useIntl(); 86 | 87 | const [visible, setVisible] = useState(false); 88 | const [childrenDrawer, setChildrenDrawer] = useState(null); 89 | const [currentContent, updateCurrentContent] = useState(null); 90 | 91 | /** 92 | * 1. 更新currentContent State 93 | * 2. 调用 props.onValueChange 更新模板 94 | */ 95 | const updateContent = useThrottle( 96 | v => { 97 | const newConfig = _.merge({}, currentContent, v); 98 | updateCurrentContent(newConfig); 99 | props.onValueChange({ 100 | [childrenDrawer]: newConfig, 101 | }); 102 | }, 103 | [currentContent], 104 | 800 105 | ); 106 | 107 | const [type, setType] = useState('template'); 108 | 109 | const swapItems = (moduleKey: string, oldIdx: number, newIdx: number) => { 110 | const newValues = _.clone(_.get(props.value, moduleKey, [])); 111 | props.onValueChange({ 112 | [moduleKey]: arrayMove(newValues, newIdx, oldIdx), 113 | }); 114 | }; 115 | 116 | const deleteItem = (moduleKey: string, idx: number) => { 117 | const newValues = _.get(props.value, moduleKey, []); 118 | props.onValueChange({ 119 | [moduleKey]: newValues.slice(0, idx).concat(newValues.slice(idx + 1)), 120 | }); 121 | }; 122 | 123 | const modules = useMemo(() => { 124 | const titleNameMap = props.value?.titleNameMap; 125 | return MODULES({ intl, titleNameMap }); 126 | }, [intl, props.value?.titleNameMap]); 127 | 128 | const contentOfModule = useMemo(() => { 129 | return CONTENT_OF_MODULE({ intl }); 130 | }, [intl]); 131 | 132 | const DEFAULT_TITLE_MAP = getDefaultTitleNameMap({ intl }); 133 | const isList = _.endsWith(childrenDrawer, 'List'); 134 | 135 | // #region 1 render: moduleContent 136 | 137 | // #region 1.1 render: ModuleList 138 | const renderModuleList = ({ icon, key, name }, idx, values) => { 139 | const header = ( 140 | <> 141 | {icon} 142 | 143 | {DEFAULT_TITLE_MAP[key] ? ( 144 | { 149 | props.onValueChange({ 150 | titleNameMap: { 151 | ...(props.value.titleNameMap || {}), 152 | [key]: e.target.value, 153 | }, 154 | }); 155 | }} 156 | style={{ padding: 0 }} 157 | /> 158 | ) : ( 159 | name 160 | )} 161 | 162 | 163 | ); 164 | 165 | const list = _.map(values, (value, idx: number) => ( 166 | swapItems(key, oldIdx, newIdx)} 170 | > 171 |
{ 173 | setChildrenDrawer(key); 174 | updateCurrentContent({ 175 | ...value, 176 | dataIndex: idx, 177 | }); 178 | }} 179 | > 180 | {`${idx + 1}. ${Object.values(value || {}).join(' - ')}`} 181 |
182 | { 184 | Modal.confirm({ 185 | content: intl.formatMessage({ id: '确认删除' }), 186 | onOk: () => deleteItem(key, idx), 187 | }); 188 | }} 189 | /> 190 |
191 | )); 192 | 193 | return ( 194 |
195 | 196 | 197 |
198 | {list} 199 |
{ 202 | setChildrenDrawer(key); 203 | updateCurrentContent(null); 204 | }} 205 | > 206 | 207 |
208 |
209 |
210 |
211 |
212 | ); 213 | }; 214 | // #endregion 215 | 216 | // #region 1.2 render: ModuleListItem when !_.endsWith(module.key,'List') 217 | const renderModuleListItem = ({ icon, key, name }) => ( 218 |
219 | ( 223 | 224 | )} 225 | > 226 | { 230 | updateCurrentContent(_.get(props.value, key)); 231 | setChildrenDrawer(key); 232 | }} 233 | > 234 | {icon} 235 | {name} 236 | 237 | } 238 | className="no-content-panel" 239 | key="no-content-panel__renderModuleListItem" 240 | /> 241 | 242 |
243 | ); 244 | // #endregion 245 | 246 | const moduleContent = ( 247 | 248 |
249 | {modules.map((module, idx) => { 250 | if (!_.endsWith(module.key, 'List')) { 251 | return renderModuleListItem(module); 252 | } 253 | const values = _.get(props.value, module.key, []); 254 | return renderModuleList(module, idx, values); 255 | })} 256 |
257 | m.key === childrenDrawer)?.name} 259 | width={450} 260 | onClose={() => setChildrenDrawer(null)} 261 | visible={!!childrenDrawer} 262 | > 263 | { 268 | if (isList) { 269 | const newValue = _.get(props.value, childrenDrawer, []); 270 | if (currentContent) { 271 | newValue[currentContent.dataIndex] = _.merge( 272 | {}, 273 | currentContent, 274 | v 275 | ); 276 | } else { 277 | newValue.push(v); 278 | } 279 | props.onValueChange({ 280 | [childrenDrawer]: newValue, 281 | }); 282 | // 关闭抽屉 283 | setChildrenDrawer(null); 284 | // 清空当前选中内容 285 | updateCurrentContent(null); 286 | } else { 287 | updateContent(v); 288 | } 289 | }} 290 | /> 291 | 292 |
293 | ); 294 | 295 | // #endregion 296 | 297 | return ( 298 | <> 299 | 313 | setType(e.target.value)}> 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | } 324 | width={480} 325 | closable={false} 326 | onClose={() => setVisible(false)} 327 | visible={visible} 328 | > 329 | {type === 'module' ? ( 330 | moduleContent 331 | ) : ( 332 | // type === 'theme' 333 | <> 334 | props.onThemeChange(v)} 337 | /> 338 | props.onTemplateChange(v)} 341 | /> 342 | 343 | )} 344 | 345 | 346 | ); 347 | }; 348 | -------------------------------------------------------------------------------- /src/components/FormCreator/ColorPicker/index.less: -------------------------------------------------------------------------------- 1 | .color-block { 2 | width: 16px; 3 | height: 16px; 4 | border-radius: 2px; 5 | cursor: pointer; 6 | } 7 | 8 | .color-picker-overlay { 9 | // 重写颜色选择器样式 10 | :global { 11 | // 中间颜色滑轨部分 12 | .flexbox-fix:nth-last-child(3) { 13 | &>:last-child { 14 | display: none; 15 | } 16 | } 17 | 18 | // 中间 rgb 手动输入部分 19 | .flexbox-fix:nth-last-child(2) { 20 | input { 21 | width: 100% !important; 22 | color: rgba(0,0,0,0.65); 23 | text-align: center; 24 | } 25 | label { 26 | color: rgba(0,0,0,0.45) !important; 27 | } 28 | } 29 | 30 | // 底部推荐色部分 31 | .flexbox-fix:last-child { 32 | &>div { 33 | width: 16px !important; 34 | height: 16px !important; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/FormCreator/ColorPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown } from 'antd'; 2 | import React, { useMemo, useCallback } from 'react'; 3 | import { SketchPicker } from 'react-color'; 4 | import cx from 'classnames'; 5 | import _ from 'lodash-es'; 6 | import './index.less'; 7 | 8 | const DEFAULT_COLORS = [ 9 | '#F4664A', 10 | '#E86452', 11 | '#FF9845', 12 | '#FAAD14', 13 | '#F6BD16', 14 | '#5AD8A6', 15 | '#30BF78', 16 | '#6DC8EC', 17 | '#5B8FF9', 18 | '#1E9493', 19 | '#945FB9', 20 | '#FF99C3', 21 | '#5D7092', 22 | ]; 23 | 24 | type ColorPickerProps = { 25 | value: string; 26 | onChange?: (value) => void; 27 | canChangeColor?: boolean; 28 | style?: React.CSSProperties; 29 | className?: string; 30 | }; 31 | 32 | export const ColorPicker: React.FC = props => { 33 | const { 34 | value: color, 35 | onChange, 36 | style, 37 | canChangeColor = true, 38 | className, 39 | } = props; 40 | 41 | const onColorChange = useCallback( 42 | newColor => { 43 | const { 44 | rgb: { r, g, b, a }, 45 | } = newColor; 46 | const alpha = 47 | color === undefined || _.lowerCase(color) === 'transparent' ? 1 : a; 48 | onChange?.(`rgba(${r}, ${g}, ${b}, ${alpha})`); 49 | }, 50 | [onChange, color] 51 | ); 52 | 53 | const overlay = useMemo(() => { 54 | return ( 55 | 61 | ); 62 | }, [onColorChange, color]); 63 | return ( 64 | 69 |
73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/FormCreator/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Form, Input, InputNumber, Button, Checkbox, Select } from 'antd'; 3 | import { FormItemProps } from 'antd/lib/form'; 4 | import _ from 'lodash-es'; 5 | import { ColorPicker } from './ColorPicker'; 6 | import { FormattedMessage } from 'react-intl'; 7 | 8 | type Props = { 9 | /** 表单配置 */ 10 | config: Array<{ 11 | type: string /** 组件类型 */; 12 | attributeId: string; 13 | displayName: string; 14 | formItemProps?: FormItemProps; 15 | cfg?: { 16 | [k: string]: any /**其它和组件本身有关的配置 */; 17 | }; 18 | }>; 19 | /** 表单已配置内容 */ 20 | value: { 21 | [key: string]: any; 22 | }; 23 | onChange: (v: any) => void; 24 | /** 列表型内容 */ 25 | isList: boolean; 26 | }; 27 | 28 | const FormItemComponentMap = (type: string) => ( 29 | props: { value: any; onChange?: (v) => void } = { value: null } 30 | ) => { 31 | switch (type) { 32 | case 'checkbox': 33 | return ; 34 | case 'select': 35 | return ; 38 | case 'number': 39 | return ; 40 | case 'textArea': 41 | return ; 42 | case 'color-picker': 43 | return ; 44 | default: 45 | return ; 46 | } 47 | }; 48 | 49 | export const FormCreator: React.FC = props => { 50 | const [fields, setFields] = useState([]); 51 | 52 | useEffect(() => { 53 | const datas = Object.keys(props.value || {}).map(d => ({ 54 | name: [d], 55 | value: props.value[d], 56 | })); 57 | setFields(datas); 58 | }, [props.value]); 59 | 60 | const handleChange = (values: any) => { 61 | if ('edu_time' in values && typeof values.edu_time === 'string') { 62 | values.edu_time = values.edu_time.split(','); 63 | } 64 | if ('work_time' in values) { 65 | values.work_time = values.work_time.split(','); 66 | } 67 | props.onChange(values); 68 | }; 69 | const formProps = { 70 | [props.isList ? 'onFinish' : 'onValuesChange']: handleChange, 71 | }; 72 | 73 | return ( 74 |
75 |
81 | {_.map(props.config, c => { 82 | return ( 83 | 90 | {FormItemComponentMap(c.type)({ 91 | ...(c.cfg || {}), 92 | value: _.get(props.value, [c.attributeId]), 93 | })} 94 | 95 | ); 96 | })} 97 | {props.isList && ( 98 | 99 | 102 | 103 | )} 104 |
105 |
106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /src/components/LangSwitcher/index.less: -------------------------------------------------------------------------------- 1 | .language-switcher { 2 | margin: 0 7px; 3 | display: inline-block; 4 | font-size: 12px; 5 | 6 | .divider { 7 | padding: 0 4px; 8 | } 9 | } 10 | 11 | body[lang='en-US'] { 12 | .language-switcher span[data-lang='zh-CN'] { 13 | color: rgba(255, 255, 255, 0.25); 14 | cursor: pointer; 15 | } 16 | } 17 | body[lang='zh-CN'] { 18 | .language-switcher span[data-lang='en-US'] { 19 | color: rgba(255, 255, 255, 0.25); 20 | cursor: pointer; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/LangSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import { Popover } from 'antd'; 4 | import qs from 'query-string'; 5 | import { getLanguage } from '@/i18n'; 6 | import { getMode } from '@/hooks/useModeSwitcher'; 7 | import './index.less'; 8 | import { useIntl } from 'react-intl'; 9 | 10 | export const LangSwitcher = ({ className }: { className?: string }) => { 11 | const lang = getLanguage(); 12 | const mode = getMode(); 13 | const intl = useIntl(); 14 | 15 | const changeLanguage = value => { 16 | if (value === lang) return; 17 | 18 | const { 19 | pathname, 20 | hash: currentHash, 21 | search: currentSearch, 22 | } = window.location; 23 | const hash = currentHash === '#/' ? '' : currentHash; 24 | const search = qs.stringify({ 25 | ...qs.parse(currentSearch), 26 | lang: value, 27 | }); 28 | window.location.href = `${pathname}?${search}${hash}`; 29 | }; 30 | 31 | const RadioContent = ( 32 | 33 | changeLanguage('zh-CN')} 36 | data-lang="zh-CN" 37 | > 38 | 中 39 | 40 | / 41 | changeLanguage('en-US')} 44 | data-lang="en-US" 45 | > 46 | En 47 | 48 | 49 | ); 50 | 51 | return ( 52 |
53 | {mode === 'edit' ? ( 54 | 60 | {RadioContent} 61 | 62 | ) : ( 63 | RadioContent 64 | )} 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/Resume/Template1/index.less: -------------------------------------------------------------------------------- 1 | // 响应式布局 2 | // 内容 3 | .template1-resume { 4 | width: 794px; 5 | min-height: 942px; 6 | margin-bottom: 60px; 7 | display: grid; 8 | grid-template-columns: 2fr 3fr; 9 | 10 | @media (max-width: 794px) { 11 | width: 100%; 12 | 13 | grid-template-columns: 1fr; 14 | } 15 | } 16 | 17 | @media print { 18 | @page { 19 | size: A4; 20 | } 21 | 22 | .template1-resume { 23 | width: 100%; 24 | display: grid; 25 | grid-template-columns: 2fr 3fr; 26 | box-shadow: none; 27 | } 28 | } 29 | 30 | // 内容 31 | .template1-resume { 32 | // 加点阴影 33 | box-shadow: 0px 2px 4px 1px rgba(0, 0, 0, 0.15); 34 | 35 | // 布局位置 36 | .basic-info { 37 | padding: 24px 18px 24px 24px; 38 | } 39 | 40 | .main-info { 41 | height: 100%; 42 | padding: 33px 24px 32px 20px; 43 | background: #f2f2f2; 44 | } 45 | 46 | // 内容样式 通用 47 | section { 48 | &:not(:first-of-type) { 49 | margin-top: 24px; 50 | } 51 | 52 | .section-title { 53 | margin-bottom: 12px; 54 | font-size: 24px; 55 | line-height: 32px; 56 | letter-spacing: 0; 57 | } 58 | 59 | .section-info { 60 | font-size: 18px; 61 | line-height: 24px; 62 | color: rgba(0, 0, 0, 0.85); 63 | margin-bottom: 8px; 64 | 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: center; 68 | } 69 | } 70 | 71 | .basic-info section .section-info { 72 | .info-name { 73 | max-width: 198px; 74 | padding-right: 4px; 75 | } 76 | } 77 | 78 | // basic-info 内容样式 79 | .basic-info .avatar { 80 | margin: 12px auto; 81 | display: block; 82 | width: 84px; 83 | height: 84px; 84 | } 85 | 86 | .basic-info .name { 87 | margin: 8px auto 24px; 88 | font-size: 24px; 89 | 90 | text-align: center; 91 | } 92 | 93 | .basic-info .profile .profile-list { 94 | margin-bottom: 24px; 95 | 96 | > div{ 97 | display: flex; 98 | align-items: center; 99 | } 100 | > div:not(:last-of-type) { 101 | margin-bottom: 4px; 102 | } 103 | 104 | .anticon { 105 | margin-right: 8px; 106 | } 107 | } 108 | 109 | // 非通用,skill 110 | .basic-info .section-skill { 111 | .section-info { 112 | margin-top: 12px; 113 | } 114 | 115 | .skill-rate { 116 | font-size: 14px; 117 | display: flex; 118 | 119 | .ant-rate-star { 120 | margin-right: 4px; 121 | } 122 | } 123 | 124 | .skill-detail-item { 125 | margin-top: 4px; 126 | } 127 | } 128 | 129 | // 非通用 130 | .basic-info section .sub-info { 131 | color: rgba(0, 0, 0, 0.45); 132 | } 133 | 134 | .basic-info .section-award { 135 | .sub-info { 136 | font-size: 14px; 137 | margin-left: 8px; 138 | } 139 | } 140 | 141 | .basic-info .education-item { 142 | &:not(:first-of-type) { 143 | margin-top: 8px; 144 | } 145 | } 146 | 147 | .basic-info .section-work { 148 | > div { 149 | line-height: 24px; 150 | } 151 | 152 | a.sub-info { 153 | font-size: 12px; 154 | margin-left: 8px; 155 | } 156 | } 157 | 158 | // main-info 内容样式 159 | .main-info section { 160 | margin-bottom: 16px; 161 | 162 | .section-header { 163 | display: flex; 164 | margin-bottom: 10px; 165 | 166 | img { 167 | margin-right: 8px; 168 | } 169 | 170 | h1 { 171 | position: relative; 172 | font-size: 18px; 173 | font-weight: bold; 174 | color: #fff; 175 | width: 200px; 176 | width: max-content; 177 | line-height: 26px; 178 | padding: 0 100px 0 10px; 179 | border-top-left-radius: 3px; 180 | border-bottom-left-radius: 3px; 181 | } 182 | h1:before { 183 | content: ''; 184 | display: block; 185 | width: 18.4px; 186 | height: 18.4px; 187 | background: #f2f2f2; 188 | position: absolute; 189 | right: -9px; 190 | top: 4px; 191 | transform: rotate(45deg); 192 | } 193 | } 194 | 195 | .section-detail { 196 | letter-spacing: 1.2px; 197 | 198 | &:not(:first-of-type) { 199 | margin-top: 4px; 200 | } 201 | } 202 | } 203 | 204 | // 非通用 205 | .main-info .section-project, 206 | .main-info .section-work-exp { 207 | .section-info .ant-tag { 208 | line-height: 20px; 209 | height: 20px; 210 | border: none; 211 | } 212 | 213 | .section-item:not(:first-of-type) { 214 | margin-top: 18px; 215 | } 216 | 217 | .section-info .sub-info, 218 | .section-info .info-time { 219 | color: rgba(0, 0, 0, 0.45); 220 | font-size: 12px; 221 | margin-left: 8px; 222 | font-weight: 300; 223 | } 224 | 225 | .work-description, 226 | .project-content { 227 | white-space: pre-wrap; 228 | } 229 | } 230 | 231 | .section-aboutme > div { 232 | margin: 6px 0; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/components/Resume/Template1/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Rate, Tag } from 'antd'; 3 | import { 4 | MobileFilled, 5 | MailFilled, 6 | GithubFilled, 7 | ZhihuCircleFilled, 8 | TrophyFilled, 9 | CheckCircleFilled, 10 | ScheduleFilled, 11 | CrownFilled, 12 | EnvironmentFilled, 13 | HeartFilled, 14 | } from '@ant-design/icons'; 15 | import _ from 'lodash-es'; 16 | import { FormattedMessage, useIntl } from 'react-intl'; 17 | import { getDefaultTitleNameMap } from '@/data/constant'; 18 | import { Avatar } from '../../Avatar'; 19 | import type { ResumeConfig, ThemeConfig } from '../../types'; 20 | import './index.less'; 21 | 22 | type Props = { 23 | value: ResumeConfig; 24 | theme: ThemeConfig; 25 | }; 26 | 27 | const wrapper = ({ id, title, color }) => WrappedComponent => { 28 | return ( 29 |
30 |
31 | {id && ( 32 | 33 | )} 34 |

{title}

35 |
36 |
{WrappedComponent}
37 |
38 | ); 39 | }; 40 | 41 | /** 42 | * @description 简历内容区 43 | */ 44 | export const Template1: React.FC = props => { 45 | const intl = useIntl(); 46 | const { value, theme } = props; 47 | 48 | /** 个人基础信息 */ 49 | const profile = _.get(value, 'profile'); 50 | 51 | const titleNameMap = _.get( 52 | value, 53 | 'titleNameMap', 54 | getDefaultTitleNameMap({ intl }) 55 | ); 56 | 57 | /** 教育背景 */ 58 | const educationList = _.get(value, 'educationList'); 59 | 60 | /** 工作经历 */ 61 | const workExpList = _.get(value, 'workExpList'); 62 | 63 | /** 项目经验 */ 64 | const projectList = _.get(value, 'projectList'); 65 | 66 | /** 个人技能 */ 67 | const skillList = _.get(value, 'skillList'); 68 | 69 | /** 更多信息 */ 70 | const awardList = _.get(value, 'awardList'); 71 | 72 | /** 作品 */ 73 | const workList = _.get(value, 'workList'); 74 | 75 | /** 自我介绍 */ 76 | const aboutme = _.split(_.get(value, ['aboutme', 'aboutme_desc']), '\n'); 77 | 78 | return ( 79 |
80 |
81 | {/* 头像 */} 82 | {!value?.avatar?.hidden && ( 83 | 89 | )} 90 | {/* 个人信息 */} 91 |
92 | {profile?.name &&
{profile.name}
} 93 |
94 | {profile?.mobile && ( 95 |
96 | 97 | {profile.mobile} 98 |
99 | )} 100 | {profile?.email && ( 101 |
102 | 103 | {profile.email} 104 |
105 | )} 106 | {profile?.github && ( 107 |
108 | 109 | { 112 | window.open(profile.github); 113 | }} 114 | > 115 | {profile.github} 116 | 117 |
118 | )} 119 | {profile?.zhihu && ( 120 |
121 | 124 | { 127 | window.open(profile.zhihu); 128 | }} 129 | > 130 | {profile.zhihu} 131 | 132 |
133 | )} 134 | {profile?.workExpYear && ( 135 |
136 | 137 | 138 | : {profile.workExpYear} 139 | 140 |
141 | )} 142 | {profile?.workPlace && ( 143 |
144 | 147 | 148 | : {profile.workPlace} 149 | 150 |
151 | )} 152 | {profile?.positionTitle && ( 153 |
154 | 155 | 156 | : {profile.positionTitle} 157 | 158 |
159 | )} 160 |
161 |
162 | {/* 自我介绍 */} 163 | {!!_.trim(_.join(aboutme, '')) && ( 164 |
165 |
166 | 167 |
168 | {aboutme.map((d, idx) => ( 169 |
{d}
170 | ))} 171 |
172 | )} 173 | {/* 教育背景 */} 174 | {educationList?.length ? ( 175 |
176 |
177 | {/* */} 178 | {titleNameMap?.educationList} 179 |
180 | {educationList.map((education, idx) => { 181 | const [start, end] = education.edu_time; 182 | return ( 183 |
184 |
185 | {education.school} 186 | 187 | {start} 188 | {end ? ` ~ ${end}` : } 189 | 190 |
191 |
192 | {education.major && {education.major}} 193 | {education.academic_degree && ( 194 | 195 | ({education.academic_degree}) 196 | 197 | )} 198 |
199 |
200 | ); 201 | })} 202 |
203 | ) : null} 204 | {workList?.length ? ( 205 |
206 |
207 | {/* */} 208 | {titleNameMap?.workList} 209 |
210 | {workList.map((work, idx) => { 211 | return ( 212 |
213 |
214 | 217 | {work.work_name} 218 | 219 | 220 | 221 |
222 | {work.work_desc &&
{work.work_desc}
} 223 |
224 | ); 225 | })} 226 |
227 | ) : null} 228 | {/* 专业技能 */} 229 | {skillList?.length ? ( 230 |
231 |
232 | {/* */} 233 | {titleNameMap?.skillList} 234 |
235 | {skillList.map((skill, idx) => { 236 | return skill ? ( 237 | 238 |
246 | {skill.skill_name} 247 | 253 |
254 | {_.split(skill.skill_desc, '\n').map((d, idx) => 255 | d ? ( 256 |
257 | 260 | {d} 261 |
262 | ) : null 263 | )} 264 |
265 | ) : null; 266 | })} 267 |
268 | ) : null} 269 | {/* 更多信息 */} 270 | {awardList?.length ? ( 271 |
272 |
273 | {/* */} 274 | {titleNameMap?.awardList} 275 |
276 | {awardList.map((award, idx) => { 277 | return ( 278 |
279 | 282 | {award.award_info} 283 | {award.award_time && ( 284 | 285 | ({award.award_time}) 286 | 287 | )} 288 |
289 | ); 290 | })} 291 |
292 | ) : null} 293 |
294 |
295 | {workExpList?.length 296 | ? wrapper({ 297 | id: 'work-experience', 298 | title: titleNameMap?.workExpList, 299 | color: theme.color, 300 | })( 301 |
302 | {_.map(workExpList, (work, idx) => { 303 | const [start = null, end = null] = 304 | typeof work.work_time === 'string' 305 | ? `${work.work_time || ''}`.split(',') 306 | : work.work_time; 307 | return work ? ( 308 |
309 |
310 | 311 | {work.company_name} 312 | 313 | {work.department_name} 314 | 315 | 316 | 317 | {start} 318 | {end ? ` ~ ${end}` : } 319 | 320 |
321 |
{work.work_desc}
322 |
323 | ) : null; 324 | })} 325 |
326 | ) 327 | : null} 328 | 329 | {projectList?.length 330 | ? wrapper({ 331 | id: 'skill', 332 | title: titleNameMap?.projectList, 333 | color: theme.color, 334 | })( 335 |
336 | {_.map(projectList, (project, idx) => 337 | project ? ( 338 |
339 |
340 | 341 | {project.project_name} 342 | 343 | {project.project_time} 344 | 345 | 346 | {project.project_role && ( 347 | 348 | {project.project_role} 349 | 350 | )} 351 |
352 |
353 | 354 | : 355 | 356 | {project.project_desc} 357 |
358 |
359 | 360 | : 361 | 362 | 363 | {project.project_content} 364 | 365 |
366 |
367 | ) : null 368 | )} 369 |
370 | ) 371 | : null} 372 |
373 |
374 | ); 375 | }; 376 | -------------------------------------------------------------------------------- /src/components/Resume/Template2/index.less: -------------------------------------------------------------------------------- 1 | // 响应式布局 2 | // 内容 3 | .template2-resume { 4 | width: 794px; 5 | min-height: 942px; 6 | margin-bottom: 60px; 7 | 8 | @media (max-width: 794px) { 9 | width: 100%; 10 | } 11 | } 12 | 13 | @media print { 14 | @page { 15 | size: A4; 16 | } 17 | 18 | .template2-resume { 19 | width: 100%; 20 | box-shadow: none; 21 | } 22 | } 23 | 24 | // 内容 25 | .template2-resume { 26 | // 加点阴影 27 | box-shadow: 0px 2px 4px 1px rgba(0, 0, 0, 0.15); 28 | 29 | // 布局位置 30 | .basic-info { 31 | padding: 18px 24px 12px 24px; 32 | } 33 | 34 | .main-info { 35 | padding: 0 24px 24px; 36 | background: #fff; 37 | } 38 | 39 | .section-title { 40 | font-size: 16px; 41 | line-height: 18px; 42 | margin-bottom: 8px; 43 | letter-spacing: 0; 44 | position: relative; 45 | 46 | display: flex; 47 | .title { 48 | width: auto; 49 | } 50 | .title-addon { 51 | flex: 1; 52 | position: relative; 53 | } 54 | 55 | .title-addon::after { 56 | content: ' '; 57 | position: absolute; 58 | right: 0; 59 | top: 50%; 60 | left: 16px; 61 | height: 1px; 62 | transform: translateY(-1px); 63 | background-color: currentColor; 64 | opacity: 0.54; 65 | } 66 | } 67 | 68 | .basic-info .section { 69 | border-radius: 0; 70 | } 71 | 72 | // 内容样式 通用 73 | .section { 74 | &:not(:first-of-type) { 75 | margin-top: 12px; 76 | } 77 | 78 | .section-info { 79 | font-size: 14px; 80 | line-height: 16px; 81 | color: rgba(0, 0, 0, 0.85); 82 | margin-bottom: 4px; 83 | 84 | display: flex; 85 | justify-content: space-between; 86 | align-items: center; 87 | } 88 | } 89 | 90 | .basic-info section .section-info { 91 | .info-name { 92 | max-width: 198px; 93 | padding-right: 4px; 94 | } 95 | } 96 | 97 | // basic-info 内容样式 98 | .basic-info .avatar { 99 | margin: 0 auto; 100 | display: block; 101 | width: 84px; 102 | height: 84px; 103 | } 104 | 105 | .basic-info .name { 106 | margin: 0 0 8px 4px; 107 | font-size: 24px; 108 | line-height: 36px; 109 | } 110 | 111 | .basic-info .profile { 112 | display: flex; 113 | justify-content: space-between; 114 | align-items: center; 115 | flex-wrap: wrap-reverse; 116 | 117 | margin-bottom: 12px; 118 | 119 | > div:first-of-type { 120 | flex: 1; 121 | } 122 | > div:nth-of-type(2) { 123 | width: 150px; 124 | } 125 | 126 | .profile-list { 127 | display: flex; 128 | flex-wrap: wrap; 129 | margin-left: 4px; 130 | 131 | > div { 132 | flex: 220px 1; 133 | } 134 | 135 | > div:not(:last-of-type) { 136 | margin-bottom: 4px; 137 | } 138 | 139 | .anticon { 140 | margin-right: 8px; 141 | } 142 | } 143 | } 144 | 145 | // 非通用,skill 146 | .basic-info .section-skill { 147 | .skill-item { 148 | display: flex; 149 | justify-content: space-between; 150 | 151 | &:not(:first-of-type) { 152 | margin-top: 2px; 153 | } 154 | } 155 | 156 | .skill-rate { 157 | font-size: 14px; 158 | display: flex; 159 | 160 | .ant-rate-star { 161 | margin-right: 4px; 162 | } 163 | } 164 | 165 | .skill-detail-item { 166 | margin-top: 4px; 167 | } 168 | } 169 | 170 | // 非通用 171 | .basic-info section .sub-info { 172 | color: rgba(0, 0, 0, 0.45); 173 | } 174 | 175 | .basic-info .section-award { 176 | .sub-info { 177 | font-size: 14px; 178 | margin-left: 8px; 179 | } 180 | } 181 | 182 | .basic-info .education-item { 183 | &:not(:first-of-type) { 184 | margin-top: 8px; 185 | } 186 | } 187 | 188 | .basic-info .section-work { 189 | > div { 190 | line-height: 24px; 191 | } 192 | 193 | a.sub-info { 194 | font-size: 12px; 195 | margin-left: 8px; 196 | } 197 | } 198 | 199 | // main-info 内容样式 200 | .main-info section { 201 | margin-bottom: 16px; 202 | 203 | .section-detail { 204 | letter-spacing: 1.2px; 205 | 206 | &:not(:first-of-type) { 207 | margin-top: 4px; 208 | } 209 | } 210 | } 211 | 212 | // 非通用 213 | .main-info .section-project, 214 | .main-info .section-work-exp { 215 | .section-info .ant-tag { 216 | line-height: 20px; 217 | height: 20px; 218 | border: none; 219 | } 220 | 221 | .section-item:not(:first-of-type) { 222 | margin-top: 10px; 223 | } 224 | 225 | .section-info .sub-info, 226 | .section-info .info-time { 227 | color: rgba(0, 0, 0, 0.45); 228 | font-size: 12px; 229 | margin-left: 8px; 230 | font-weight: 300; 231 | } 232 | 233 | .work-description, 234 | .project-content { 235 | white-space: pre-wrap; 236 | } 237 | } 238 | } 239 | 240 | body[lang='en-US'] { 241 | .template2-resume { 242 | .section-title { 243 | &::after { 244 | left: 188px; 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/components/Resume/Template2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Rate, Tag } from 'antd'; 3 | import { 4 | PhoneFilled, 5 | MailFilled, 6 | GithubFilled, 7 | ZhihuCircleFilled, 8 | CheckCircleFilled, 9 | ScheduleFilled, 10 | EnvironmentFilled, 11 | HeartFilled, 12 | CrownFilled, 13 | } from '@ant-design/icons'; 14 | import cx from 'classnames'; 15 | import _ from 'lodash-es'; 16 | import { FormattedMessage, useIntl } from 'react-intl'; 17 | import { getDefaultTitleNameMap } from '@/data/constant'; 18 | import { Avatar } from '../../Avatar'; 19 | import type { ResumeConfig, ThemeConfig } from '../../types'; 20 | import './index.less'; 21 | 22 | type Props = { 23 | value: ResumeConfig; 24 | theme: ThemeConfig; 25 | }; 26 | 27 | const Wrapper = ({ className, title, color, children }) => { 28 | return ( 29 |
30 |
31 | {title} 32 | 33 |
34 |
{children}
35 |
36 | ); 37 | }; 38 | 39 | /** 40 | * @description 简历内容区 41 | */ 42 | export const Template2: React.FC = props => { 43 | const intl = useIntl(); 44 | const { value, theme } = props; 45 | 46 | /** 个人基础信息 */ 47 | const profile = _.get(value, 'profile'); 48 | 49 | const titleNameMap = _.get( 50 | value, 51 | 'titleNameMap', 52 | getDefaultTitleNameMap({ intl }) 53 | ); 54 | 55 | /** 教育背景 */ 56 | const educationList = _.get(value, 'educationList'); 57 | 58 | /** 工作经历 */ 59 | const workExpList = _.get(value, 'workExpList'); 60 | 61 | /** 项目经验 */ 62 | const projectList = _.get(value, 'projectList'); 63 | 64 | /** 个人技能 */ 65 | const skillList = _.get(value, 'skillList'); 66 | 67 | /** 更多信息 */ 68 | const awardList = _.get(value, 'awardList'); 69 | 70 | /** 作品 */ 71 | const workList = _.get(value, 'workList'); 72 | 73 | /** 自我介绍 */ 74 | const aboutme = _.split(_.get(value, ['aboutme', 'aboutme_desc']), '\n'); 75 | 76 | return ( 77 |
78 |
79 |
80 |
81 | {profile?.name &&
{profile.name}
} 82 |
83 | {profile?.mobile && ( 84 |
85 | 86 | {profile.mobile} 87 |
88 | )} 89 | {profile?.email && ( 90 |
91 | 92 | {profile.email} 93 |
94 | )} 95 | {profile?.github && ( 96 |
97 | 98 | { 101 | window.open(profile.github); 102 | }} 103 | > 104 | {profile.github} 105 | 106 |
107 | )} 108 | {profile?.zhihu && ( 109 |
110 | 113 | { 116 | window.open(profile.zhihu); 117 | }} 118 | > 119 | {profile.zhihu} 120 | 121 |
122 | )} 123 | {profile?.workExpYear && ( 124 |
125 | 128 | 129 | : {profile.workExpYear} 130 | 131 |
132 | )} 133 | {profile?.workPlace && ( 134 |
135 | 138 | 139 | : {profile.workPlace} 140 | 141 |
142 | )} 143 | {profile?.positionTitle && ( 144 |
145 | 146 | 147 | : {profile.positionTitle} 148 | 149 |
150 | )} 151 |
152 |
153 | {/* 头像 */} 154 | {!value?.avatar?.hidden && ( 155 | 161 | )} 162 |
163 | {/* */} 164 | {/* 教育背景 */} 165 | {educationList?.length ? ( 166 | 168 | title={titleNameMap.educationList} 169 | className="section section-education" 170 | color={theme.color} 171 | > 172 | {educationList.map((education, idx) => { 173 | const [start, end] = education.edu_time; 174 | return ( 175 |
176 |
177 | 178 | {education.school} 179 | 180 | {education.major && {education.major}} 181 | {education.academic_degree && ( 182 | 186 | ({education.academic_degree}) 187 | 188 | )} 189 | 190 | 191 | 192 | {start} 193 | {end ? ` ~ ${end}` : ' 至今'} 194 | 195 |
196 |
197 | ); 198 | })} 199 |
200 | ) : null} 201 | {workList?.length ? ( 202 | 204 | title={titleNameMap.workList} 205 | className="section section-work" 206 | color={theme.color} 207 | > 208 | {workList.map((work, idx) => { 209 | return ( 210 |
211 |
212 | 215 | {work.work_name} 216 | 217 | 218 | 219 |
220 | {work.work_desc &&
{work.work_desc}
} 221 |
222 | ); 223 | })} 224 |
225 | ) : null} 226 | } 228 | className="section section-aboutme" 229 | color={theme.color} 230 | > 231 | {aboutme.map((d, idx) => ( 232 |
{d}
233 | ))} 234 |
235 | {/* 专业技能 */} 236 | {skillList?.length ? ( 237 | 239 | title={titleNameMap.skillList} 240 | className="section section-skill" 241 | color={theme.color} 242 | > 243 | {skillList.map((skill, idx) => { 244 | const skills = _.split(skill.skill_desc, '\n').join(';'); 245 | return skills ? ( 246 |
247 | 248 | 251 | {skills} 252 | 253 | {skill.skill_level && ( 254 | 260 | )} 261 |
262 | ) : null; 263 | })} 264 |
265 | ) : null} 266 | {/* {awardList?.length ? ( 267 | 273 | {awardList.map((award, idx) => { 274 | return ( 275 |
276 | 279 | {award.award_info} 280 | {award.award_time && ( 281 | 282 | ({award.award_time}) 283 | 284 | )} 285 |
286 | ); 287 | })} 288 |
289 | ) : null} */} 290 |
291 |
292 | {workExpList?.length ? ( 293 | 296 | title={titleNameMap.workExpList} 297 | color={theme.color} 298 | > 299 |
300 | {_.map(workExpList, (work, idx) => { 301 | const [start = null, end = null] = 302 | typeof work.work_time === 'string' 303 | ? `${work.work_time || ''}`.split(',') 304 | : work.work_time; 305 | return work ? ( 306 |
307 |
308 | 309 | {work.company_name} 310 | {work.department_name} 311 | 312 | 313 | {start} 314 | {end ? ` ~ ${end}` : } 315 | 316 |
317 |
{work.work_desc}
318 |
319 | ) : null; 320 | })} 321 |
322 |
323 | ) : null} 324 | {projectList?.length ? ( 325 | 328 | title={titleNameMap.projectList} 329 | color={theme.color} 330 | > 331 |
332 | {_.map(projectList, (project, idx) => 333 | project ? ( 334 |
335 |
336 | 337 | {project.project_name} 338 | 339 | {project.project_time} 340 | 341 | 342 | {project.project_role && ( 343 | {project.project_role} 344 | )} 345 |
346 |
347 | 348 | : 349 | 350 | {project.project_desc} 351 |
352 |
353 | 354 | : 355 | 356 | 357 | {project.project_content} 358 | 359 |
360 |
361 | ) : null 362 | )} 363 |
364 |
365 | ) : null} 366 |
367 |
368 | ); 369 | }; 370 | -------------------------------------------------------------------------------- /src/components/Resume/Template3/index.less: -------------------------------------------------------------------------------- 1 | // 响应式布局 2 | // 内容 3 | .template3-resume { 4 | width: 794px; 5 | min-height: 942px; 6 | margin-bottom: 60px; 7 | 8 | @media (max-width: 794px) { 9 | width: 100%; 10 | } 11 | } 12 | 13 | @media print { 14 | @page { 15 | size: A4; 16 | } 17 | 18 | .template3-resume { 19 | width: 100%; 20 | box-shadow: none; 21 | } 22 | } 23 | 24 | // 内容 25 | .template3-resume { 26 | // 加点阴影 27 | box-shadow: 0px 2px 4px 1px rgba(0, 0, 0, 0.15); 28 | 29 | // 布局位置 30 | .basic-info { 31 | padding: 24px 18px 24px 24px; 32 | } 33 | 34 | .main-info { 35 | padding: 24px 24px 32px 32px; 36 | background: #fff; 37 | } 38 | 39 | // 复写 40 | .ant-ribbon { 41 | height: 24px; 42 | line-height: 24px; 43 | } 44 | 45 | .section-title { 46 | font-size: 16px; 47 | letter-spacing: 0; 48 | } 49 | 50 | .ant-card-body { 51 | padding: 12px 24px; 52 | } 53 | 54 | .basic-info .section { 55 | padding-top: 28px; 56 | border-radius: 0; 57 | } 58 | .ant-ribbon-wrapper:not(:last-of-type) .ant-card-bordered { 59 | border-bottom: 0; 60 | } 61 | 62 | // 内容样式 通用 63 | section { 64 | &:not(:first-of-type) { 65 | margin-top: 24px; 66 | } 67 | 68 | .section-info { 69 | font-size: 18px; 70 | line-height: 24px; 71 | color: rgba(0, 0, 0, 0.85); 72 | margin-bottom: 8px; 73 | 74 | display: flex; 75 | justify-content: space-between; 76 | align-items: center; 77 | } 78 | } 79 | 80 | .basic-info section .section-info { 81 | .info-name { 82 | max-width: 198px; 83 | padding-right: 4px; 84 | } 85 | } 86 | 87 | // basic-info 内容样式 88 | .basic-info .avatar { 89 | margin: 12px auto; 90 | display: block; 91 | width: 84px; 92 | height: 84px; 93 | } 94 | 95 | .basic-info .name { 96 | margin: 0 0 8px 4px; 97 | font-size: 24px; 98 | line-height: 36px; 99 | } 100 | 101 | .basic-info .profile { 102 | margin-bottom: 16px; 103 | 104 | .profile-list { 105 | display: flex; 106 | flex-wrap: wrap; 107 | margin-left: 4px; 108 | 109 | > div { 110 | display: flex; 111 | align-items: center; 112 | flex: 220px 1; 113 | margin-right: 4px; 114 | } 115 | 116 | > div:not(:last-of-type) { 117 | margin-bottom: 4px; 118 | } 119 | 120 | .anticon { 121 | margin-right: 6px; 122 | } 123 | } 124 | } 125 | 126 | // 非通用,skill 127 | .basic-info .section-skill { 128 | .skill-item { 129 | display: flex; 130 | justify-content: space-between; 131 | 132 | &:not(:first-of-type) { 133 | margin-top: 2px; 134 | } 135 | } 136 | 137 | .skill-rate { 138 | font-size: 14px; 139 | display: flex; 140 | 141 | .ant-rate-star { 142 | margin-right: 4px; 143 | } 144 | } 145 | 146 | .skill-detail-item { 147 | margin-top: 4px; 148 | } 149 | } 150 | 151 | // 非通用 152 | .basic-info section .sub-info { 153 | color: rgba(0, 0, 0, 0.45); 154 | } 155 | 156 | .basic-info .section-award { 157 | .sub-info { 158 | font-size: 14px; 159 | margin-left: 8px; 160 | } 161 | } 162 | 163 | .basic-info .education-item { 164 | &:not(:first-of-type) { 165 | margin-top: 8px; 166 | } 167 | } 168 | 169 | .basic-info .section-work { 170 | > div { 171 | line-height: 24px; 172 | } 173 | 174 | a.sub-info { 175 | font-size: 12px; 176 | margin-left: 8px; 177 | } 178 | } 179 | 180 | // main-info 内容样式 181 | .main-info section { 182 | margin-bottom: 16px; 183 | 184 | .section-header { 185 | display: flex; 186 | margin-bottom: 10px; 187 | 188 | img { 189 | margin-right: 8px; 190 | } 191 | 192 | h1 { 193 | position: relative; 194 | font-size: 16px; 195 | font-weight: bold; 196 | color: #fff; 197 | width: 200px; 198 | width: max-content; 199 | line-height: 24px; 200 | padding: 0 100px 0 10px; 201 | border-top-left-radius: 3px; 202 | border-bottom-left-radius: 3px; 203 | margin-left: -12px; 204 | } 205 | h1:before { 206 | content: ''; 207 | display: block; 208 | width: 18px; 209 | height: 18px; 210 | background: #fff; 211 | position: absolute; 212 | right: -9px; 213 | top: 4px; 214 | transform: rotate(45deg); 215 | } 216 | } 217 | 218 | .section-detail { 219 | letter-spacing: 1.2px; 220 | 221 | &:not(:first-of-type) { 222 | margin-top: 4px; 223 | } 224 | } 225 | } 226 | 227 | // 非通用 228 | .main-info .section-project, 229 | .main-info .section-work-exp { 230 | .section-info .ant-tag { 231 | line-height: 20px; 232 | height: 20px; 233 | border: none; 234 | } 235 | 236 | .section-item:not(:first-of-type) { 237 | margin-top: 18px; 238 | } 239 | 240 | .section-info .sub-info, 241 | .section-info .info-time { 242 | color: rgba(0, 0, 0, 0.45); 243 | font-size: 12px; 244 | margin-left: 8px; 245 | font-weight: 300; 246 | } 247 | 248 | .work-description, 249 | .project-content { 250 | white-space: pre-wrap; 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/components/Resume/Template3/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Rate, Tag, Badge, Card } from 'antd'; 3 | import { 4 | PhoneFilled, 5 | MailFilled, 6 | GithubFilled, 7 | ZhihuCircleFilled, 8 | TrophyFilled, 9 | CheckCircleFilled, 10 | ScheduleFilled, 11 | EnvironmentFilled, 12 | HeartFilled, 13 | CrownFilled, 14 | } from '@ant-design/icons'; 15 | import _ from 'lodash-es'; 16 | import { FormattedMessage, useIntl } from 'react-intl'; 17 | import { getDefaultTitleNameMap } from '@/data/constant'; 18 | import type { ResumeConfig, ThemeConfig } from '../../types'; 19 | import './index.less'; 20 | 21 | type Props = { 22 | value: ResumeConfig; 23 | theme: ThemeConfig; 24 | }; 25 | 26 | const wrapper = ({ id, title, color }) => WrappedComponent => { 27 | return ( 28 |
29 |
30 |

{title}

31 |
32 |
{WrappedComponent}
33 |
34 | ); 35 | }; 36 | 37 | const CardWrapper: React.FC<{ 38 | title: string | JSX.Element; 39 | className: string; 40 | color: string; 41 | }> = ({ title, color, className, children }) => { 42 | return ( 43 | {title}
} 45 | color={color} 46 | placement="start" 47 | > 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | /** 54 | * @description 简历内容区 55 | */ 56 | export const Template3: React.FC = props => { 57 | const intl = useIntl(); 58 | const { value, theme } = props; 59 | 60 | /** 个人基础信息 */ 61 | const profile = _.get(value, 'profile'); 62 | 63 | const titleNameMap = _.get( 64 | value, 65 | 'titleNameMap', 66 | getDefaultTitleNameMap({ intl }) 67 | ); 68 | 69 | /** 教育背景 */ 70 | const educationList = _.get(value, 'educationList'); 71 | 72 | /** 工作经历 */ 73 | const workExpList = _.get(value, 'workExpList'); 74 | 75 | /** 项目经验 */ 76 | const projectList = _.get(value, 'projectList'); 77 | 78 | /** 个人技能 */ 79 | const skillList = _.get(value, 'skillList'); 80 | 81 | /** 更多信息 */ 82 | const awardList = _.get(value, 'awardList'); 83 | 84 | /** 作品 */ 85 | const workList = _.get(value, 'workList'); 86 | 87 | /** 自我介绍 */ 88 | const aboutme = _.split(_.get(value, ['aboutme', 'aboutme_desc']), '\n'); 89 | 90 | return ( 91 |
92 |
93 | {/* */} 94 |
95 | {profile?.name &&
{profile.name}
} 96 |
97 | {profile?.mobile && ( 98 |
99 | 100 | {profile.mobile} 101 |
102 | )} 103 | {profile?.email && ( 104 |
105 | 106 | {profile.email} 107 |
108 | )} 109 | {profile?.github && ( 110 |
111 | 112 | { 115 | window.open(profile.github); 116 | }} 117 | > 118 | {profile.github} 119 | 120 |
121 | )} 122 | {profile?.zhihu && ( 123 |
124 | 127 | { 130 | window.open(profile.zhihu); 131 | }} 132 | > 133 | {profile.zhihu} 134 | 135 |
136 | )} 137 | {profile?.workExpYear && ( 138 |
139 | 140 | 141 | : {profile.workExpYear} 142 | 143 |
144 | )} 145 | {profile?.workPlace && ( 146 |
147 | 150 | 151 | : {profile.workPlace} 152 | 153 |
154 | )} 155 | {profile?.positionTitle && ( 156 |
157 | 158 | 159 | : {profile.positionTitle} 160 | 161 |
162 | )} 163 |
164 |
165 | {/*
*/} 166 | {/* 教育背景 */} 167 | {educationList?.length ? ( 168 | 170 | title={titleNameMap.educationList} 171 | className="section section-education" 172 | color={theme.color} 173 | > 174 | {educationList.map((education, idx) => { 175 | const [start, end] = education.edu_time; 176 | return ( 177 |
178 |
179 | 180 | {education.school} 181 | 182 | {education.major && {education.major}} 183 | {education.academic_degree && ( 184 | 188 | ({education.academic_degree}) 189 | 190 | )} 191 | 192 | 193 | 194 | {start} 195 | {end ? ` ~ ${end}` : } 196 | 197 |
198 |
199 | ); 200 | })} 201 |
202 | ) : null} 203 | {workList?.length ? ( 204 | 206 | title={titleNameMap.workList} 207 | className="section section-work" 208 | color={theme.color} 209 | > 210 | {workList.map((work, idx) => { 211 | return ( 212 |
213 |
214 | 217 | {work.work_name} 218 | 219 | 220 | 221 |
222 | {work.work_desc &&
{work.work_desc}
} 223 |
224 | ); 225 | })} 226 |
227 | ) : null} 228 | } 230 | className="section section-aboutme" 231 | color={theme.color} 232 | > 233 | {aboutme.map((d, idx) => ( 234 |
{d}
235 | ))} 236 |
237 | {/* 专业技能 */} 238 | {skillList?.length ? ( 239 | 241 | title={titleNameMap.skillList} 242 | className="section section-skill" 243 | color={theme.color} 244 | > 245 | {skillList.map((skill, idx) => { 246 | const skills = _.split(skill.skill_desc, '\n').join(';'); 247 | return skills ? ( 248 |
249 | 250 | 253 | {skills} 254 | 255 | {skill.skill_level && ( 256 | 262 | )} 263 |
264 | ) : null; 265 | })} 266 |
267 | ) : null} 268 | {awardList?.length ? ( 269 | 271 | title={titleNameMap.awardList} 272 | className="section section-award" 273 | color={theme.color} 274 | > 275 | {awardList.map((award, idx) => { 276 | return ( 277 |
278 | 281 | {award.award_info} 282 | {award.award_time && ( 283 | 284 | ({award.award_time}) 285 | 286 | )} 287 |
288 | ); 289 | })} 290 |
291 | ) : null} 292 |
293 |
294 | {workExpList?.length 295 | ? wrapper({ 296 | id: 'work-experience', 297 | title: titleNameMap?.workExpList, 298 | color: theme.color, 299 | })( 300 |
301 | {_.map(workExpList, (work, idx) => { 302 | const [start = null, end = null] = 303 | typeof work.work_time === 'string' 304 | ? `${work.work_time || ''}`.split(',') 305 | : work.work_time; 306 | return work ? ( 307 |
308 |
309 | 310 | {work.company_name} 311 | 312 | {work.department_name} 313 | 314 | 315 | 316 | {start} 317 | {end ? ` ~ ${end}` : } 318 | 319 |
320 |
{work.work_desc}
321 |
322 | ) : null; 323 | })} 324 |
325 | ) 326 | : null} 327 | 328 | {projectList?.length 329 | ? wrapper({ 330 | id: 'skill', 331 | title: titleNameMap?.projectList, 332 | color: theme.color, 333 | })( 334 |
335 | {_.map(projectList, (project, idx) => 336 | project ? ( 337 |
338 |
339 | 340 | {project.project_name} 341 | 342 | {project.project_time} 343 | 344 | 345 | {project.project_role && ( 346 | 347 | {project.project_role} 348 | 349 | )} 350 |
351 |
352 | 353 | : 354 | 355 | {project.project_desc} 356 |
357 |
358 | 359 | : 360 | 361 | 362 | {project.project_content} 363 | 364 |
365 |
366 | ) : null 367 | )} 368 |
369 | ) 370 | : null} 371 |
372 |
373 | ); 374 | }; 375 | -------------------------------------------------------------------------------- /src/components/Resume/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Template1 } from './Template1'; 3 | import { Template2 } from './Template2'; 4 | import { Template3 } from './Template3'; 5 | 6 | export const Resume: React.FC = ({ template, ...restProps }) => { 7 | const Template = React.useMemo(() => { 8 | switch (template) { 9 | case 'template2': 10 | return Template2; 11 | case 'template3': 12 | return Template3; 13 | default: 14 | return Template1; 15 | } 16 | }, [template]); 17 | 18 | return Template ?