├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build ├── compress.sh ├── deploy.sh └── upload.sh ├── config ├── fileUtil.js ├── webpack.base.js ├── webpack.config.build.js ├── webpack.config.dev.js └── webpack.config.js ├── contribution.md ├── package.json ├── pnpm-lock.yaml ├── public └── css │ └── print.css └── src ├── app ├── index.html ├── index.js └── modules │ ├── btns.js │ ├── clickObjEditor.js │ ├── header.js │ ├── iframePage.js │ ├── public.js │ ├── schemaEditor.js │ ├── textArea.js │ └── toolsBtn.js ├── assets ├── css │ ├── app.scss │ └── base.scss └── favicon.png ├── components └── Toast │ ├── index.js │ └── index.scss ├── constants ├── index.js ├── schema.js ├── schema │ ├── abc.js │ ├── demo1.js │ ├── react1.js │ └── vue1.js └── svgs │ └── index.js ├── pages ├── abc │ ├── App.jsx │ ├── App.vue │ ├── index.html │ ├── index.js │ └── index.scss ├── demo1 │ ├── demo.html │ ├── index.html │ ├── index.js │ └── index.scss ├── icons │ ├── App.jsx │ ├── components │ │ └── Icon.jsx │ ├── index.html │ ├── index.scss │ └── main.js ├── introduce │ ├── index.html │ ├── index.js │ └── index.scss ├── react1 │ ├── App.jsx │ ├── components │ │ ├── BaseInfo.jsx │ │ ├── Exps.jsx │ │ └── OtherInfo.jsx │ ├── index.html │ ├── index.scss │ └── main.js └── vue1 │ ├── App.vue │ ├── components │ ├── Exps.vue │ └── Infos.vue │ ├── index.html │ ├── index.scss │ └── main.js └── utils └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react","@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-runtime","@babel/plugin-transform-react-jsx"] 4 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | config 2 | public -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['airbnb-base', 'plugin:react/recommended'], 7 | parserOptions: { 8 | ecmaVersion: 12, 9 | sourceType: 'module', 10 | }, 11 | rules: { 12 | 'linebreak-style': 'off', 13 | 'no-use-before-define': 'off', 14 | 'consistent-return': 'off', 15 | 'no-restricted-syntax': 'off', 16 | 'no-shadow': 'off', 17 | 'new-cap': 'warn', 18 | 'no-underscore-dangle': 'warn', 19 | 'no-loop-func': 'warn', 20 | 'import/prefer-default-export': 'warn', 21 | 'no-param-reassign': 'warn', 22 | semi: ['error', 'never'], 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: prod-CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | types: [ assigned ] 12 | branches: [ main ] 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | # This workflow contains a single job called "build" 20 | build: 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v2 28 | with: 29 | node-version: '14' 30 | # 准备 pnpm环境 31 | - uses: pnpm/action-setup@v2.1.0 32 | with: 33 | version: 6.30.1 34 | # 配置rsa密钥自动登陆 35 | - uses: webfactory/ssh-agent@v0.4.1 36 | with: 37 | ssh-private-key: ${{ secrets.ACCESS_TOKEN }} 38 | - name: Setup knownhosts 39 | run: ssh-keyscan ${{ secrets.REMOTE_ORIGIN }} >> ~/.ssh/known_hosts 40 | 41 | - name: Install dependence 42 | run: | 43 | echo 开始----安装依赖 44 | pnpm install 45 | - name: Build 46 | run: | 47 | echo 开始----构建 48 | pnpm build 49 | - name: Compress dist 50 | run: | 51 | echo 开始----压缩 52 | pnpm compress 53 | # 上传压缩的内容 54 | - name: Upload package 55 | run: | 56 | echo 开始---上传 57 | pnpm upload 58 | # 部署上传的包 59 | - name: Deploy 60 | run: | 61 | echo 开始----部署 62 | pnpm deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.tar.gz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 sugar 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 2 | 个人简历在线生成 3 | 4 | [点击体验](https://resume.sugarat.top) 5 | 6 | ## 推荐阅读:[如何书写一份好的互联网校招简历](https://juejin.cn/post/6928390537946857479) 7 | 8 | ## 后续规划 9 | * [ ] 接入markdown编辑(正在设计JSON与MD的友好互转方案) 10 | * 网上优秀模板样式导入 11 | * [ ] [木及简历](https://resume.mdedit.online/#/) 12 | * [ ] [Yang03/online-resume-generator](https://yang03.github.io/online-resume-generator/dist/index.html) 13 | * ....更多欢迎推荐自己喜欢的模板 14 | ## 如何食用本仓库 15 | 1. clone仓库代码 16 | ```shell 17 | git clone https://github.com/ATQQ/resume.git 18 | ``` 19 | ```shell 20 | cd resume 21 | ``` 22 | 2. 安装依赖 23 | ```shell 24 | npm i -g pnpm 25 | 26 | pnpm install 27 | ``` 28 | 3. 本地运行 29 | ```shell 30 | npm run dev 31 | ``` 32 | 4. 构建 33 | ```shell 34 | npm run build 35 | ``` 36 | 37 | ## 如何贡献你的模板 38 | 本仓库接入了Github Action,pr合并后会自动更新到[线上](https://resume.sugarat.top) 39 | 40 | 遵循约定优于配置的观点,贡献者只需关心简历部分的实现即可 41 | 42 | 简历模板实现部分与项目整体是低耦合的,所以理论上支持任意前端技术栈编写简历: 43 | * html/css/js 44 | * vue 45 | * react 46 | * jQuery 47 | * ...更多 48 | 49 | 转到详细[贡献指南](./contribution.md) 50 | 51 | ## 已有模板展示 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /build/compress.sh: -------------------------------------------------------------------------------- 1 | # /bin/bash 2 | compressDir="./dist" # 需要压缩目录 3 | compressFile="resume.tar.gz" # 压缩后的文件名 4 | echo "开始-----归档解压" 5 | tar -zvcf ${compressFile} ${compressDir} -------------------------------------------------------------------------------- /build/deploy.sh: -------------------------------------------------------------------------------- 1 | # /bin/bash 2 | compressFile="resume.tar.gz" # 压缩后的文件名 3 | user="root" # 远程登录用户 4 | origin="sugarat.top" # 远程登录origin 5 | targetDir="/www/wwwroot/resume.sugarat.top" # 目标目录 6 | echo "开始-----部署" 7 | ssh -p22 ${user}@${origin} "tar -zvxf ${compressFile} -C ${targetDir}" 8 | echo "清理-----临时的文件" 9 | rm -rf $compressFile -------------------------------------------------------------------------------- /build/upload.sh: -------------------------------------------------------------------------------- 1 | # /bin/bash 2 | compressFile="resume.tar.gz" # 压缩后的文件名 3 | user="root" # 远程登录用户 4 | origin="sugarat.top" # 远程登录origin 5 | echo "开始-----上传" 6 | scp $compressFile $user@$origin:./ -------------------------------------------------------------------------------- /config/fileUtil.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | // 打包html 6 | const htmlWebpackPlugin = require('html-webpack-plugin'); 7 | /** 8 | * 递归获取指定目录中的所有文件路径 9 | * @param {String} dir 目录名 10 | * @returns {Array} directorys 文件相对路径数组 11 | */ 12 | let getDirFiles = (dir) => { 13 | let result = [] 14 | let files = fs.readdirSync(dir, { withFileTypes: true }) 15 | files.forEach(file => { 16 | if (file.isFile()) { 17 | result.push(path.join(dir, file.name)) 18 | } else { 19 | result.push(...getDirFiles(path.join(dir, file.name))) 20 | } 21 | }) 22 | return result; 23 | } 24 | 25 | /** 26 | * 获取指定目录中所有文件,限定文件类型 27 | * @param {String} dir 目录 28 | * @param {String} type 文件类型(后缀) js/.js 29 | */ 30 | let getDirFileByType = (dir, type) => { 31 | return getDirFiles(dir).filter(file => path.extname(file).endsWith(type)) 32 | } 33 | 34 | /** 35 | * 获取指定目录中的所有文件的绝对路径 36 | * @param {String} dir 目录名 37 | */ 38 | let getDirFilesWithFullPath = (dir) => { 39 | return getDirFiles(dir).map(file => path.resolve(file)) 40 | } 41 | 42 | 43 | function getEntry(url, suffix = ".js") { 44 | const files = getDirFileByType(url, suffix) 45 | return files.reduce((pre, file) => { 46 | const filename = path.basename(file) 47 | const key = filename.slice(0, filename.lastIndexOf(suffix)) 48 | pre[key] = './' + file 49 | return pre 50 | }, {}) 51 | } 52 | 53 | function getHtml(filename, chunks, template, title = '', options = {}) { 54 | const minify = process.env.NODE_ENV === 'production' 55 | return new htmlWebpackPlugin({ 56 | filename, 57 | minify: { 58 | collapseWhitespace: minify, 59 | removeComments: minify, 60 | removeRedundantAttributes: minify, 61 | removeScriptTypeAttributes: minify, 62 | removeStyleLinkTypeAttributes: minify, 63 | useShortDoctype: minify 64 | }, 65 | chunks, //引入对应的js 66 | template, 67 | title: title, 68 | scriptLoading: 'defer', 69 | favicon: './src/assets/favicon.png', 70 | meta: { 71 | viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, user-scalable=no', 72 | description: '在线简历生成', 73 | keywords: '校招简历,简历模板,简历生成,在线简历模板,在线简历生成', 74 | render: 'webkit' 75 | }, 76 | hash: true, // 清除缓存 77 | ...options 78 | }) 79 | } 80 | 81 | function getPagesConfig(baseDir = 'src/pages') { 82 | const dirs = fs.readdirSync(baseDir, { withFileTypes: true }) 83 | const pageEntry = {} 84 | dirs.forEach(dir => { 85 | if (dir.isDirectory()) { 86 | const { name } = dir 87 | const jsFilesPath = getDirFileByType(path.resolve(baseDir, name), 'js') 88 | const entry = jsFilesPath.reduce((pre, current) => { 89 | const key = current.slice(current.indexOf(name), current.length - 3).split('/').join('_') 90 | pre[key] = current 91 | return pre 92 | }, {}) 93 | pageEntry[name] = entry 94 | } 95 | }) 96 | return pageEntry 97 | } 98 | 99 | function getEntryAndPage(baseDir = 'src/pages') { 100 | const pageConfig = getPagesConfig(baseDir) 101 | const pageName = Object.keys(pageConfig) 102 | const entry = pageName.reduce((pre, name) => { 103 | pre = { 104 | ...pre, 105 | ...pageConfig[name] 106 | } 107 | return pre 108 | }, {}) 109 | const pages = pageName.map(name => { 110 | const pageDir = `${name}/index.html` 111 | const page = getHtml(`pages/${pageDir}`, Object.keys(pageConfig[name]), `${baseDir}/${pageDir}`, name) 112 | return page 113 | }) 114 | return { 115 | entry, 116 | pages 117 | } 118 | } 119 | 120 | /** 121 | * 自动创建src/constants/schema.js 文件 122 | */ 123 | function writeSchemaJS() { 124 | const files = getDirFilesWithFullPath('src/constants/schema') 125 | const { dir } = path.parse(files[0]) 126 | const targetFilePath = path.resolve(dir, '../', 'schema.js') 127 | const names = files.map(file => path.parse(file).name) 128 | const res = `${names.map(n => { 129 | return `import ${n} from './schema/${n}'` 130 | }).join('\n')} 131 | 132 | export default{ 133 | ${names.join(',')} 134 | }` 135 | fs.writeFileSync(targetFilePath, res) 136 | } 137 | module.exports = { 138 | getDirFiles, 139 | getDirFileByType, 140 | getDirFilesWithFullPath, 141 | getEntry, 142 | getHtml, 143 | getEntryAndPage, 144 | writeSchemaJS 145 | } 146 | -------------------------------------------------------------------------------- /config/webpack.base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const { getEntryAndPage, getHtml, writeSchemaJS } = require('./fileUtil') 6 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 8 | //清理打包 9 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const { entry, pages } = getEntryAndPage('src/pages') 12 | 13 | // 生成 schema.js 文件 14 | writeSchemaJS() 15 | 16 | module.exports = { 17 | entry: { 18 | ...entry, 19 | app: path.resolve(__dirname, '../src/app/index.js'), 20 | }, 21 | output: { 22 | filename: 'js/[name]-[contenthash:8].js', 23 | path: path.resolve(__dirname, './../dist'), 24 | }, 25 | // target: ['web', 'es5'], 26 | resolve: { 27 | extensions: ['.js', '.jsx', '.vue'], 28 | alias: { 29 | '@': path.resolve(__dirname, '../src'), 30 | }, 31 | modules: ['node_modules', path.resolve(__dirname, 'src')], 32 | }, 33 | module: { 34 | // 配置loader 35 | rules: [ 36 | { 37 | test: /\.(sc|c)ss$/, 38 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 39 | }, 40 | { 41 | test: /\.(png|jpg|gif|jpeg)$/, 42 | loader: 'file-loader', 43 | options: { 44 | name: '[hash].[ext]', 45 | outputPath: './img', 46 | esModule: false, 47 | }, 48 | }, 49 | { 50 | test: /\.jsx?$/, 51 | use: 'babel-loader?cacheDirectory', 52 | }, 53 | { 54 | test: /\.vue$/, 55 | loader: 'vue-loader', 56 | }, 57 | ], 58 | }, 59 | // 配置插件 60 | plugins: [ 61 | ...pages, 62 | getHtml('index.html', ['app'], 'src/app/index.html', '首页-简历自动生成', { 63 | pageNames: pages.map((page) => page.userOptions.title), 64 | }), 65 | new CleanWebpackPlugin(), 66 | //css分离(输出文件名)) 67 | new MiniCssExtractPlugin({ 68 | // 类似 webpackOptions.output里面的配置 可以忽略 69 | filename: 'css/[name]-[contenthash:8].css', 70 | chunkFilename: 'css/[id]-[contenthash:8].css', 71 | }), 72 | new webpack.HotModuleReplacementPlugin(), 73 | new CopyWebpackPlugin({ 74 | patterns: [{ from: 'public' }], 75 | }), 76 | new VueLoaderPlugin(), 77 | ], 78 | } 79 | -------------------------------------------------------------------------------- /config/webpack.config.build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 4 | // 压缩css 5 | const optimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin') 6 | 7 | 8 | module.exports = { 9 | mode: 'production', 10 | devtool: 'source-map', 11 | // 配置插件 12 | plugins: [ 13 | new optimizeCssAssetsWebpackPlugin(), 14 | ], 15 | } -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = { 4 | mode: 'development', 5 | devServer: { 6 | contentBase: './dist', //项目基本访问目录 7 | host: 'localhost', //服务器ip地址 8 | port: 8088, //端口 9 | open: true, //自动打开页面 10 | } 11 | } -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { merge } = require('webpack-merge') 3 | 4 | const baseConfig = require('./webpack.base') 5 | const devConfig = require('./webpack.config.dev') 6 | const proConfig = require('./webpack.config.build') 7 | 8 | module.exports = () => { 9 | const env = process.env.NODE_ENV 10 | switch (env) { 11 | case 'development': 12 | return merge(baseConfig, devConfig); 13 | case 'production': 14 | return merge(baseConfig, proConfig); 15 | default: 16 | throw new Error('No matching configuration was found!'); 17 | } 18 | } -------------------------------------------------------------------------------- /contribution.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | ## 目录介绍 3 | 贡献者只需要关心./src目录,对其它配置感兴趣可自行品鉴 4 | ``` 5 | ./src 6 | ├── assets 静态资源css/img 7 | ├── constants 常量 8 | │ ├── index.js 存放路径与中文title的映射 9 | │ ├── schema 存放每个简历模板的默认JSON数据,与pages中的模板一一对应 10 | │ ├────── demo1.js 11 | │ ├────── react1.js 12 | │ └────── vue1.js 13 | ├── pages 简历模板 14 | │ ├── demo1 - 原生js编写的简历 15 | │ ├── react1 - react编写的简历 16 | │ ├── vue1 - vue编写的简历 17 | │ └── introduce - 应用的使用文档 18 | ├── utils 19 | ├── app 20 | │ ├── index.js 项目的入口js 21 | │ └── index.html 项目的入口页面 22 | │ 23 | ``` 24 | ## 新增模板 25 | 目前对于框架接入了React/Vue的支持,其余框架(如Angular)在后续有需求可接入 26 | 27 | ### 1.创建描述文件 28 | 在schema目录下创建页面的json描述文件,如abc.js 29 | ``` 30 | ./src 31 | ├── constants 32 | │ └── schema 33 | │ └────── abc.js 34 | ``` 35 | 36 | abc.js 37 | ```js 38 | export default { 39 | name: '王五', 40 | position: '求职目标: Web前端工程师', 41 | infos: [ 42 | '1:很多文字', 43 | '2:很多文字', 44 | '3:很多文字', 45 | ] 46 | } 47 | ``` 48 | 49 | ### 2.创建page 50 | 在pages目录下创建与json描述文件同名的目录,如abc 51 | 52 | ``` 53 | ./src 54 | ├── pages 55 | │ └── abc 56 | ``` 57 | 58 | ### 3.编写页面代码 59 | 下面提供了4种方式实现同一效果 60 | 61 | 期望的效果 62 | 63 | ![图片](https://img.cdn.sugarat.top/mdImg/MTYxNDQ4MDYyMjQ1Ng==614480622456) 64 | 65 | 期望的渲染结构 66 | ```html 67 |
68 |
69 |
70 |

