32 |
33 |
34 |
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 = '