王五

71 |

求职目标: Web前端工程师

72 |
73 |
    74 |
  • 1:很多文字
  • 75 |
  • 2:很多文字
  • 76 |
  • 3:很多文字
  • 77 |
78 |
79 |
80 | ``` 81 | 82 | 下面开始编写代码 83 | 84 | **为方便阅读,代码进行了折叠** 85 | 86 | 首先是样式,这里选择sass预处理语言,当然也可以用原生css 87 | 88 |
89 | index.scss 90 | 91 | ```scss 92 | @import './../../assets/css/base.scss'; 93 | html, 94 | body, 95 | #resume { 96 | height: 100%; 97 | overflow: hidden; 98 | } 99 | // 上面部分是推荐引入的通用样式 100 | 101 | // 下面书写我们的样式 102 | $themeColor: red; 103 | 104 | #app { 105 | padding: 1rem; 106 | } 107 | 108 | header { 109 | h1 { 110 | color: $themeColor; 111 | } 112 | h2 { 113 | font-weight: lighter; 114 | } 115 | } 116 | 117 | .infos { 118 | list-style: none; 119 | li { 120 | color: $themeColor; 121 | } 122 | } 123 | ``` 124 |
125 | 126 | 其次是页面描述文件 127 | 128 |
129 | index.html 130 | 131 | ```html 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | <%= htmlWebpackPlugin.options.title %> 141 | 142 | 143 | 144 | 145 |
146 |
147 | 148 |
149 |
150 | 151 | 152 | 153 | ``` 154 |
155 | 156 | **下面就开始使用各种技术栈进行逻辑代码编写** 157 | 158 |
159 | 原生js 160 | 161 | **目录结构** 162 | 163 | ``` 164 | ./src 165 | ├── pages 166 | │ └── abc 167 | │ └───── index.html 168 | │ └───── index.scss 169 | │ └───── index.js 170 | ``` 171 | 172 | **index.js** 173 | ```js 174 | import { getSchema } from "../../utils" 175 | import './index.scss' 176 | 177 | window.refresh = function () { 178 | const schema = getSchema() 179 | const { name, position, infos } = schema 180 | 181 | clearPage() 182 | renderHeader(name, position) 183 | renderInfos(infos) 184 | } 185 | 186 | function clearPage() { 187 | document.getElementById('app').innerHTML = '' 188 | } 189 | 190 | function renderHeader(name, position) { 191 | const html = ` 192 |
193 |

${name}

194 |

${position}

195 |
` 196 | document.getElementById('app').innerHTML += html 197 | } 198 | 199 | function renderInfos(infos = []) { 200 | if (infos?.length === 0) { 201 | return 202 | } 203 | const html = ` 204 | ` 209 | document.getElementById('app').innerHTML += html 210 | } 211 | 212 | window.onload = function () { 213 | refresh() 214 | } 215 | ``` 216 | 217 | 218 |
219 | 220 |
221 | Vue 222 | 223 | **目录结构** 224 | ``` 225 | ./src 226 | ├── pages 227 | │ └── abc 228 | │ └───── index.html 229 | │ └───── index.scss 230 | │ └───── index.js 231 | │ └───── App.vue 232 | ``` 233 | 234 | **index.js** 235 | 236 | ```js 237 | import Vue from 'vue' 238 | import App from './App.vue' 239 | import './index.scss' 240 | 241 | Vue.config.productionTip = process.env.NODE_ENV === 'development' 242 | 243 | new Vue({ 244 | render: h => h(App) 245 | }).$mount('#app') 246 | ``` 247 | 248 | **App.vue** 249 | 250 | ```vue 251 | 268 | 269 | 287 | ``` 288 |
289 | 290 |
291 | React 292 | 293 | **目录结构** 294 | ``` 295 | ./src 296 | ├── pages 297 | │ └── abc 298 | │ └───── index.html 299 | │ └───── index.scss 300 | │ └───── index.js 301 | │ └───── App.jsx 302 | ``` 303 | 304 | **index.js** 305 | ```js 306 | import React from 'react' 307 | import ReactDOM from 'react-dom'; 308 | import App from './App.jsx' 309 | import './index.scss' 310 | 311 | ReactDOM.render( 312 | 313 | 314 | , 315 | document.getElementById('app') 316 | ) 317 | ``` 318 | 319 | **App.jsx** 320 | ```jsx 321 | import React, { useEffect, useState } from 'react' 322 | import { getSchema } from '../../utils' 323 | 324 | export default function App() { 325 | const [schema, updateSchema] = useState(getSchema()) 326 | const { name, position, infos = [] } = schema 327 | useEffect(() => { 328 | window.refresh = function () { 329 | updateSchema(getSchema()) 330 | } 331 | }, []) 332 | return ( 333 |
334 |
335 |

{name}

336 |

{position}

337 |
338 |
339 | { 340 | infos.map((info, i) => { 341 | return

{info}

342 | }) 343 | } 344 |
345 |
346 | ) 347 | } 348 | ``` 349 | 350 |
351 | 352 |
353 | jQuery 354 | 355 | **目录结构** 356 | ``` 357 | ./src 358 | ├── pages 359 | │ └── abc 360 | │ └───── index.html 361 | │ └───── index.scss 362 | │ └───── index.js 363 | ``` 364 | 365 | **index.js** 366 | ```js 367 | import { getSchema } from "../../utils" 368 | import './index.scss' 369 | 370 | window.refresh = function () { 371 | const schema = getSchema() 372 | const { name, position, infos } = schema 373 | 374 | clearPage() 375 | renderHeader(name, position) 376 | renderInfos(infos) 377 | } 378 | 379 | function clearPage() { 380 | $('#app').empty() 381 | } 382 | 383 | function renderHeader(name, position) { 384 | const html = ` 385 |
386 |

${name}

387 |

${position}

388 |
` 389 | $('#app').append(html) 390 | } 391 | 392 | function renderInfos(infos = []) { 393 | if (infos?.length === 0) { 394 | return 395 | } 396 | const html = ` 397 | ` 402 | $('#app').append(html) 403 | } 404 | 405 | window.onload = function () { 406 | refresh() 407 | } 408 | ``` 409 | 410 |
411 | 412 | 如果觉得导航栏展示abc不友好,当然也可以更改 413 | 414 | ``` 415 | ./src 416 | ├── constants 417 | │ ├── index.js 存放路径与中文title的映射 418 | ``` 419 | 420 | **./src/constants/index.js** 中加入别名 421 | ```js 422 | export const navTitle = { 423 | 'abc': '开发示例' 424 | } 425 | ``` 426 | 427 | ![图片](https://img.cdn.sugarat.top/mdImg/MTYxNDQ5MDMyMDA3Nw==614490320077) 428 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resume", 3 | "version": "1.0.0", 4 | "description": "个人简历生成", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development webpack serve --config=config/webpack.config.js", 8 | "build": "cross-env NODE_ENV=production webpack --config=config/webpack.config.js", 9 | "compress": "bash ./build/compress.sh", 10 | "deploy": "bash ./build/deploy.sh", 11 | "upload": "bash ./build/upload.sh", 12 | "oneStep": "npm run build && npm run compress && npm run upload && npm run deploy", 13 | "lint": "eslint src --fix" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/ATQQ/resume.git" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/ATQQ/resume/issues" 24 | }, 25 | "homepage": "https://github.com/ATQQ/resume#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.17.2", 28 | "@babel/plugin-transform-react-jsx": "^7.16.7", 29 | "@babel/plugin-transform-runtime": "^7.17.0", 30 | "@babel/preset-env": "^7.16.11", 31 | "@babel/preset-react": "^7.16.7", 32 | "@babel/runtime": "^7.17.2", 33 | "babel-loader": "^8.2.3", 34 | "clean-webpack-plugin": "^3.0.0", 35 | "copy-webpack-plugin": "^7.0.0", 36 | "cross-env": "^7.0.3", 37 | "css-loader": "^5.2.7", 38 | "eslint": "^7.32.0", 39 | "eslint-config-airbnb-base": "^14.2.1", 40 | "eslint-plugin-import": "^2.25.4", 41 | "eslint-plugin-react": "^7.28.0", 42 | "html-webpack-plugin": "^5.5.0", 43 | "mini-css-extract-plugin": "^1.6.2", 44 | "node-sass": "^5.0.0", 45 | "optimize-css-assets-webpack-plugin": "^6.0.1", 46 | "sass-loader": "^11.1.1", 47 | "vue-loader": "^15.9.8", 48 | "vue-template-compiler": "^2.6.14", 49 | "webpack": "^5.68.0", 50 | "webpack-cli": "^4.9.2", 51 | "webpack-dev-server": "^3.11.3", 52 | "webpack-merge": "^5.8.0" 53 | }, 54 | "dependencies": { 55 | "html2canvas": "^1.4.1", 56 | "jsoneditor": "^9.7.2", 57 | "react": "^17.0.2", 58 | "react-dom": "^17.0.2", 59 | "vue": "^2.6.14" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /public/css/print.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | } 5 | header{ 6 | display: none !important; 7 | } 8 | .main{ 9 | padding: 0 !important; 10 | margin: 0 !important; 11 | } 12 | #page{ 13 | border: none !important; 14 | margin:0 !important; 15 | padding: 0 !important; 16 | transform: scale(1) !important; 17 | } 18 | .right{ 19 | display: none !important; 20 | } 21 | 22 | @page { 23 | size: A4 portrait; 24 | margin:0; 25 | } -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= htmlWebpackPlugin.options.title %> 14 | 15 | 16 | 17 | 18 |
19 | 24 |
25 | ← Resume 在线简历生成 26 |
27 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 |

这里显示点击内容对应的最小json对象

53 |
54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 |

输入框显示点击的内容,修改内容会直接同步

62 |

也可以直接在下方的json编辑器中进行直接修改

63 |

推荐电脑访问,手机导出的pdf样式可能有缺陷

64 |
65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | import '../assets/css/app.scss' 2 | import html2canvas from 'html2canvas' 3 | 4 | import { toggleControlPanel } from './modules/public' 5 | import initHeaderNav from './modules/header' 6 | 7 | import { 8 | registerJSONBtn, 9 | registerJSPDF, 10 | registerPCPrint, 11 | registerResetBtn, 12 | registerToggle, 13 | } from './modules/btns' 14 | import registerIframePageLoad from './modules/iframePage' 15 | import { registerInputToolsBtn } from './modules/toolsBtn' 16 | import { registerTextAreaInput } from './modules/textArea' 17 | 18 | window.html2canvas = html2canvas 19 | 20 | init() 21 | 22 | /** 23 | * 初始化 24 | */ 25 | function init() { 26 | toggleControlPanel() 27 | initHeaderNav() 28 | 29 | registerTextAreaInput() 30 | registerInputToolsBtn() 31 | // 激活Page的拓展功能与右侧操作面板 32 | registerIframePageLoad() 33 | 34 | // 注册按钮上的事件 35 | registerResetBtn() 36 | registerJSONBtn() 37 | registerToggle() 38 | registerPCPrint() 39 | registerJSPDF() 40 | } 41 | -------------------------------------------------------------------------------- /src/app/modules/btns.js: -------------------------------------------------------------------------------- 1 | import { 2 | copyRes, 3 | Dom2PDF, 4 | downloadTxtFile, 5 | getDefaultSchema, 6 | getPageKey, 7 | highLightDom, 8 | setSchema, 9 | } from '../../utils' 10 | // 完整的json编辑器 11 | import editor from './schemaEditor' 12 | import { toast } from '../../components/Toast' 13 | 14 | // 点击部分的json编辑器 15 | import clickObjEditor from './clickObjEditor' 16 | import { getSessionStorage, refreshIframePage } from './public' 17 | import { getTextArea } from './textArea' 18 | 19 | /** 20 | * 激活重置按钮 21 | */ 22 | export function registerResetBtn() { 23 | // 重置 24 | document.getElementById('reset').addEventListener('click', () => { 25 | if (window.confirm('是否初始化数据,这将会覆盖原有数据')) { 26 | const key = getPageKey() 27 | const data = getDefaultSchema(key) 28 | setSchema(data, key) 29 | 30 | editor.set(data) 31 | clickObjEditor.set({}) 32 | refreshIframePage(true) 33 | getTextArea().value = '' 34 | } 35 | }) 36 | } 37 | /** 38 | * 激活JSON下载/复制按钮 39 | */ 40 | export function registerJSONBtn() { 41 | document.querySelector('.json-btns').addEventListener('click', (e) => { 42 | switch (e.target.dataset.type) { 43 | case 'copy': 44 | copyRes(JSON.stringify(editor.get())) 45 | break 46 | case 'download': 47 | toast.success('开始下载') 48 | downloadTxtFile(JSON.stringify(editor.get()), `${Date.now()}.json`) 49 | break 50 | default: 51 | break 52 | } 53 | }) 54 | } 55 | 56 | export function registerToggle() { 57 | // 切换模式 58 | document.getElementById('toggle').addEventListener('click', () => { 59 | if (editor.mode === 'tree') { 60 | editor.changeEditorMode('code') 61 | return 62 | } 63 | editor.changeEditorMode('tree') 64 | }) 65 | } 66 | 67 | export function registerPCPrint() { 68 | // 打印 - 导出pdf 69 | document.getElementById('print').addEventListener('click', () => { 70 | // 解除高亮 71 | highLightDom(getSessionStorage('clickDom'), 0) 72 | 73 | if (window.print) { 74 | window.print() 75 | return 76 | } 77 | toast.error('PC上才能使用此按钮') 78 | }) 79 | } 80 | 81 | export function registerJSPDF() { 82 | // jsPDF - 导出pdf 83 | document.getElementById('pdf').addEventListener('click', () => { 84 | const dom = document.getElementById('page').contentDocument.body 85 | if (!dom) return 86 | // 解除高亮 87 | highLightDom(getSessionStorage('clickDom'), 0) 88 | Dom2PDF(dom, `${Date.now()}.pdf`) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/app/modules/clickObjEditor.js: -------------------------------------------------------------------------------- 1 | // 右侧编辑点击的Editor 2 | 3 | import { getSessionStorage, updatePage } from './public' 4 | import schemaEditor from './schemaEditor' 5 | 6 | class ClickObjEditor { 7 | constructor() { 8 | this.init() 9 | } 10 | 11 | init() { 12 | let timer = null 13 | // eslint-disable-next-line no-undef 14 | this.editor = new JSONEditor(document.getElementById('clickEditor'), { 15 | onChange: () => { 16 | if (timer) { 17 | clearTimeout(timer) 18 | } 19 | if (!getSessionStorage('activeValues')) { 20 | return 21 | } 22 | timer = setTimeout(() => { 23 | const path = getSessionStorage('activeObjPath') 24 | const json = schemaEditor.get() 25 | let temp = json 26 | path.forEach((key, i) => { 27 | if (i + 1 === path.length) { 28 | temp[key] = this.editor.get() 29 | schemaEditor.set(json) 30 | updatePage(json) 31 | } else { 32 | temp = temp[key] 33 | } 34 | }) 35 | }, 200) 36 | }, 37 | modes: ['tree', 'code'], 38 | }) 39 | } 40 | 41 | static getInstance() { 42 | if (!ClickObjEditor.instance) { 43 | ClickObjEditor.instance = new ClickObjEditor('jsonEditor') 44 | } 45 | return ClickObjEditor.instance 46 | } 47 | 48 | get() { 49 | return this.editor.get() 50 | } 51 | 52 | set(data) { 53 | this.editor.set(data) 54 | } 55 | } 56 | 57 | export default ClickObjEditor.getInstance() 58 | -------------------------------------------------------------------------------- /src/app/modules/header.js: -------------------------------------------------------------------------------- 1 | import { createEmptySpan, createLink, debounce } from '../../utils' 2 | import { navTitle } from '../../constants' 3 | import { jsonDataStack, scalePage } from './public' 4 | import { getTextArea } from './textArea' 5 | 6 | const dataStack = jsonDataStack 7 | 8 | /** 9 | * 初始化导航栏 10 | */ 11 | export default function initNav(defaultPage = getActivePageKey() || 'react1') { 12 | const $nav = document.querySelector('header nav') 13 | 14 | // 优先根据别名顺序生成 15 | const titleKeys = Object.keys(navTitle) 16 | .concat($nav.innerText.split(',')) 17 | .reduce((pre, now) => { 18 | if (!pre.includes(now)) { 19 | pre.push(now) 20 | } 21 | return pre 22 | }, []) 23 | // 获取所有模板的链接 24 | const links = titleKeys.map((titleKey) => { 25 | const link = createLink(navTitle[titleKey] || titleKey, `./pages/${titleKey}`) 26 | // iframe中打开 27 | return link 28 | }) 29 | 30 | // 加入自定义的链接 31 | links.push(createEmptySpan()) 32 | links.push(createLink('Github', 'https://github.com/ATQQ/resume', true)) 33 | links.push(createLink('贡献模板', 'https://github.com/ATQQ/resume/blob/main/README.md', true)) 34 | links.push( 35 | createLink( 36 | '如何书写一份好的互联网校招简历', 37 | 'https://juejin.cn/post/6928390537946857479', 38 | true, 39 | ), 40 | ) 41 | links.push(createLink('实现原理', 'https://juejin.cn/post/6934595007370231822', true)) 42 | links.push(createLink('建议/反馈', 'https://www.wenjuan.com/s/MBryA3gI/', true)) 43 | 44 | // 渲染到页面中 45 | const t = document.createDocumentFragment() 46 | links.forEach((link) => { 47 | t.appendChild(link) 48 | }) 49 | $nav.innerHTML = '' 50 | $nav.append(t) 51 | 52 | // 默认页面 53 | const _link = links.find((link) => link?.href?.endsWith(defaultPage)) 54 | changeIframePage(_link.href) 55 | activeLink(_link) 56 | 57 | // 窄屏手动开/关导航栏 58 | document.getElementById('open-menu').addEventListener('click', () => { 59 | if (!$nav.style.display) { 60 | $nav.style.display = 'block' 61 | return 62 | } 63 | if ($nav.style.display === 'block') { 64 | $nav.style.display = 'none' 65 | } else { 66 | $nav.style.display = 'block' 67 | } 68 | }) 69 | 70 | // 切换Page 71 | $nav.addEventListener('click', (e) => { 72 | // TODO:待优化窄屏幕逻辑 73 | if (e.target.tagName.toLowerCase() === 'a') { 74 | if ($nav.style.display) { 75 | $nav.style.display = 'none' 76 | } 77 | } 78 | 79 | // 新窗口打开 80 | if (e.target?.target !== 'page') { 81 | return 82 | } 83 | 84 | if (e.target.href === document.getElementById('page').src) { 85 | e.preventDefault() 86 | return 87 | } 88 | 89 | // 清空历史操作栈 90 | dataStack.clear() 91 | const $textarea = getTextArea() 92 | $textarea.ActiveValues = null 93 | $textarea.value = '' 94 | // iframe中打开 95 | if (e.target.tagName.toLowerCase() === 'a') { 96 | e.preventDefault() 97 | changeIframePage(e.target.href) 98 | activeLink(e.target) 99 | } 100 | }) 101 | 102 | // 适配屏幕 103 | window.addEventListener( 104 | 'resize', 105 | debounce((e) => { 106 | // TODO:导航栏 后续优化 107 | const width = e.currentTarget.innerWidth 108 | if (width > 900) { 109 | $nav.style.display = '' 110 | } 111 | scalePage(width) 112 | }, 500), 113 | ) 114 | window.addEventListener('load', (e) => { 115 | scalePage(e.currentTarget.innerWidth) 116 | }) 117 | } 118 | 119 | function getActivePageKey() { 120 | const activePath = localStorage.getItem('lastActivePage') 121 | return activePath?.slice(activePath.lastIndexOf('/') + 1) 122 | } 123 | 124 | function changeIframePage(src) { 125 | const page = document.getElementById('page') 126 | if (src) { 127 | page.src = src 128 | } 129 | } 130 | 131 | function activeLink(link) { 132 | Array.from(link.parentElement.children).forEach((el) => { 133 | el.classList.remove('active') 134 | }) 135 | link.classList.remove('active') 136 | link.classList.add('active') 137 | } 138 | -------------------------------------------------------------------------------- /src/app/modules/iframePage.js: -------------------------------------------------------------------------------- 1 | import { 2 | getPageKey, getSchema, highLightDom, traverseDomTreeMatchStr, 3 | } from '../../utils' 4 | import { 5 | getSessionStorage, scrollIntoView, setSessionStorage, toggleControlPanel, 6 | } from './public' 7 | import editor from './schemaEditor' 8 | import clickObjEditor from './clickObjEditor' 9 | import { getTextArea } from './textArea' 10 | 11 | function registerIframePageLoad() { 12 | document.getElementById('page').onload = (e) => { 13 | // show control panel 14 | toggleControlPanel(false) 15 | // 初始化json编辑器内容 16 | editor.set(getSchema(getPageKey())) 17 | // 初始化 18 | clickObjEditor.set({}) 19 | 20 | // 获取点击到的内容 21 | e.path[0].contentDocument.body.addEventListener('click', (e) => { 22 | const $target = e.target 23 | const clickText = $target.textContent.trim() 24 | const matchDoms = traverseDomTreeMatchStr( 25 | document.getElementById('page').contentDocument.body, 26 | clickText, 27 | ) 28 | const mathIndex = matchDoms.findIndex((v) => v === $target) 29 | if ($target.tagName.toLowerCase() === 'a' && !$target.dataset.open) { 30 | e.preventDefault() 31 | } 32 | 33 | if (editor.mode === 'code') { 34 | editor.changeEditorMode('tree') 35 | } 36 | if (mathIndex < 0) { 37 | return 38 | } 39 | // 解除上次点击的 40 | const $textarea = getTextArea() 41 | highLightDom(getSessionStorage('clickDom'), 0) 42 | // 高亮这次的10s 43 | highLightDom($target, 10000) 44 | // 更新editor中的search内容 45 | editor.search(clickText) 46 | 47 | // 更新到textarea中的内容 48 | $textarea.value = clickText 49 | // 聚焦 50 | if (document.getElementById('focus').checked) { 51 | $textarea.focus() 52 | setTimeout(scrollIntoView, 0, $target) 53 | } 54 | // 记录点击的dom 55 | setSessionStorage('clickDom', e.target) 56 | let i = -1 57 | for (const r of editor.searchResults) { 58 | if (r.node.value === clickText) { 59 | i += 1 60 | // 匹配到json中的节点 61 | if (i === mathIndex) { 62 | // 高亮一下$textarea 63 | $textarea.style.boxShadow = '0 0 1rem yellow' 64 | setTimeout(() => { 65 | $textarea.style.boxShadow = '' 66 | }, 200) 67 | // 触发editor onEvent事件-用于捕获path 68 | editor.activeResult.node.dom.value.click() 69 | return 70 | } 71 | } 72 | editor.nextActive() 73 | } 74 | }) 75 | 76 | storageActivePagePath() 77 | } 78 | } 79 | 80 | function storageActivePagePath() { 81 | localStorage.setItem('lastActivePage', getPageKey()) 82 | } 83 | 84 | export default registerIframePageLoad 85 | -------------------------------------------------------------------------------- /src/app/modules/public.js: -------------------------------------------------------------------------------- 1 | import { 2 | debounce, getPageKey, highLightDom, setSchema, 3 | } from '../../utils' 4 | 5 | class JsonDataStack { 6 | constructor() { 7 | this.stack = [] 8 | } 9 | 10 | static getInstance() { 11 | if (!JsonDataStack.instance) { 12 | JsonDataStack.instance = new JsonDataStack() 13 | } 14 | return JsonDataStack.instance 15 | } 16 | 17 | get length() { 18 | return this.stack.length 19 | } 20 | 21 | push(json) { 22 | this.stack.push(json) 23 | } 24 | 25 | pop() { 26 | return this.stack.pop() 27 | } 28 | 29 | clear() { 30 | this.stack = [] 31 | } 32 | } 33 | 34 | export const jsonDataStack = JsonDataStack.getInstance() 35 | 36 | export function scalePage(width) { 37 | if (width < 800) { 38 | const scale = (width / 800).toFixed(2) 39 | document.getElementById('page').style.transform = `scale(${scale})` 40 | const pageHeight = document.getElementById('page').getBoundingClientRect().height 41 | document.getElementsByClassName('main')[0].style.height = `${pageHeight}px` 42 | } else { 43 | document.getElementById('page').style.transform = 'scale(1)' 44 | document.getElementsByClassName('main')[0].style.height = '' 45 | } 46 | 47 | // jsonEditor 48 | if (width <= 1200) { 49 | const pageHeight = document.getElementById('page').getBoundingClientRect().height 50 | document.getElementsByClassName('right')[0].style.top = `${pageHeight}px` 51 | } else { 52 | document.getElementsByClassName('right')[0].style.top = '' 53 | } 54 | } 55 | 56 | export function scrollIntoView(dom) { 57 | dom?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }) 58 | } 59 | 60 | export function toggleControlPanel(hide = true) { 61 | if (hide) { 62 | // hide control panel 63 | document.getElementsByClassName('right')[0].setAttribute('hidden', 'hidden') 64 | return 65 | } 66 | document.getElementsByClassName('right')[0].removeAttribute('hidden') 67 | } 68 | 69 | export function getNowActivePath() { 70 | const path = getSessionStorage('valuePath') 71 | return path 72 | } 73 | const sessionMap = new Map() 74 | export function setSessionStorage(key, value) { 75 | if (value === null || value === undefined) { 76 | sessionMap.delete(key) 77 | return 78 | } 79 | sessionMap.set(key, value) 80 | } 81 | 82 | export function getSessionStorage(key) { 83 | return sessionMap.get(key) 84 | } 85 | 86 | /** 87 | * 更新子页面 88 | */ 89 | export function updatePage(data, isReload = false) { 90 | initObserver() 91 | setSchema(data, getPageKey()) 92 | refreshIframePage(isReload) 93 | } 94 | 95 | /** 96 | * 高亮Page中变化的Dom 97 | */ 98 | export function initObserver() { 99 | const config = { childList: true, subtree: true, characterData: true } 100 | const $pageBody = document.getElementById('page').contentDocument.body 101 | if (!$pageBody) { 102 | return 103 | } 104 | const observer = new MutationObserver( 105 | debounce((mutationsList) => { 106 | for (const e of mutationsList) { 107 | let { target } = e 108 | if (e.type === 'characterData') { 109 | target = e.target.parentElement 110 | } 111 | highLightDom(target) 112 | } 113 | }, 100), 114 | ) 115 | 116 | observer.observe($pageBody, config) 117 | setTimeout(() => { 118 | observer.disconnect() 119 | }, 0) 120 | } 121 | 122 | export function refreshIframePage(isReload = false) { 123 | const page = document.getElementById('page') 124 | if (isReload) { 125 | page.contentWindow.location.reload() 126 | return 127 | } 128 | if (page.contentWindow.refresh) { 129 | page.contentWindow.refresh() 130 | return 131 | } 132 | page.contentWindow.location.reload() 133 | } 134 | -------------------------------------------------------------------------------- /src/app/modules/schemaEditor.js: -------------------------------------------------------------------------------- 1 | // 右侧的完整json编辑器 2 | import { getPageKey, getSchema } from '../../utils' 3 | import { updatePage, setSessionStorage } from './public' 4 | 5 | class SchemaEditor { 6 | constructor(id, mode) { 7 | this.init(id, mode) 8 | } 9 | 10 | init(id, mode = 'tree') { 11 | const timer = null 12 | // eslint-disable-next-line no-undef 13 | this.editor = new JSONEditor(document.getElementById(id), { 14 | onChange: () => { 15 | if (timer) { 16 | clearTimeout(timer) 17 | } 18 | setTimeout(updatePage, 200, this.editor.get()) 19 | }, 20 | limitDragging: true, 21 | modes: ['tree', 'code'], 22 | name: 'root', 23 | onEvent(data, e) { 24 | if (e.type === 'click' && document.activeElement.id === 'domContext') { 25 | setSessionStorage('valuePath', data.path) 26 | } 27 | }, 28 | mode, 29 | }) 30 | this.editor.mode = mode 31 | } 32 | 33 | get() { 34 | return this.editor.get() 35 | } 36 | 37 | set(json) { 38 | this.editor.set(json) 39 | } 40 | 41 | search(value) { 42 | this.editor.searchBox.dom.search.value = value 43 | this.editor.searchBox.dom.search.dispatchEvent(new Event('change')) 44 | } 45 | 46 | get mode() { 47 | return this.editor.mode 48 | } 49 | 50 | get searchResults() { 51 | return this.editor.searchBox.results 52 | } 53 | 54 | get activeResult() { 55 | return this.editor?.searchBox?.activeResult 56 | } 57 | 58 | refresh() { 59 | this.editor.refresh() 60 | } 61 | 62 | changeEditorMode(mode) { 63 | if (mode === 'tree') { 64 | document.getElementById('toggle').textContent = '切换为编辑模式' 65 | document.getElementById('jsonEditor').style.height = '' 66 | } else { 67 | document.getElementById('toggle').textContent = '切换为树形模式' 68 | document.getElementById('jsonEditor').style.height = '50vh' 69 | } 70 | this.editor.destroy() 71 | this.init('jsonEditor', mode) 72 | this.editor.set(getSchema(getPageKey())) 73 | } 74 | 75 | nextActive() { 76 | this.editor.searchBox.dom.input 77 | .querySelector('.jsoneditor-next') 78 | .dispatchEvent(new Event('click')) 79 | } 80 | 81 | static getInstance() { 82 | if (!SchemaEditor.instance) { 83 | SchemaEditor.instance = new SchemaEditor('jsonEditor') 84 | } 85 | return SchemaEditor.instance 86 | } 87 | } 88 | 89 | export default SchemaEditor.getInstance() 90 | -------------------------------------------------------------------------------- /src/app/modules/textArea.js: -------------------------------------------------------------------------------- 1 | import { 2 | debounce, getPageKey, isEqual, setSchema, 3 | } from '../../utils' 4 | import clickObjEditor from './clickObjEditor' 5 | import { 6 | getNowActivePath, getSessionStorage, initObserver, setSessionStorage, 7 | } from './public' 8 | import editor from './schemaEditor' 9 | import { resetToolsBtnStatus } from './toolsBtn' 10 | 11 | // 右侧输入框 12 | export function getTextArea() { 13 | return document.getElementById('domContext') 14 | } 15 | 16 | export function registerTextAreaInput() { 17 | const $textarea = getTextArea() 18 | $textarea.addEventListener('focusout', () => { 19 | // 便于触发点击事件 20 | // TODO: 优化异步setTimeout的执行顺序 21 | setTimeout(() => { 22 | $textarea.classList.toggle('focus') 23 | }, 150) 24 | }) 25 | $textarea.addEventListener('focus', () => { 26 | $textarea.classList.toggle('focus') 27 | resetToolsBtnStatus() 28 | 29 | setTimeout(() => { 30 | const activeData = getSessionStorage('activeValues') 31 | if (!activeData || activeData.length <= 1) { 32 | return 33 | } 34 | const lastData = activeData[activeData.length - 1] 35 | const path = getNowActivePath() 36 | for (const obj of activeData) { 37 | if (obj instanceof Object) { 38 | path.reduce((pre, key, idx) => { 39 | pre = pre[key] 40 | if (isEqual(pre, obj)) { 41 | setSessionStorage('activeObjPath', path.slice(0, idx + 1)) 42 | clickObjEditor.set(obj) 43 | } 44 | return pre 45 | }, lastData) 46 | break 47 | } 48 | } 49 | }, 150) 50 | }) 51 | $textarea.addEventListener( 52 | 'input', 53 | debounce(function () { 54 | if (!editor.activeResult?.node) { 55 | return 56 | } 57 | initObserver() 58 | // 更新点击dom 59 | getSessionStorage('clickDom').textContent = this.value 60 | 61 | // 更新editor 62 | editor.activeResult.node.value = this.value 63 | editor.activeResult.node.dom.value.click() 64 | editor.refresh() 65 | 66 | // 更新到本地 67 | setSchema(editor.get(), getPageKey()) 68 | }, 100), 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/app/modules/toolsBtn.js: -------------------------------------------------------------------------------- 1 | // 右侧输入框下方快捷操作json按钮组 2 | import { toast } from '../../components/Toast' 3 | import { cloneValue, copyRes } from '../../utils' 4 | import { 5 | getNowActivePath, 6 | getSessionStorage, 7 | jsonDataStack, 8 | setSessionStorage, 9 | updatePage, 10 | } from './public' 11 | import editor from './schemaEditor' 12 | 13 | const dataStack = jsonDataStack 14 | 15 | export function registerInputToolsBtn() { 16 | // TODO: 优化冗余代码 17 | const $textarea = document.getElementById('domContext') 18 | document.querySelector('.tools').addEventListener('click', (e) => { 19 | if (e.target.tagName.toLowerCase() !== 'button') return 20 | toast.success(e.target.textContent.trim()) 21 | switch (e.target.dataset.type) { 22 | case 'copy': 23 | copyRes($textarea.value) 24 | break 25 | case 'clear': 26 | execClear() 27 | break 28 | case 'back': 29 | execBack() 30 | break 31 | case 'delete': 32 | execDelete() 33 | break 34 | case 'copy-child': 35 | execCopyChild() 36 | break 37 | case 'before': 38 | execBefore() 39 | break 40 | case 'after': 41 | execAfter() 42 | break 43 | default: 44 | break 45 | } 46 | setSessionStorage('activeValues', null) 47 | }) 48 | function execClear() { 49 | if (!$textarea.value) { 50 | toast.warn('已经清空啦') 51 | return 52 | } 53 | 54 | dataStack.push(editor.get()) 55 | $textarea.value = '' 56 | $textarea.dispatchEvent(new Event('input')) 57 | } 58 | function execDelete() { 59 | // TODO: 删除对象的某个属性,待看看反馈是否需要 60 | const data = getSessionStorage('activeValues') 61 | if (!data?.length) { 62 | toast.error('请选择要删除的内容') 63 | return 64 | } 65 | const lastData = data[data.length - 1] 66 | const _index = data.findIndex((v) => v instanceof Array) 67 | if (_index === -1) { 68 | toast.error('此节点无法删除,请使用json更改') 69 | return 70 | } 71 | const d1 = data[_index] 72 | let key = data[_index - 1] 73 | if (key instanceof Object) { 74 | key = d1.findIndex((v) => v === key) 75 | } 76 | 77 | if (d1 instanceof Array) { 78 | dataStack.push(editor.get()) 79 | d1.splice(key, 1) 80 | updatePage(lastData, true) 81 | } 82 | } 83 | function execBack() { 84 | if (dataStack.length === 0) { 85 | toast.warn('没有可回退的内容') 86 | setTimeout(() => { 87 | toast.info('注:只能回退按钮操作') 88 | }, 1300) 89 | return 90 | } 91 | const t = dataStack.pop() 92 | updatePage(t, true) 93 | } 94 | function execCopyChild() { 95 | const data = getSessionStorage('activeValues') 96 | if (!data?.length) { 97 | toast.error('请选择要拷贝的内容') 98 | return 99 | } 100 | const lastData = data[data.length - 1] 101 | const _index = data.findIndex((v) => v instanceof Array) 102 | if (_index === -1) { 103 | toast.error('此节点无法拷贝,请使用json更改') 104 | return 105 | } 106 | const d1 = data[_index] 107 | let key = data[_index - 1] 108 | if (key instanceof Object) { 109 | key = d1.findIndex((v) => v === key) 110 | } 111 | 112 | if (d1 instanceof Array) { 113 | dataStack.push(editor.get()) 114 | d1.splice(key, 0, cloneValue(d1[key])) 115 | updatePage(lastData, true) 116 | } 117 | } 118 | function execBefore() { 119 | const data = getSessionStorage('activeValues') 120 | if (!data?.length) { 121 | toast.error('请选择要移动的内容') 122 | return 123 | } 124 | const lastData = data[data.length - 1] 125 | const _index = data.findIndex((v) => v instanceof Array) 126 | if (_index === -1) { 127 | toast.error('此节点无法移动,请使用json更改') 128 | return 129 | } 130 | const d1 = data[_index] 131 | let key = data[_index - 1] 132 | if (key instanceof Object) { 133 | key = d1.findIndex((v) => v === key) 134 | } 135 | if (key === 0) { 136 | toast.warn('已经在最前面啦') 137 | return 138 | } 139 | if (d1 instanceof Array) { 140 | dataStack.push(editor.get()); 141 | [d1[key], d1[key - 1]] = [d1[key - 1], d1[key]] 142 | updatePage(lastData, true) 143 | } 144 | } 145 | 146 | function execAfter() { 147 | const data = getSessionStorage('activeValues') 148 | if (!data?.length) { 149 | toast.error('请选择要移动的内容') 150 | return 151 | } 152 | const lastData = data[data.length - 1] 153 | const _index = data.findIndex((v) => v instanceof Array) 154 | if (_index === -1) { 155 | toast.error('此节点无法移动,请使用json更改') 156 | return 157 | } 158 | const d1 = data[_index] 159 | let key = data[_index - 1] 160 | if (key instanceof Object) { 161 | key = d1.findIndex((v) => v === key) 162 | } 163 | if (key === d1.length - 1) { 164 | toast.warn('已经在最后面啦') 165 | return 166 | } 167 | if (d1 instanceof Array) { 168 | dataStack.push(editor.get()); 169 | [d1[key], d1[key + 1]] = [d1[key + 1], d1[key]] 170 | updatePage(lastData, true) 171 | } 172 | } 173 | } 174 | 175 | export function resetToolsBtnStatus(disabledAll = false) { 176 | if (disabledAll) { 177 | return 178 | } 179 | setTimeout(() => { 180 | setSessionStorage('activeValues', null) 181 | if (!getSessionStorage('clickDom')) { 182 | return 183 | } 184 | const json = editor.get() 185 | const path = getNowActivePath() 186 | // 最外层值类型 - 不提供额外操作 187 | if (!path || path.length === 1) { 188 | return 189 | } 190 | const data = path.reduce( 191 | (p, n) => { 192 | // 倒序放入对象 193 | if (p[0][n] instanceof Object) { 194 | p.unshift(p[0][n]) 195 | } else { 196 | // 最后的key做为值插入 197 | p.unshift(n) 198 | } 199 | return p 200 | }, 201 | [json], 202 | ) 203 | setSessionStorage('activeValues', data) 204 | }, 100) 205 | } 206 | -------------------------------------------------------------------------------- /src/assets/css/app.scss: -------------------------------------------------------------------------------- 1 | @import './base.scss'; 2 | 3 | #page { 4 | display: block; 5 | width: $width; 6 | min-height: $height; 7 | border: 1px dashed #ddd; 8 | } 9 | .main { 10 | display: flex; 11 | justify-content: space-around; 12 | padding: 0.5rem; 13 | .right { 14 | margin: 0 1rem; 15 | .tips { 16 | padding: 0.5rem 0; 17 | color: #409eff; 18 | font-size: 0.8rem; 19 | } 20 | } 21 | #jsonEditor { 22 | width: calc(100vw - 900px); 23 | margin: 0 auto; 24 | position: sticky; 25 | top: 9rem; 26 | } 27 | } 28 | 29 | @media screen and (max-width: 1200px) { 30 | .main { 31 | display: flex; 32 | align-items: center; 33 | flex-direction: column; 34 | position: relative; 35 | padding: 0; 36 | .right { 37 | position: absolute; 38 | } 39 | #jsonEditor { 40 | width: auto; 41 | } 42 | } 43 | } 44 | 45 | .btns { 46 | text-align: center; 47 | button { 48 | color: #409eff; 49 | background: #ecf5ff; 50 | border: 1px solid #b3d8ff; 51 | margin: 0.5rem; 52 | border: none; 53 | padding: 0.5rem 1rem; 54 | border-radius: 0.5rem; 55 | outline: none; 56 | &:hover { 57 | cursor: pointer; 58 | transition: all 0.5s; 59 | color: #fff; 60 | background-color: #409eff; 61 | border-color: #409eff; 62 | } 63 | } 64 | } 65 | 66 | header { 67 | text-align: center; 68 | background-color: #24292e; 69 | padding: 16px; 70 | margin-bottom: 10px; 71 | position: relative; 72 | #tips { 73 | display: none; 74 | } 75 | #open-menu { 76 | display: none; 77 | background-color: transparent; 78 | outline: none; 79 | border: none; 80 | width: 20px; 81 | height: 20px; 82 | cursor: pointer; 83 | span { 84 | display: block; 85 | border-bottom: 2px solid #fff; 86 | margin-bottom: 5px; 87 | } 88 | } 89 | nav { 90 | display: block; 91 | a { 92 | color: #fff; 93 | font-weight: bold; 94 | margin: 0 10px; 95 | } 96 | span { 97 | height: 1; 98 | border-left: 2px solid #fff; 99 | } 100 | a.active { 101 | color: #24292e; 102 | background-color: #fff; 103 | font-size: 0.8rem; 104 | padding: 0.2rem; 105 | border-radius: 0.2rem; 106 | } 107 | } 108 | } 109 | 110 | @media screen and (max-width: 900px) { 111 | header { 112 | #open-menu { 113 | display: block; 114 | float: left; 115 | } 116 | #tips { 117 | display: block; 118 | color: #fff; 119 | text-align: center; 120 | } 121 | nav { 122 | display: none; 123 | width: 100%; 124 | position: absolute; 125 | left: 0; 126 | z-index: 1; 127 | background-color: #24292e; 128 | span { 129 | width: 80%; 130 | display: block; 131 | border: 0; 132 | margin: 0 auto; 133 | border-bottom: 2px solid #fff; 134 | } 135 | a { 136 | display: block; 137 | margin: 0.5rem auto; 138 | } 139 | } 140 | } 141 | } 142 | .right { 143 | position: relative; 144 | .input-area { 145 | position: sticky; 146 | top: 0rem; 147 | z-index: 1; 148 | } 149 | } 150 | 151 | #domContext { 152 | width: 100%; 153 | margin: 0 auto; 154 | margin-top: 0.5rem; 155 | padding: 1rem; 156 | box-sizing: border-box; 157 | resize: none; 158 | &:focus { 159 | box-shadow: 0 0 20px yellow; 160 | border: none; 161 | } 162 | & ~ div.tools { 163 | display: flex; 164 | justify-content: space-between; 165 | } 166 | } 167 | 168 | div.tools button { 169 | border: none; 170 | outline: none; 171 | color: #fff; 172 | background-color: #409eff; 173 | border-color: #409eff; 174 | display: inline-block; 175 | line-height: 1; 176 | white-space: nowrap; 177 | cursor: pointer; 178 | -webkit-appearance: none; 179 | text-align: center; 180 | box-sizing: border-box; 181 | outline: none; 182 | margin: 0; 183 | transition: 0.1s; 184 | font-weight: 500; 185 | padding: 0.6rem 1rem; 186 | border-radius: 4px; 187 | &.disabled { 188 | color: #fff; 189 | background-color: #a0cfff; 190 | border-color: #a0cfff; 191 | cursor: not-allowed; 192 | } 193 | &.success { 194 | color: #fff; 195 | background-color: #67c23a; 196 | border-color: #67c23a; 197 | } 198 | &:hover { 199 | opacity: 0.8; 200 | } 201 | } 202 | 203 | @media screen and (max-width: 1200px) { 204 | #domContext { 205 | padding: 0.2rem; 206 | &.focus { 207 | position: fixed; 208 | top: 0; 209 | left: 0; 210 | right: 0; 211 | height: 3rem; 212 | padding: 0.1rem 0.5rem; 213 | resize: vertical; 214 | & ~ div.tools { 215 | position: fixed; 216 | top: 4rem; 217 | left: 0; 218 | right: 0; 219 | button { 220 | display: block; 221 | margin-top: 0.5rem; 222 | font-size: 0.5rem; 223 | padding: 0.3rem 0.5rem; 224 | } 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/assets/css/base.scss: -------------------------------------------------------------------------------- 1 | $width: 794px; 2 | $height: 1120px; 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | a { 8 | text-decoration: none; 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ATQQ/resume/d4afecfed383cc77a46d2b36760d4ca614678a3d/src/assets/favicon.png -------------------------------------------------------------------------------- /src/components/Toast/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | 3 | class Toast { 4 | constructor() { 5 | const div = document.createElement('div') 6 | const span = document.createElement('span') 7 | this.toast = div 8 | this.span = span 9 | div.appendChild(span) 10 | } 11 | 12 | success(text, time) { 13 | this.toast.className = 'toast toast-success' 14 | this.span.textContent = text 15 | this.show(time) 16 | } 17 | 18 | error(text, time) { 19 | this.toast.className = 'toast toast-error' 20 | this.span.textContent = text 21 | this.show(time) 22 | } 23 | 24 | warn(text, time) { 25 | this.toast.className = 'toast toast-warn' 26 | this.span.textContent = text 27 | this.show(time) 28 | } 29 | 30 | info(text, time) { 31 | this.toast.className = 'toast toast-info' 32 | this.span.textContent = text 33 | this.show(time) 34 | } 35 | 36 | show(time = 2000) { 37 | document.body.appendChild(this.toast) 38 | if (this.timer) { 39 | clearTimeout(this.timer) 40 | } 41 | this.timer = setTimeout(() => { 42 | document.body.removeChild(this.toast) 43 | }, time) 44 | } 45 | 46 | static getInstance() { 47 | if (this.toast) { 48 | return this.toast 49 | } 50 | return new Toast() 51 | } 52 | } 53 | const toast = Toast.getInstance() 54 | export { 55 | toast, 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Toast/index.scss: -------------------------------------------------------------------------------- 1 | .toast{ 2 | position: fixed; 3 | text-align: center; 4 | box-sizing: border-box; 5 | left: 0; 6 | width: 100%; 7 | top: 10%; 8 | span{ 9 | padding: 8px 20px; 10 | border-radius: 6px; 11 | font-weight: bold; 12 | } 13 | &-success span{ 14 | background-color: #f0f9eb; 15 | color: #67C23A; 16 | } 17 | &-error span{ 18 | background-color: #fef0f0; 19 | color: #F56C6C; 20 | } 21 | &-warn span{ 22 | background-color: #fdf6ec; 23 | color: #E6A23C; 24 | } 25 | &-info span{ 26 | background-color: #ecf5ff; 27 | color: #409EFF; 28 | } 29 | } -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const navTitle = { 2 | demo1: '模板1', 3 | react1: '模板2', 4 | vue1: '模板3', 5 | icons: '图标列表', 6 | introduce: '使用文档', 7 | abc: '演示示例', 8 | } 9 | -------------------------------------------------------------------------------- /src/constants/schema.js: -------------------------------------------------------------------------------- 1 | import abc from './schema/abc' 2 | import demo1 from './schema/demo1' 3 | import react1 from './schema/react1' 4 | import vue1 from './schema/vue1' 5 | 6 | export default{ 7 | abc,demo1,react1,vue1 8 | } -------------------------------------------------------------------------------- /src/constants/schema/abc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: '王五', 3 | position: '求职目标: Web前端工程师', 4 | infos: [ 5 | '1:很多文字', 6 | '2:很多文字', 7 | '3:很多文字', 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/schema/demo1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: '张三', 3 | position: 'Web前端工程师', 4 | avatar: 'https://img.cdn.sugarat.top/mdImg/MTYxMzMwNTIyNzkyNA==613305227924', 5 | left: [ 6 | { 7 | title: '基本资料', 8 | introduce: [ 9 | '电话:12345678910', 10 | '邮箱:engineerzjl@foxmail.com', 11 | '学校:五道口职业技术学院(本科)', 12 | '专业:计算机科学与技术', 13 | ['Github:', { type: 'a', href: 'https://github.com/ATQQ', text: 'ATQQ' }], 14 | ['掘金:', { type: 'a', href: 'https://juejin.cn/user/1028798615918983', text: '粥里有勺糖' }], 15 | ], 16 | }, 17 | { 18 | title: '专业技能', 19 | introduce: [ 20 | '前端:Vue、uni-app、小程序', 21 | '服务端:egg.js,express,了解koa', 22 | '数据库:MySql,MongoDB,Redis', 23 | '其它:Git,PS,AE', 24 | ], 25 | }, 26 | { 27 | title: '奖项荣誉', 28 | introduce: [ 29 | '2020年02月 国家奖学金', 30 | '2020年11月 国家励志奖学金', 31 | '2020年02月 XX大赛一等奖', 32 | ], 33 | }, 34 | { 35 | title: '自我评价', 36 | introduce: [ 37 | '2年前端学习与开发经验', 38 | '熟悉小程序,h5开发,有上线产品开发经验', 39 | '阅读过Vue源码,了解其核心原理', 40 | '有多人项目合作开发经验,良好的跨职能沟通能力', 41 | ], 42 | }, 43 | ], 44 | right: [ 45 | { 46 | title: '校园经历', 47 | introduce: [ 48 | { 49 | ordinary: [ 50 | '2018.09 – 2022.06', 51 | ['五道口职业技术学院', '硕士', '计算机科学与技术'], 52 | ], 53 | dot: [ 54 | '担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉', 55 | '担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)', 56 | ], 57 | }, 58 | { 59 | ordinary: [ 60 | '2015.09 – 2019.06', 61 | ['五道口职业技术学院', '本科', '软件工程'], 62 | ], 63 | dot: [ 64 | '担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉', 65 | '担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)', 66 | ], 67 | }, 68 | ], 69 | }, 70 | { 71 | title: '实习经历', 72 | introduce: [ 73 | { 74 | ordinary: [ 75 | ['美团', 'Web前端工程师', '2019.12 – 2020.3'], 76 | ], 77 | dot: [ 78 | '参与XX项目的研发,解决了xx难题', 79 | '开发了xx插件/工具,解决了xx问题,提升了xx效率', 80 | '优化xx项目,收益xx', 81 | ], 82 | }, 83 | ], 84 | }, 85 | { 86 | title: '项目经验', 87 | introduce: [ 88 | { 89 | ordinary: [ 90 | ['项目/产品名称 - 校园二手交易平台', { type: 'a', href: 'https://github.com/ATQQ/resume', text: 'xx.yy.com' }], 91 | ], 92 | dot: [ 93 | '现有的xx方式处理xxx情况存在xx问题', 94 | '负责需求分析,数据库设计,前后端开发', 95 | '解决了XXXXX问题,学会了xx技术,收获了xx', 96 | '前端部分使用Vue,服务端使用Node/TS/MySQL', 97 | ], 98 | }, 99 | { 100 | ordinary: [ 101 | ['XX产品名称', { type: 'a', href: 'https://github.com/ATQQ/resume', text: 'xx.yy.com' }], 102 | ], 103 | dot: [ 104 | '简介:校园二手交易平台', 105 | '难点:图片压缩,大量图片加载,级联选择组件', 106 | '收获:了解了常见图片压缩方案,图片的加载优化方案', 107 | '技术栈:Vue/express/typescript/mysql/TS/MySQL', 108 | ], 109 | }, 110 | { 111 | ordinary: [ 112 | ['XX产品名称', { type: 'a', href: 'https://github.com/ATQQ/resume', text: 'xx.yy.com' }], 113 | ], 114 | dot: [ 115 | '参与二手交易平台的前端建设,服务端开发,数据库设计', 116 | '使用跨端开发框架xx,开发了web,小程序,app等多端应用', 117 | '有xx特色功能', 118 | ], 119 | }, 120 | ], 121 | }, 122 | ], 123 | } 124 | -------------------------------------------------------------------------------- /src/constants/schema/react1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'React编写', 3 | position: '求职目标: Web前端工程师', 4 | infos: [ 5 | { icon: 'dianhua', value: '12345678910' }, 6 | { icon: 'dianziyoujian', value: 'daffahd22@qq.com' }, 7 | { icon: 'xueli', value: '五道口职业技术学院(本科)' }, 8 | { icon: 'jisuanjishuiping', value: '计算机科学与技术' }, 9 | ], 10 | leftExps: [ 11 | { 12 | title: '专业技能', 13 | values: [ 14 | { 15 | introduce: [ 16 | '熟悉HTML,JS,CSS,熟悉ES5,了解ES6,熟悉Vue开发,了解其核心原理', 17 | '熟悉常用的数据结构;熟悉计算机网络,操作系统等;', 18 | '了解Node,有后端开发经验,熟悉MySql,了解Redis,MongoDB', 19 | ], 20 | }, 21 | ], 22 | }, 23 | { 24 | title: '校园经历', 25 | values: [ 26 | { 27 | des: ['2018.09 – 2022.06', '五道口职业技术学院(本科)', '软件工程'], 28 | introduce: [ 29 | '担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉', 30 | '担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)', 31 | ], 32 | }, 33 | { 34 | des: ['2018.09 – 2022.06', '五道口职业技术学院(本科)', '软件工程'], 35 | introduce: [ 36 | '担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉', 37 | '担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)', 38 | ], 39 | }, 40 | ], 41 | }, 42 | { 43 | title: '实习经历', 44 | values: [ 45 | { 46 | des: ['2019.12 – 2020.3', '美团', 'Web前端工程师'], 47 | introduce: [ 48 | '参与XX项目的研发,解决了xx难题', 49 | '开发了xx插件/工具,解决了xx问题,提升了xx效率', 50 | ], 51 | }, 52 | ], 53 | }, 54 | { 55 | title: '项目经验', 56 | values: [ 57 | { 58 | des: ['上天入地 - 校园二手交易平台', 'github.com/ATQQ/resume'], 59 | introduce: [ 60 | '现有的xx方式处理xxx情况存在xx问题', 61 | '前端部分使用Vue,服务端使用Node/TS/MySQL', 62 | ], 63 | }, 64 | { 65 | des: ['天地玄黄', '这里可以放生成的短链'], 66 | introduce: [ 67 | '简介:校园二手交易平台', 68 | '难点:图片压缩,大量图片加载,级联选择组件', 69 | '技术栈:Vue/express/typescript/mysql/TS/MySQL', 70 | ], 71 | }, 72 | { 73 | des: ['阿巴阿巴', 'www.baidu.com'], 74 | introduce: [ 75 | '参与二手交易平台的前端建设,服务端开发,数据库设计', 76 | '使用跨端开发框架xx,开发了web,小程序,app等多端应用', 77 | 'xxxxxdadaddad特色', 78 | ], 79 | }, 80 | ], 81 | }, 82 | { 83 | title: '开源项目', 84 | values: [ 85 | { 86 | des: ['xx Vs Code插件', '超链接'], 87 | introduce: [ 88 | '简介:图床xxx', 89 | '难点:图片压缩,大量图片加载,级联选择组件', 90 | '技术栈:Vue/express/typescript/mysql/TS/MySQL', 91 | ], 92 | }, 93 | ], 94 | }, 95 | ], 96 | rightExps: [ 97 | { 98 | title: '获奖荣誉', 99 | values: [ 100 | { 101 | des: ['2020-11', '国家奖学金'], 102 | }, 103 | { 104 | des: ['2020-10', '国家励志奖学金'], 105 | }, 106 | { 107 | des: ['2020-9', 'XX大赛金牌'], 108 | }, 109 | ], 110 | }, 111 | { 112 | title: '兴趣爱好', 113 | values: [ 114 | { 115 | des: ['篮球', '足球', '吉他', '阿巴阿巴'], 116 | }, 117 | ], 118 | }, 119 | { 120 | title: '个人站点', 121 | values: [ 122 | { 123 | des: ['掘金', '粥里有勺糖'], 124 | }, 125 | { 126 | des: ['CSDN', '粥里有勺糖'], 127 | }, 128 | { 129 | des: ['博客园', '粥里有勺糖'], 130 | }, 131 | ], 132 | }, 133 | { 134 | title: '自我评价', 135 | values: [ 136 | { 137 | introduce: [ 138 | '丰富实习经验,热爱互联网', 139 | '学习能力强,责任感强烈,能承受压力及时完成任务', 140 | '熟悉小程序,h5开发,有上线产品开发经验', 141 | '阅读过Vue源码,了解其核心原理', 142 | ], 143 | }, 144 | ], 145 | }, 146 | ], 147 | } 148 | -------------------------------------------------------------------------------- /src/constants/schema/vue1.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Vue编写', 3 | position: '求职意向:Web前端工程师', 4 | infos: [ 5 | { icon: 'dianhua', value: '12345678910' }, 6 | { icon: 'dianziyoujian', value: 'daffahd22@qq.com' }, 7 | { icon: 'xueli', value: '五道口职业技术学院(本科)' }, 8 | { icon: 'jisuanjishuiping', value: '计算机科学与技术' }, 9 | ], 10 | exps: [ 11 | { 12 | title: '专业技能', 13 | values: [ 14 | { 15 | introduce: [ 16 | '熟悉HTML,JS,CSS,熟悉ES5,了解ES6,熟悉Vue开发,了解其核心原理', 17 | '熟悉常用的数据结构;熟悉计算机网络,操作系统等;', 18 | '了解Node,有后端开发经验,熟悉MySql,了解Redis,MongoDB', 19 | ], 20 | }, 21 | ], 22 | }, 23 | { 24 | title: '获奖荣誉', 25 | values: [ 26 | { 27 | des: { 28 | time: '2020年02月', 29 | tips: ['国家奖学金'], 30 | }, 31 | }, 32 | { 33 | des: { 34 | time: '2020年10月', 35 | tips: ['国家励志奖学金'], 36 | }, 37 | }, 38 | { 39 | des: { 40 | time: '2020年11月', 41 | tips: ['XX大赛金牌'], 42 | }, 43 | }, 44 | ], 45 | }, 46 | // { 47 | // title: '获奖另一种展示方式', 48 | // values: [ 49 | // { 50 | // introduce: [ 51 | // '2020-12 国家xxx奖', 52 | // '2020-11 省xxx奖', 53 | // '2020-10 校xxx奖', 54 | // ] 55 | // }, 56 | // ] 57 | // }, 58 | { 59 | title: '校园经历', 60 | values: [ 61 | { 62 | des: { 63 | time: '2018.09 – 2022.06', 64 | tips: ['五道口职业技术学院', '本科', '软件工程'], 65 | }, 66 | introduce: [ 67 | '担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉', 68 | '担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)', 69 | ], 70 | }, 71 | { 72 | des: { 73 | time: '2018.09 – 2022.06', 74 | tips: ['五道口职业技术学院', '本科', '软件工程'], 75 | }, 76 | introduce: [ 77 | '担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉', 78 | '担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)', 79 | ], 80 | }, 81 | ], 82 | }, 83 | { 84 | title: '实习经历', 85 | values: [ 86 | { 87 | des: { 88 | time: '2019.12 – 2020.3', 89 | tips: ['美团', 'Web前端工程师'], 90 | }, 91 | introduce: [ 92 | '参与XX项目的研发,解决了xx难题', 93 | '开发了xx插件/工具,解决了xx问题,提升了xx效率', 94 | ], 95 | }, 96 | ], 97 | }, 98 | { 99 | title: '项目经验', 100 | values: [ 101 | { 102 | des: { 103 | time: '上天入地 - 校园二手交易平台', 104 | tips: ['github.com/ATQQ/resume'], 105 | }, 106 | introduce: [ 107 | '现有的xx方式处理xxx情况存在xx问题', 108 | '前端部分使用Vue,服务端使用Node/TS/MySQL', 109 | ], 110 | }, 111 | { 112 | des: { 113 | time: '天地玄黄', 114 | tips: ['这里可以放生成的短链'], 115 | }, 116 | introduce: [ 117 | '简介:校园二手交易平台', 118 | '难点:图片压缩,大量图片加载,级联选择组件', 119 | '技术栈:Vue/express/typescript/mysql/TS/MySQL', 120 | ], 121 | }, 122 | ], 123 | }, 124 | ], 125 | } 126 | -------------------------------------------------------------------------------- /src/constants/svgs/index.js: -------------------------------------------------------------------------------- 1 | export default function svgList(color = 'black') { 2 | const dianhua = '' 3 | const dianziyoujian = '' 4 | const xueli = '' 5 | const jisuanjishuiping = '' 6 | const experience1 = '' 7 | const certificate1 = '' 8 | const communicate = '' 9 | const file1 = '' 10 | const healthy1 = '' 11 | const activity = '' 12 | const experience2 = '' 13 | const city = '' 14 | const cet = '' 15 | const money = '' 16 | const origin = { 17 | dianhua, 18 | dianziyoujian, 19 | xueli, 20 | jisuanjishuiping, 21 | experience1, 22 | certificate1, 23 | communicate, 24 | file1, 25 | healthy1, 26 | activity, 27 | experience2, 28 | city, 29 | cet, 30 | money, 31 | } 32 | 33 | const res = Object.keys(origin).reduce((pre, key) => { 34 | pre[key] = origin[key].replace(/fill=".*?"/g, `fill="${color}"`) 35 | return pre 36 | }, {}) 37 | return res 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/abc/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { getSchema } from '../../utils' 3 | 4 | export default function App() { 5 | const [schema, updateSchema] = useState(getSchema()) 6 | const { name, position, infos = [] } = schema 7 | useEffect(() => { 8 | window.refresh = function () { 9 | updateSchema(getSchema()) 10 | } 11 | }, []) 12 | return ( 13 |
14 |
15 |

{name}

16 |

{position}

17 |
18 |
19 | { 20 | infos.map((info, i) => { 21 | return

{info}

22 | }) 23 | } 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/abc/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | -------------------------------------------------------------------------------- /src/pages/abc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/pages/abc/index.js: -------------------------------------------------------------------------------- 1 | // import { getSchema } from "../../utils" 2 | // import './index.scss' 3 | 4 | // window.refresh = function () { 5 | // const schema = getSchema() 6 | // const { name, position, infos } = schema 7 | 8 | // clearPage() 9 | // renderHeader(name, position) 10 | // renderInfos(infos) 11 | // } 12 | 13 | // function clearPage() { 14 | // document.getElementById('app').innerHTML = '' 15 | // } 16 | 17 | // function renderHeader(name, position) { 18 | // const html = ` 19 | //
20 | //

${name}

21 | //

${position}

22 | //
` 23 | // document.getElementById('app').innerHTML += html 24 | // } 25 | 26 | // function renderInfos(infos = []) { 27 | // if (infos?.length === 0) { 28 | // return 29 | // } 30 | // const html = ` 31 | // ` 36 | // document.getElementById('app').innerHTML += html 37 | // } 38 | 39 | // window.onload = function () { 40 | // refresh() 41 | // } 42 | 43 | import Vue from 'vue' 44 | import App from './App.vue' 45 | import './index.scss' 46 | 47 | Vue.config.productionTip = process.env.NODE_ENV === 'development' 48 | 49 | new Vue({ 50 | render: (h) => h(App), 51 | }).$mount('#app') 52 | 53 | // import React from 'react' 54 | // import ReactDOM from 'react-dom'; 55 | // import App from './App.jsx' 56 | // import './index.scss' 57 | 58 | // ReactDOM.render( 59 | // 60 | // 61 | // , 62 | // document.getElementById('app') 63 | // ) 64 | 65 | // import { getSchema } from "../../utils" 66 | // import './index.scss' 67 | 68 | // window.refresh = function () { 69 | // const schema = getSchema() 70 | // const { name, position, infos } = schema 71 | 72 | // clearPage() 73 | // renderHeader(name, position) 74 | // renderInfos(infos) 75 | // } 76 | 77 | // function clearPage() { 78 | // $('#app').empty() 79 | // } 80 | 81 | // function renderHeader(name, position) { 82 | // const html = ` 83 | //
84 | //

${name}

85 | //

${position}

86 | //
` 87 | // $('#app').append(html) 88 | // } 89 | 90 | // function renderInfos(infos = []) { 91 | // if (infos?.length === 0) { 92 | // return 93 | // } 94 | // const html = ` 95 | // ` 100 | // $('#app').append(html) 101 | // } 102 | 103 | // window.onload = function () { 104 | // refresh() 105 | // } 106 | -------------------------------------------------------------------------------- /src/pages/abc/index.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/base.scss'; 2 | html, 3 | body, 4 | #resume { 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | // 上面部分是推荐引入的通用样式 9 | 10 | // 下面书写我们的样式 11 | $themeColor: red; 12 | 13 | #app { 14 | padding: 1rem; 15 | } 16 | 17 | header { 18 | h1 { 19 | color: $themeColor; 20 | } 21 | h2 { 22 | font-weight: lighter; 23 | } 24 | } 25 | 26 | .infos { 27 | list-style: none; 28 | li { 29 | color: $themeColor; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/demo1/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | demo1 9 | 10 | 11 | 12 |
13 | 14 |

张三

15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
Web前端工程师
23 |
24 | 25 | 26 | 27 |
28 |
29 | 30 |
31 |

32 |

33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 |
46 | 基本资料 47 |
48 |
    49 |
  • 电话:12345678910
  • 50 |
  • 邮箱:engineerzjl@foxmail.com
  • 51 |
  • 学校:五道口职业技术学院(本科)
  • 52 |
  • 专业:计算机科学与技术
  • 53 |
  • Github:ATQQ
  • 54 |
  • 掘金:粥里有勺糖
  • 55 |
56 |
57 |
58 |
59 | 专业技能 60 |
61 |
    62 |
  • 前端:Vue、uni-app、小程序
  • 63 |
  • 服务端:egg.js,express,了解koa
  • 64 |
  • 数据库:MySql,MongoDB,Redis
  • 65 |
  • 其它:Git,PS,AE
  • 66 |
67 |
68 |
69 |
70 | 奖项荣誉 71 |
72 |
    73 |
  • 2020年02月 国家奖学金
  • 74 |
  • 2020年11月 国家励志奖学金
  • 75 |
  • 2020年02月 XX大赛一等奖
  • 76 |
  • 2020年02月 XX大赛省一
  • 77 |
78 |
79 |
80 |
81 | 自我评价 82 |
83 |
    84 |
  • 2年前端学习与开发经验
  • 85 |
  • 熟悉小程序,h5开发,有上线产品开发经验
  • 86 |
  • 阅读过Vue源码,了解其核心原理
  • 87 |
  • 有多人项目合作开发经验,良好的跨职能沟通能力
  • 88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 校园经历 96 |
97 |
    98 |
  • 2018.09 – 2022.06
  • 99 |
  • 五道口职业技术学院 硕士 计算机科学与技术
  • 100 |
  • 担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉
  • 101 |
  • 担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)
  • 102 |
103 |
    104 |
  • 2015.09 – 2019.06
  • 105 |
  • 五道口职业技术学院 本科 软件工程
  • 106 |
  • 担任(加入)xx实验室/团队 (职称),负责了实验室门户网站的维护,带领团队成员参加了xx大赛,获得xx荣誉
  • 107 |
  • 担任xxx,负责了xxx工作,收获了XX技能(如Adobe系列)
  • 108 |
109 |
110 |
111 |
112 | 实习经历 113 |
114 |
    115 |
  • 美团 Web前端工程师2019.12 – 2020.3
  • 116 |
  • 参与XX项目的研发,解决了xx难题
  • 117 |
  • 开发了xx插件/工具,解决了xx问题,提升了xx效率
  • 118 |
  • 优化xx项目,收益xx
  • 119 |
120 |
121 |
122 |
123 | 项目经验 124 |
125 |
    126 |
  • 项目/产品名称 - 校园二手交易平台 xx.yy.com 127 |
  • 128 |
  • 现有的xx方式处理xxx情况存在xx问题
  • 129 |
  • 负责需求分析,数据库设计,前后端开发
  • 130 |
  • 解决了XXXXX问题,学会了xx技术,收获了xx
  • 131 |
  • 前端部分使用Vue,服务端使用Node/TS/MySQL
  • 132 |
133 |
    134 |
  • XX产品名称 xx.yy.com
  • 135 |
  • 简介:校园二手交易平台
  • 136 |
  • 难点:图片压缩,大量图片加载,级联选择组件
  • 137 |
  • 收获:了解了常见图片压缩方案,图片的加载优化方案
  • 138 |
  • 技术栈:Vue/express/typescript/mysql
  • 139 |
140 |
    141 |
  • XX产品名称 xx.yy.com
  • 142 |
  • 参与二手交易平台的前端建设,服务端开发,数据库设计
  • 143 |
  • 使用跨度开发框架xx,开发了web,小程序,app等多端应用
  • 144 |
  • 有xx特色功能
  • 145 |
146 |
147 |
148 |
149 |
150 | 151 | 152 | -------------------------------------------------------------------------------- /src/pages/demo1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 |
15 | 16 |

张三

17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
Web前端工程师
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |

34 |

35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /src/pages/demo1/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | import { createLink, getSchema } from '../../utils/index' 3 | 4 | function updateName(value) { 5 | document.getElementById('name').textContent = value 6 | } 7 | function updatePosition(value) { 8 | document.getElementById('position').textContent = value 9 | } 10 | function updateAvatar(value) { 11 | const $avatar = document.getElementById('avatar') 12 | if (!value) { 13 | $avatar.style.display = 'none' 14 | return 15 | } 16 | $avatar.querySelector('img').style.transform = 'scale(1) translateY(0px)' 17 | $avatar.style.display = 'block' 18 | $avatar.querySelector('img').src = value 19 | const $img = new Image() 20 | $img.src = value 21 | $img.onload = function () { 22 | const { width } = $avatar.querySelector('img').getBoundingClientRect() 23 | const containerWidth = 160 24 | if (width < containerWidth) { 25 | const scaleValue = containerWidth / width 26 | $avatar.querySelector( 27 | 'img', 28 | ).style.transform = `scale(${scaleValue}) translateY(${(containerWidth - width) / 2}px)` 29 | } else { 30 | $avatar.querySelector('img').style.transform = 'scale(1) translateY(0px)' 31 | } 32 | } 33 | } 34 | 35 | function createDom(des) { 36 | if (typeof des === 'string') { 37 | const $span = document.createElement('span') 38 | $span.textContent = des 39 | return $span 40 | } 41 | 42 | if (des instanceof Object) { 43 | const { type } = des 44 | if (type === 'a') { 45 | const { href, text } = des 46 | return createLink(text, href, true) 47 | } 48 | } 49 | } 50 | 51 | function parseIntroduceSchema(value) { 52 | let str = '' 53 | if (typeof value === 'string') { 54 | str = value 55 | } 56 | if (value instanceof Array) { 57 | str = value.map((_v) => createDom(_v).outerHTML).join('') 58 | } 59 | return str 60 | } 61 | 62 | function updateLeftInfo(value) { 63 | const $leftInfo = document.getElementById('leftInfo') 64 | $leftInfo.innerHTML = '' 65 | const createInfoItemHtml = (item) => { 66 | const { title, introduce } = item 67 | const li = introduce.map((v) => `
  • ${parseIntroduceSchema(v)}
  • `).join('') 68 | 69 | const domHtml = ` 70 |
    71 |
    72 | ${title} 73 |
    74 | 77 |
    78 | ` 79 | return domHtml 80 | } 81 | 82 | for (const item of value) { 83 | $leftInfo.innerHTML += createInfoItemHtml(item) 84 | } 85 | } 86 | 87 | function updateRightInfo(value) { 88 | const $rightInfo = document.getElementById('rightInfo') 89 | $rightInfo.innerHTML = '' 90 | const createInfoItemHtml = (item) => { 91 | const { title, introduce } = item 92 | const ul = introduce 93 | .map((v) => { 94 | let str = '' 95 | const { ordinary, dot } = v 96 | str += ordinary.map((v) => `
  • ${parseIntroduceSchema(v)}
  • `).join('') 97 | str += dot.map((v) => `
  • ${parseIntroduceSchema(v)}
  • `).join('') 98 | return `` 99 | }) 100 | .join('') 101 | const domHtml = ` 102 |
    103 |
    104 | ${title} 105 |
    106 | ${ul} 107 |
    108 | ` 109 | return domHtml 110 | } 111 | 112 | for (const item of value) { 113 | $rightInfo.innerHTML += createInfoItemHtml(item) 114 | } 115 | } 116 | 117 | window.refresh = function () { 118 | const data = getSchema() 119 | const { 120 | name, position, avatar, left, right, 121 | } = data || {} 122 | // 隐藏 123 | document.getElementById('resume').style.display = 'none' 124 | 125 | // 更新数据 126 | // TODO: 做一层diff 127 | updateName(name) 128 | updatePosition(position) 129 | updateAvatar(avatar) 130 | updateLeftInfo(left || []) 131 | updateRightInfo(right || []) 132 | 133 | // 展示 134 | document.getElementById('resume').style.display = 'block' 135 | } 136 | 137 | window.refresh() 138 | -------------------------------------------------------------------------------- /src/pages/demo1/index.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets//css/base.scss'; 2 | $themeColor: #b9ffe6; 3 | $themeColorDeep: #7bffd1; 4 | $panelColor: #eee; 5 | html, 6 | body, 7 | #resume { 8 | height: 100%; 9 | overflow: hidden; 10 | } 11 | #resume { 12 | margin: 0 auto; 13 | padding: 0; 14 | width: $width; 15 | } 16 | html ::-webkit-scrollbar { 17 | width: 0 !important; 18 | } 19 | 20 | .name { 21 | font-size: 3rem; 22 | text-align: center; 23 | font-weight: bold; 24 | } 25 | .position { 26 | text-align: center; 27 | margin-top: 0.5em; 28 | position: relative; 29 | font-weight: lighter; 30 | .leftSymbol, 31 | .rightSymbol { 32 | position: absolute; 33 | top: 0; 34 | } 35 | .leftSymbol { 36 | left: 16%; 37 | } 38 | .rightSymbol { 39 | right: 16%; 40 | } 41 | .s1 { 42 | font-size: 1.5rem; 43 | } 44 | .s2 { 45 | font-size: 1.2rem; 46 | } 47 | .s3 { 48 | font-size: 0.9rem; 49 | } 50 | } 51 | 52 | .splitLine { 53 | margin-top: 1rem; 54 | p { 55 | border-bottom: 6px solid $themeColor; 56 | margin-top: 0.2rem; 57 | } 58 | } 59 | 60 | main { 61 | margin-top: 0.3rem; 62 | height: 100%; 63 | overflow: hidden; 64 | .left, 65 | .right { 66 | float: left; 67 | } 68 | .left { 69 | width: 40%; 70 | height: 100%; 71 | background-color: $panelColor; 72 | #leftInfo { 73 | padding-bottom: 1.5rem; 74 | } 75 | } 76 | .right { 77 | height: 100%; 78 | width: 50%; 79 | } 80 | } 81 | 82 | .avatar { 83 | width: 10rem; 84 | height: 10rem; 85 | overflow: hidden; 86 | margin: 1rem auto 0 auto; 87 | border-radius: 50%; 88 | border: 0.2rem solid #999999; 89 | text-align: center; 90 | img { 91 | height: 100%; 92 | } 93 | } 94 | 95 | .info-item { 96 | padding-left: 3rem; 97 | padding-right: 1rem; 98 | margin-top: 1.5rem; 99 | .title { 100 | width: 82%; 101 | font-size: 1.5rem; 102 | text-align: left; 103 | background-color: $themeColor; 104 | padding: 0.2rem 0 0.2rem 30%; 105 | position: relative; 106 | &::before { 107 | content: ''; 108 | top: 0; 109 | left: 0; 110 | display: block; 111 | position: absolute; 112 | border: 1.3rem solid transparent; 113 | border-left: 1rem solid 114 | $panelColor; 115 | } 116 | &::after { 117 | content: ''; 118 | top: -0.6rem; 119 | right: 0.3rem; 120 | display: block; 121 | position: absolute; 122 | border: 0.6rem solid transparent; 123 | border-right: 0.6rem solid 124 | $themeColorDeep; 125 | transform: rotate(-45deg); 126 | } 127 | } 128 | ul { 129 | list-style: none; 130 | li { 131 | margin-top: 0.2rem; 132 | line-height: 1.5rem; 133 | font-size: 0.8rem; 134 | a { 135 | color: #000; 136 | } 137 | } 138 | } 139 | } 140 | 141 | .exp-info { 142 | overflow: hidden; 143 | padding-left: 3rem; 144 | margin-top: 1.5rem; 145 | .title { 146 | font-size: 1.5rem; 147 | } 148 | ul { 149 | margin-top: 1rem; 150 | li { 151 | line-height: 1.2rem; 152 | font-size: 0.8rem; 153 | display: flex; 154 | justify-content: space-between; 155 | a { 156 | color: #000; 157 | } 158 | } 159 | li.dot { 160 | display: block; 161 | &::before { 162 | content: '▪'; 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/pages/icons/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { copyRes, getSchema } from '../../utils' 3 | import Icon from './components/Icon'; 4 | 5 | import './index.scss' 6 | 7 | import svgList from '../../constants/svgs'; 8 | 9 | export default function App() { 10 | const [data, setJSON] = useState(getSchema()) 11 | useEffect(() => { 12 | window.refresh = () => { 13 | setJSON(getSchema()) 14 | } 15 | }, []) 16 | 17 | const [icons] = useState(svgList('#666')) 18 | const keys = Object.keys(icons) 19 | return ( 20 |
    21 |
    22 |

    图标列表

    23 |
    24 | 32 | 39 |
    40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/pages/icons/components/Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Icon(props) { 4 | const { 5 | name, 6 | html, 7 | handleClick, 8 | } = props; 9 | return ( 10 |
  • { 13 | handleClick && 14 | handleClick(name); 15 | }} 16 | > 17 | 22 |
    {name}
    23 |
  • 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/icons/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 |
    15 |
    16 | 17 |
    18 |
    19 | 20 | 21 | -------------------------------------------------------------------------------- /src/pages/icons/index.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/base.scss'; 2 | html, 3 | body, 4 | #resume { 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | $themeColor: #3b8fcb; 9 | #resume { 10 | padding: 1rem 2rem; 11 | } 12 | header { 13 | padding: 1rem 0; 14 | overflow: hidden; 15 | h1 { 16 | text-align: center; 17 | } 18 | } 19 | 20 | ul { 21 | text-align: center; 22 | list-style: none; 23 | } 24 | .icon-list { 25 | list-style: none; 26 | display: flex; 27 | flex-wrap: wrap; 28 | } 29 | .icon-item { 30 | color: #666; 31 | cursor: pointer; 32 | min-width: 14%; 33 | i { 34 | width: 2.5rem; 35 | height: 2.5rem; 36 | display: block; 37 | margin: 0 auto; 38 | } 39 | .name { 40 | margin-top: 0.5rem; 41 | text-align: center; 42 | } 43 | margin: 1rem; 44 | } 45 | .icon { 46 | width: 2.5rem; 47 | height: 2.5rem; 48 | // vertical-align: -0.15rem; 49 | fill: currentColor; 50 | overflow: hidden; 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/icons/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App.jsx' 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('app'), 10 | ) 11 | -------------------------------------------------------------------------------- /src/pages/introduce/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 |

    使用文档

    15 |
    16 |

    1. 如何导出pdf

    17 |
      18 |
    1. 提供了两种导出方式(区别在超链接的支持程度上)
    2. 19 |
    3. 电脑推荐选择 (PC-打印)按钮
    4. 20 |
    5. 其它终端推荐使用(jsPDF - 打印)
    6. 21 |
    22 |
      23 |

      2. 如何修改简历

      24 |
        25 |
      • 修改的内容都会实时保存在本地
      • 26 |
      27 |
        28 |
      1. 每个简历,对应一个json schema
      2. 29 |
      3. 在简历右侧有一个JSON编辑器
      4. 30 |
      5. 31 | 33 |
      6. 34 |
      7. 当然也可直接点击想要修改的内容,在编辑框中进行内容修改
      8. 35 |
      36 |

      操作演示

      37 |
        38 |
      • 39 | 40 |
      • 41 |
      42 |
      43 | 44 | 45 | -------------------------------------------------------------------------------- /src/pages/introduce/index.js: -------------------------------------------------------------------------------- 1 | import './index.scss' 2 | -------------------------------------------------------------------------------- /src/pages/introduce/index.scss: -------------------------------------------------------------------------------- 1 | h1{ 2 | color: red; 3 | } -------------------------------------------------------------------------------- /src/pages/react1/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { getSchema } from '../../utils' 3 | import BaseInfo from './components/BaseInfo' 4 | import Exps from './components/Exps' 5 | import OtherInfo from './components/OtherInfo' 6 | import './index.scss' 7 | 8 | export default function App() { 9 | const [data, setJSON] = useState(getSchema()) 10 | useEffect(() => { 11 | window.refresh = () => { 12 | setJSON(getSchema()) 13 | } 14 | }, []) 15 | const { name, position, infos = [], leftExps = [], rightExps = [] } = data 16 | return ( 17 |
      18 |
      19 | 20 | 21 |
      22 |
      23 |
      24 | 25 |
      26 |
      27 | 28 |
      29 |
      30 |
      31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/react1/components/BaseInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function BaseInfo(props) { 4 | const { name, position } = props 5 | return ( 6 |
      7 |

      {name}

      8 |

      {position}

      9 |
      10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/react1/components/Exps.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Exps(props) { 4 | const { exps } = props 5 | return ( 6 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/react1/components/OtherInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import svgList from './../../../constants/svgs' 3 | const $svgList = svgList('#3b8fcb') 4 | export default function OtherInfo(props) { 5 | const { infos } = props 6 | return ( 7 |
      8 |
        9 | { 10 | infos.map(({ icon, value }, i) => { 11 | return
      • 12 | 13 | {value} 14 |
      • 15 | }) 16 | } 17 | 18 |
      19 |
      20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/react1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 |
      15 |
      16 | 17 |
      18 |
      19 | 20 | 21 | -------------------------------------------------------------------------------- /src/pages/react1/index.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/base.scss'; 2 | html, 3 | body, 4 | #resume { 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | $themeColor: #3b8fcb; 9 | #resume { 10 | padding: 1rem 2rem; 11 | } 12 | header { 13 | padding: 1rem 0; 14 | overflow: hidden; 15 | .base-info { 16 | width: 45%; 17 | float: left; 18 | h1 { 19 | color: $themeColor; 20 | font-size: 2.5rem; 21 | } 22 | h2 { 23 | color: #000; 24 | font-weight: lighter; 25 | font-size: 1rem; 26 | } 27 | } 28 | 29 | .other-info { 30 | width: 55%; 31 | float: right; 32 | ul { 33 | list-style: none; 34 | li { 35 | display: flex; 36 | align-items: center; 37 | } 38 | i { 39 | margin-right: 0.5rem; 40 | height: 1.5rem; 41 | } 42 | span { 43 | font-weight: normal; 44 | font-size: 0.8rem; 45 | } 46 | } 47 | } 48 | } 49 | 50 | .exps { 51 | overflow: hidden; 52 | margin-top: 0.5rem; 53 | ul { 54 | list-style: none; 55 | } 56 | .left { 57 | float: left; 58 | width: 70%; 59 | } 60 | .right { 61 | float: right; 62 | width: 25%; 63 | } 64 | .exp { 65 | margin-top: 0.5rem; 66 | } 67 | div.h3 { 68 | color: $themeColor; 69 | font-size: 1.5rem; 70 | line-height: 1.5rem; 71 | font-weight: normal; 72 | padding-left: 0.5rem; 73 | } 74 | div.h3 + div.hr { 75 | margin-top: 0.5rem; 76 | border-width: 0; 77 | border-bottom: 2px solid $themeColor; 78 | position: relative; 79 | overflow: initial; 80 | &::before { 81 | content: ''; 82 | position: absolute; 83 | transform: translateY(60%); 84 | left: 0; 85 | bottom: 0; 86 | display: block; 87 | width: 0.5rem; 88 | height: 0.5rem; 89 | border-radius: 50%; 90 | background-color: $themeColor; 91 | } 92 | } 93 | p.des { 94 | font-size: 0.8rem; 95 | font-weight: bold; 96 | margin-top: 0.5rem; 97 | span { 98 | min-width: 30%; 99 | margin-right: 1rem; 100 | display: inline-block; 101 | } 102 | } 103 | .introduce { 104 | padding-right: 0.5rem; 105 | margin-top: 0.5rem; 106 | font-size: 0.8rem; 107 | } 108 | } 109 | .icon { 110 | width: 1.5rem; 111 | height: 1.5rem; 112 | // vertical-align: -0.15rem; 113 | fill: currentColor; 114 | overflow: hidden; 115 | } 116 | -------------------------------------------------------------------------------- /src/pages/react1/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App.jsx' 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('app'), 10 | ) 11 | -------------------------------------------------------------------------------- /src/pages/vue1/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 40 | -------------------------------------------------------------------------------- /src/pages/vue1/components/Exps.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 67 | -------------------------------------------------------------------------------- /src/pages/vue1/components/Infos.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 39 | -------------------------------------------------------------------------------- /src/pages/vue1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 |
      15 |
      16 | 17 |
      18 |
      19 | 20 | 21 | -------------------------------------------------------------------------------- /src/pages/vue1/index.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/base.scss'; 2 | html, 3 | body, 4 | #resume { 5 | height: 100%; 6 | overflow: hidden; 7 | padding: 0.5rem; 8 | } 9 | 10 | $themeColor: #a79d91; 11 | 12 | header { 13 | text-align: center; 14 | h1 { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | font-size: 2rem; 19 | font-weight: bold; 20 | &::before, 21 | &::after { 22 | content: ''; 23 | display: block; 24 | height: 0.2rem; 25 | background-color: $themeColor; 26 | width: 20%; 27 | } 28 | 29 | &::before { 30 | margin-right: 4rem; 31 | } 32 | &::after { 33 | margin-left: 4rem; 34 | } 35 | } 36 | h2 { 37 | font-weight: lighter; 38 | font-size: 0.8rem; 39 | margin-top: 0.5rem; 40 | } 41 | .infos { 42 | display: flex; 43 | justify-content: space-around; 44 | list-style: none; 45 | margin-top: 1rem; 46 | li { 47 | display: flex; 48 | align-items: center; 49 | } 50 | i { 51 | margin-right: 0.5rem; 52 | height: 1.6rem; 53 | } 54 | .icon { 55 | width: 1rem; 56 | height: 1rem; 57 | // vertical-align: -0.15rem; 58 | fill: currentColor; 59 | overflow: hidden; 60 | background-color: $themeColor; 61 | border-radius: 50%; 62 | padding: 0.3rem; 63 | } 64 | span { 65 | color: $themeColor; 66 | font-size: 0.8rem; 67 | } 68 | } 69 | } 70 | 71 | div .exps { 72 | list-style: none; 73 | margin-top: 1rem; 74 | li { 75 | border-top: 0.3rem solid $themeColor; 76 | padding: 0.5rem; 77 | h3 { 78 | color: $themeColor; 79 | font-weight: bold; 80 | font-size: 1.5rem; 81 | padding: 0.5rem 0; 82 | } 83 | p { 84 | font-size: 0.8rem; 85 | } 86 | .des { 87 | margin-top: 0.5rem; 88 | display: flex; 89 | span { 90 | min-width: 20%; 91 | margin-right: 2rem; 92 | } 93 | .time { 94 | color: $themeColor; 95 | min-width: 15%; 96 | } 97 | } 98 | .introduce { 99 | margin-top: 0.5rem; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/pages/vue1/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import './index.scss' 4 | 5 | Vue.config.productionTip = process.env.NODE_ENV === 'development' 6 | 7 | new Vue({ 8 | render: (h) => h(App), 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import defaultSchema from '../constants/schema' 2 | import { toast } from '../components/Toast/index' 3 | 4 | export function createLink(text, href, newTab = false) { 5 | const a = document.createElement('a') 6 | a.href = href 7 | a.text = text 8 | a.target = newTab ? '_blank' : 'page' 9 | return a 10 | } 11 | 12 | export function createEmptySpan() { 13 | const span = document.createElement('span') 14 | return span 15 | } 16 | 17 | export function getSchema(key = '') { 18 | if (!key) { 19 | key = getPathnameKey(window.location.pathname) 20 | } 21 | let data = localStorage.getItem(key) 22 | if (!data) { 23 | setSchema(getDefaultSchema(key), key) 24 | return getSchema() 25 | } 26 | // 如果默认是空对象的则再取一次默认值 27 | if (data === '{}') { 28 | setSchema(getDefaultSchema(key), key) 29 | data = localStorage.getItem(key) 30 | } 31 | return JSON.parse(data) 32 | } 33 | 34 | export function getDefaultSchema(key) { 35 | const _key = key.slice(key.lastIndexOf('/') + 1) 36 | return defaultSchema[_key] || {} 37 | } 38 | 39 | export function setSchema(data, key = '') { 40 | if (!key) { 41 | key = getPathnameKey(window.location.pathname) 42 | } 43 | localStorage.setItem(key, JSON.stringify(data)) 44 | } 45 | 46 | /** 47 | * 简单防抖 48 | */ 49 | export function debounce(fn, delay) { 50 | let timer = null 51 | return function (...rest) { 52 | if (timer) { 53 | clearTimeout(timer) 54 | } 55 | fn = fn.bind(this, ...rest) 56 | timer = setTimeout(fn, delay) 57 | } 58 | } 59 | 60 | /** 61 | * 将结果写入的剪贴板 62 | * @param {String} text 63 | */ 64 | export function copyRes(text) { 65 | const input = document.createElement('input') 66 | document.body.appendChild(input) 67 | input.setAttribute('value', text) 68 | input.select() 69 | if (document.execCommand('copy')) { 70 | document.execCommand('copy') 71 | } 72 | document.body.removeChild(input) 73 | toast.success('结果已成功复制到剪贴板') 74 | } 75 | 76 | export function downloadTxtFile(str, filename) { 77 | const blob = new Blob([`\ufeff${str}`], { type: 'text/txt,charset=UTF-8' }) 78 | const href = URL.createObjectURL(blob) // 创建blob地址 79 | const a = document.createElement('a') 80 | a.href = href 81 | a.download = filename 82 | a.click() 83 | } 84 | 85 | export function getBase64Image(img) { 86 | const canvas = document.createElement('canvas') 87 | canvas.width = img.width 88 | canvas.height = img.height 89 | const ctx = canvas.getContext('2d') 90 | ctx.drawImage(img, 0, 0, img.width, img.height) 91 | const dataURL = canvas.toDataURL('image/png') 92 | return dataURL 93 | } 94 | 95 | /** 96 | * 遍历目标Dom树,找出文本内容与目标一致的dom组 97 | */ 98 | export function traverseDomTreeMatchStr(dom, str, res = []) { 99 | if (dom?.children?.length > 0) { 100 | for (const d of dom.children) { 101 | traverseDomTreeMatchStr(d, str, res) 102 | } 103 | } else if (dom?.textContent?.trim() === str) { 104 | res.push(dom) 105 | } 106 | 107 | return res 108 | } 109 | 110 | /** 111 | * 高亮指定dom一段时间 112 | */ 113 | export function highLightDom(dom, time = 500, color = '#fff566') { 114 | if (!dom?.style) return 115 | if (time === 0) { 116 | dom.style.backgroundColor = '' 117 | return 118 | } 119 | dom.style.backgroundColor = color 120 | setTimeout(() => { 121 | dom.style.backgroundColor = '' 122 | }, time) 123 | } 124 | 125 | /** 126 | * 获取路由对应的的Schema Key 127 | */ 128 | export function getPathnameKey(pathname) { 129 | return pathname.replace(/\/$/, '') 130 | } 131 | 132 | export function TransferAllImgToBase64(dom) { 133 | return new Promise((res) => { 134 | if (!dom) { 135 | res() 136 | return 137 | } 138 | const $imgs = dom.querySelectorAll('img') 139 | if ($imgs.length === 0) { 140 | res() 141 | return 142 | } 143 | // 图片转base64 144 | let i = 0 145 | for (const $img of $imgs) { 146 | if (!$img.src.startsWith('http')) { 147 | i += 1 148 | if (i === $imgs.length) { 149 | res() 150 | } 151 | } 152 | const image = new Image() 153 | image.src = `${$img.src}?v=${Math.random()}` // 处理缓存 154 | image.crossOrigin = '*' // 支持跨域图片 155 | image.onload = function () { 156 | i += 1 157 | $img.src = getBase64Image(image) 158 | if (i === $imgs.length) { 159 | res() 160 | } 161 | } 162 | image.onerror = function () { 163 | i += 1 164 | if (i === $imgs.length) { 165 | res() 166 | } 167 | } 168 | } 169 | }) 170 | } 171 | 172 | export function Dom2PDF(dom, filename) { 173 | TransferAllImgToBase64(dom).then(() => { 174 | window 175 | .html2canvas(dom, { 176 | dpi: 300, 177 | scale: 2, 178 | }) 179 | .then((canvas) => { 180 | // 返回图片dataURL,参数:图片格式和清晰度(0-1) 181 | const pageData = canvas.toDataURL('image/jpeg', 1.0) 182 | // 方向默认竖直,尺寸ponits,格式a4[595.28,841.89] 183 | const doc = new window.jspdf.jsPDF('', 'pt', 'a4') 184 | // addImage后两个参数控制添加图片的尺寸,此处将页面高度按照a4纸宽高比列进行压缩 185 | doc.addImage(pageData, 'JPEG', 0, 0, 595.28, (592.28 / canvas.width) * canvas.height) 186 | // doc.addImage(pageData, 'JPEG', 0, 0, 595.28, 841.89); 187 | doc.save(filename) 188 | }) 189 | }) 190 | } 191 | 192 | export function cloneValue(value) { 193 | if (value instanceof Object) { 194 | return JSON.parse(JSON.stringify(value)) 195 | } 196 | return value 197 | } 198 | 199 | export function getPageKey() { 200 | return getPathnameKey(document.getElementById('page').contentWindow.location.pathname) 201 | } 202 | 203 | export function isObject(a) { 204 | return a instanceof Object 205 | } 206 | 207 | export function isValueType(a) { 208 | return !isObject(a) 209 | } 210 | 211 | export function isSame(a, b) { 212 | // 为什么不用isNaN 213 | // 因为isNaN(undefined) 为true 214 | // eslint-disable-next-line no-self-compare 215 | return a === b || (a !== a && b !== b) 216 | } 217 | 218 | export function isSameType(a, b) { 219 | // 两者都是值类型 220 | if (typeof a === typeof b && !(a instanceof Object) && !(b instanceof Object)) { 221 | return true 222 | } 223 | 224 | // 两者都是对象 225 | if (a instanceof Object && b instanceof Object) { 226 | const aOk = a instanceof Array 227 | const bOk = b instanceof Array 228 | // 都是数组,或者都不是数组则ok --> aOK === bOk 229 | return aOk === bOk 230 | } 231 | return false 232 | } 233 | export function isEqual(a, b) { 234 | if (!isSameType(a, b)) { 235 | return false 236 | } 237 | if (isValueType(a)) { 238 | return a === b 239 | } 240 | // 都是数组 241 | if (Array.isArray(a)) { 242 | if (a.length !== b.length) { 243 | return false 244 | } 245 | 246 | // 逐项判断 247 | for (let i = 0; i < a.length; i += 1) { 248 | const _a = a[i] 249 | const _b = b[i] 250 | // 类型不等 251 | if (!isSameType(_a, _b)) { 252 | return false 253 | } 254 | 255 | // 值类型,值不等 256 | if (isValueType(_a) && !isSame(_a, _b)) { 257 | return false 258 | } 259 | 260 | // 对象 - 递归判断了 261 | if (isObject(_a) && !isEqual(_a, _b)) { 262 | return false 263 | } 264 | } 265 | } else { 266 | // 都是普通对象 267 | const aKeys = Reflect.ownKeys(a) 268 | const bKeys = Reflect.ownKeys(b) 269 | 270 | // 键数量不一致 271 | if (aKeys.length !== bKeys.length) { 272 | return false 273 | } 274 | 275 | for (const aKey of aKeys) { 276 | const _a = a[aKey] 277 | const _b = b[aKey] 278 | // 类型不等 279 | if (!isSameType(_a, _b)) { 280 | return false 281 | } 282 | 283 | // 值类型,值不等 284 | if (isValueType(_a) && !isSame(_a, _b)) { 285 | return false 286 | } 287 | 288 | // 对象 - 递归判断了 289 | if (isObject(_a) && !isEqual(_a, _b)) { 290 | return false 291 | } 292 | } 293 | } 294 | 295 | return true 296 | } 297 | --------------------------------------------------------------------------------