Я создал онлайн-приложение для создания резюме

внешний интерфейс

图片

предисловие

Месяц назад я написал статьюКак написать хорошее резюме для набора в онлайн-школу, цель состоит в том, чтобы помочь учащимся, которые собираются начать подавать заявки на набор в школу, улучшить свои резюме.

Флаг также установлен в статье

图片

Посмотреть на Githubзафиксировать запись, потребовалось около недели, чтобы придумать план, задуманный в моем сердце. Он может быть не идеальным, но я думаю, что он должен помочь некоторым ученикам.

Конечно, хорошие вещи отображаются три раза, O(∩_∩)O~~

Если вас не устраивает стиль шаблона (цвет, верстка), разберитесьПередняя магиястудентов могут клонироватьсклад, бросьте свое волшебное украшение

Студенты, заинтересованные в проекте, также приветствуютсяспособствоватьВзгляните на свой любимый шаблон резюме (код). Теоретически нет предела стеку технологий разработки. Конечно, вопросы или предложения также приветствуются.

В этой статье в основном рассказывается о дизайнерских идеях, технических решениях и некоторых проблемах, с которыми столкнулись, и решениях этого проекта (с использованием большого количества хакерских навыков), а также последующем планировании.

Дизайн проекта

макет

图片

Базовая структура страницы всего приложения

<body>
    <header>
        <!-- 导航 -->
        <nav></nav>
    </header>
    <div>
        <!-- 展示简历 -->
        <iframe></iframe>
        <!-- 控制区域 -->
        <div></div>
    </div>
</body>

Некоторые друзья могут задаться вопросом, почему здесь используется iframe?

Здесь я дам вам краткое введение, и я объясню его вам позже, когда я буду говорить о техническом решении.

Раздел резюме в моем виденииПоказывать только логику, которую можно рассматривать как независимую статическую страницу

Поскольку это только для отображения, независимо от того, какая внешняя магия может выполнять эту работу, поэтому, чтобы облегчить использование заклинаний различными магами, эта часть отделена.Составителям шаблона резюме нужно только заботиться о том, как восстановить статическая страница. Остальная логика взаимодействия передается на родительскую страницу для унифицированной обработки

Технический отбор

图片

Vanilla JS — самый легкий в мире JavaScript-фреймворк (никто) ----родной js

Основная часть всего приложения реализована на нативном js

Часть отображения CV дистальная теоретически любая методика может быть использована для достижения стека, связанного с низкой родительской страницей

коммуникация

图片

  • Переключайтесь между различными шаблонами резюме с помощью панели навигации.
  • Изменения в резюме автоматически синхронизируются с описанием страницы в области управления
  • Измените информацию описания страницы в области управления и обновите содержимое резюме в режиме реального времени.

описать резюме

图片

  • Используйте json для описания структуры и содержания резюме
  • Шаблон соответствует json

Отображение информации описания страницы

图片

  • Используйте JSON для описания различной информации в резюме
  • Предоставляет редактор JSON
  • Здесь редактор json используетjsoneditor

доступ к данным

图片

  • Весь поток данных односторонний, внешний отвечает за обновление, а внутренний (часть отображения возобновления) отвечает только за чтение
  • Данные хранятся локально, поэтому можно не опасаться утечки личной информации.
  • используется здесьlocalStorage

эффект первого издания

图片

图片

Ниже описаны ключевые части реализации проекта.

выполнить

Структура каталогов проекта

./config                         webpack配置文件
├── webpack.base.js             -- 公共配置
├── webpack.config.build.js     -- 生产环境特有配置
├── webpack.config.dev.js       -- 开发环境特有配置
├── webpack.config.js           -- 引用的配置文件
│
./public            公共静态资源
├── css   
│   └── print.css  打印时用的样式
│
./src       核心代码
├── assets          静态资源css/img
├── constants       常量
│   ├── index.js    存放导航的名称映射信息
│   ├── schema      存放每个简历模板的默认JSON数据,与pages中的模板一一对应
│   └────── demo1.js   
├── pages           简历模板目录
│   └── demo1       -- 其中的一个模板
│
├── utils           工具方法
├── app.js          项目的入口js
├── index.html      项目的入口页面

Соглашение о конфигурации

Согласно согласованной структуре каталогов, с помощью автоматизированных скриптов

Все шаблоны унифицированы в директории src/pages/xxx

Соглашение о шаблоне страницыindex.html, все файлы js в этом каталоге будут автоматически добавлены в запись веб-пакета и автоматически внедрены в текущий шаблон страницы.

Например

./src
├── pages          
│   └── xxx
│   └───── index.html
│   └───── index.scss
│   └───── index.js

Автоматически генерировать конфигурацию записи/страницы здесьКод можно перенестикздесьПроверить

Автоматически сгенерированные результаты следующие

图片

Формат содержимого каждого плагина HTMLWebpackPlugin выглядит следующим образом.

图片

Автоматически генерировать панель навигации

В верхней части главной страницы есть панель навигации для переключения маршрута шаблона резюме.

图片

Содержание ссылки в этой части очень скучно, если вы заполняете его вручную.Как добиться автоматической генерации?

Во-первых, содержимое навигационной части заголовка шаблона домашней страницы

<header>
    <nav id="nav">
        <%= htmlWebpackPlugin.options.pageNames %>
    </nav>
</header>

htmlWebpackPlugin.optionsвыражатьHTMLWebpackPluginобъектыuserOptionsАтрибуты

Мы получили заголовки всех страниц выше и используем все заголовки,соединения сшиваются вместе, а затем связываютсяuserOptions.pageNames, первоначальный результат рендеринга страницы становится

<header>
    <nav id="nav">
        abc,demo1,vue1,react1,introduce
    </nav>
</header>

Имея первоначальные результаты рендеринга, давайте напишем метод для преобразования этого содержимого вaПросто пометить

const navTitle = {
    'demo1': '模板1',
    'react1': '模板2',
    'vue1': '模板3',
    'introduce': '使用文档',
    'abc': '开发示例'
}

function createLink(text, href, newTab = false) {
    const a = document.createElement('a')
    a.href = href
    a.text = text
    a.target = newTab ? '_blank' : 'page'
    return a
}

/**
 * 初始化导航栏
 */
function initNav(defaultPage = 'react1') {
    const $nav = document.querySelector('header nav')
    // 获取所有模板的链接---处理原始内容
    const links = $nav.innerText.split(',').map(pageName => {
        const link = createLink(navTitle[pageName] || pageName, `./pages/${pageName}`)
        // iframe中打开
        return link
    })

    // 加入自定义的链接
    links.push(createLink('Github', 'https://github.com/ATQQ/resume', true))
    links.push(createLink('贡献模板', 'https://github.com/ATQQ/resume/blob/main/README.md', true))
    links.push(createLink('如何书写一份好的互联网校招简历', 'https://juejin.cn/post/6928390537946857479', true))
    links.push(createLink('建议/反馈', 'https://www.wenjuan.com/s/MBryA3gI/', true))

    // 渲染到页面中
    const t = document.createDocumentFragment()
    links.forEach(link => {
        t.appendChild(link)
    })
    $nav.innerHTML = ''
    $nav.append(t)
}

initNav()

Таким образом, панель навигации создается «автоматически».

Автоматически экспортировать описания страниц

содержание

./src
├── constants      
│   ├── index.js
│   ├── schema.js
│   ├── schema    
│   ├────── demo1.js  
│   ├────── react1.js  
│   └────── vue1.js

Данные по умолчанию для каждой страницы считываются из ./src/constants/schema.js.

import abc from './schema/abc'
import demo1 from './schema/demo1'
import react1 from './schema/react1'
import vue1 from './schema/vue1'

export default{
    abc,demo1,react1,vue1
}

Содержимое описания каждого шаблона распространяется в каталоге schema.Если каждый разработчик вручную добавляет свой собственный шаблон в schema.js, легко вызвать конфликты, поэтому просто создайте его автоматически.

Инструментальный метод перемещается вздесьПроверить

/**
 * 自动创建src/constants/schema.js 文件
 */
function writeSchemaJS() {
    const files = getDirFilesWithFullPath('src/constants/schema')
    const { dir } = path.parse(files[0])
    const targetFilePath = path.resolve(dir, '../', 'schema.js')
    const names = files.map(file => path.parse(file).name)
    const res = `${names.map(n => {
        return `import ${n} from './schema/${n}'`
    }).join('\n')}

export default{
    ${names.join(',')}
}`
    fs.writeFileSync(targetFilePath, res)
}

доступ к данным

Операция доступа к данным используется как на родительской, так и на дочерней странице, а извлечение является общедоступным методом.

Данные хранятся в localStorage, а маршрут каждого шаблона резюме используется в качествеkey

./src/utils/index.js

import defaultSchema from '../constants/schema'

export function getSchema(key = '') {
    if (!key) {
        // 默认key为路由 如 origin.com/pages/react1
        // key就为 pages/react1
        key = window.location.pathname.replace(/\/$/, '')
    }
    // 先从本地取
    let data = localStorage.getItem(key)
    // 如果没有就设置一个默认的再取
    if (!data) {
        setSchema(getDefaultSchema(key), key)
        return getSchema()
    }
    // 如果默认是空对象的则再取一次默认值
    if (data === '{}') {
        setSchema(getDefaultSchema(key), key)
        data = localStorage.getItem(key)
    }
    return JSON.parse(data)
}

export function getDefaultSchema(key) {
    const _key = key.slice(key.lastIndexOf('/') + 1)
    return defaultSchema[_key] || {}
}

export function setSchema(data, key = '') {
    if (!key) {
        key = window.location.pathname.replace(/\/$/, '')
    }
    localStorage.setItem(key, JSON.stringify(data))
}

Json показать описание

Информация описания json должна отображаться в области управления, а часть отображения используетjsoneditor

Конечно, jsoneditor также поддерживает различные операции с данными (CRUD), а также предоставляет кнопки быстрого доступа.

Здесь мы используем cdn для представления jsoneditor.

<link rel="stylesheet" href="https://img.cdn.sugarat.top/css/jsoneditor.min.css">
<script src="https://img.cdn.sugarat.top/js/jsoneditor.min.js"></script>

инициализация

/**
 * 初始化JSON编辑器
 * @param {string} id 
 */
function initEditor(id) {
    let timer = null
    // 这里做了一个简单的防抖
    const editor = new JSONEditor(document.getElementById(id), {
        // json内容改动时触发
        onChangeJSON(data) {
            if (timer) {
                clearTimeout(timer)
            }
            // updatePage方法用于通知子页面更新
            setTimeout(updatePage, 200, data)
        }
    })
    return editor
}

const editor = initEditor('jsonEditor')

Отображение результатов

图片

время отображения/обновления данных json

  • Поскольку каждый маршрут переключения вызывает событие ONLOAD iFrame.
  • Так что потратьте время, чтобы редактор обновил содержимое json здесь.
function getPageKey() {
    return document.getElementById('page').contentWindow.location.pathname.replace(/\/$/, '')
}

document.getElementById('page').onload = function (e) {
    // 更新editor中显示的内容
    editor.set(getSchema(getPageKey()))
}

Написание шаблонных страниц

Вот 4 способа получить одну и ту же страницу

желаемый эффект

图片

файл описания

Создайте файл описания json для страницы в каталоге схемы, например abc.js.

./src
├── constants
│   └── schema
│   └────── abc.js  

abc.js

export default {
    name: '王五',
    position: '求职目标: Web前端工程师',
    infos: [
        '1:很多文字',
        '2:很多文字',
        '3:很多文字',
    ]
}

Желаемая структура рендеринга

<div id="resume">
    <div id="app">
        <header>
            <h1>王五</h1>
            <h2>求职目标: Web前端工程师</h2>
        </header>
        <ul class="infos">
            <li>1:很多文字<li>
            <li>2:很多文字<li>
            <li>3:很多文字<li>
        </ul>
    </div>
</div>

Начните писать код ниже

с родительской страницейЕдинственная адекватная логикаНеобходимо смонтировать метод обновления в окне дочерней страницы, чтобы родительская страница активно вызывала обновление.

родной js

import { getSchema } from "../../utils"

window.refresh = function () {
    const schema = getSchema()
    const { name, position, infos } = schema
    // ... render逻辑
}

vue

<script>
import { getSchema } from '../../utils';
export default {
  data() {
    return {
      schema: getSchema(),
    };
  },
  mounted() {
    window.refresh = this.refresh;
  },
  methods: {
    refresh() {
      this.schema = getSchema();
    },
  },
};
</script>

react

import React, { useEffect, useState } from 'react'
import { getSchema } from '../../utils'

export default function App() {
    const [schema, updateSchema] = useState(getSchema())
    const { name, position, infos = [] } = schema
    useEffect(() => {
        window.refresh = function () {
            updateSchema(getSchema())
        }
    }, [])
    return (
        <div>
            { /* 渲染dom的逻辑 */ }
        </div>
    )
}

Код свернут для удобства чтения

Первое это стиль, тут выбираем язык препроцессинга sass, конечно можно и нативный css использовать

index.scss
@import './../../assets/css/base.scss';
html,
body,
#resume {
  height: 100%;
  overflow: hidden;
}
// 上面部分是推荐引入的通用样式

// 下面书写我们的样式
$themeColor: red;

#app {
  padding: 1rem;
}

header {
  h1 {
    color: $themeColor;
  }
  h2 {
    font-weight: lighter;
  }
}

.infos {
  list-style: none;
  li {
    color: $themeColor;
  }
}

затем файл описания страницы

index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>
        <%= htmlWebpackPlugin.options.title %>
    </title>
</head>

<body>
    <div id="resume">
        <div id="app">

        </div>
    </div>
</body>

</html>

Начнем использовать различные технологические стеки для написания логического кода

родной js

Структура каталогов

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js

index.js

import { getSchema } from "../../utils"
import './index.scss'

window.refresh = function () {
    const schema = getSchema()
    const { name, position, infos } = schema

    clearPage()
    renderHeader(name, position)
    renderInfos(infos)
}

function clearPage() {
    document.getElementById('app').innerHTML = ''
}

function renderHeader(name, position) {
    const html = `
    <header>
        <h1>${name}</h1>
        <h2>${position}</h2>
    </header>`
    document.getElementById('app').innerHTML += html
}

function renderInfos(infos = []) {
    if (infos?.length === 0) {
        return
    }
    const html = `
    <ul class="infos">
    ${infos.map(info => {
        return `<li>${info}</li>`
    }).join('')}
    </ul>`
    document.getElementById('app').innerHTML += html
}

window.onload = function () {
    refresh()
}
Vue

Структура каталогов

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js
│   └───── App.vue

index.js

import Vue from 'vue'
import App from './App.vue'
import './index.scss'

Vue.config.productionTip = process.env.NODE_ENV === 'development'

new Vue({
    render: h => h(App)
}).$mount('#app')

App.vue

<template>
  <div id="app">
    <header>
      <h1>{{ schema.name }}</h1>
      <h2>{{ schema.position }}</h2>
    </header>
    <div class="infos">
      <p
        v-for="(info,
        i) in schema.infos"
        :key="i"
      >
        {{ info }}
      </p>
    </div>
  </div>
</template>

<script>
import { getSchema } from '../../utils';
export default {
  data() {
    return {
      schema: getSchema(),
    };
  },
  mounted() {
    window.refresh = this.refresh;
  },
  methods: {
    refresh() {
      this.schema = getSchema();
    },
  },
};
</script>
React

Структура каталогов

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js
│   └───── App.jsx

index.js

import React from 'react'
import ReactDOM from 'react-dom';
import App from './App.jsx'
import './index.scss'

ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
    document.getElementById('app')
)

App.jsx

import React, { useEffect, useState } from 'react'
import { getSchema } from '../../utils'

export default function App() {
    const [schema, updateSchema] = useState(getSchema())
    const { name, position, infos = [] } = schema
    useEffect(() => {
        window.refresh = function () {
            updateSchema(getSchema())
        }
    }, [])
    return (
        <div>
            <header>
                <h1>{name}</h1>
                <h2>{position}</h2>
            </header>
            <div className="infos">
                {
                    infos.map((info, i) => {
                        return <p key={i}>{info}</p>
                    })
                }
            </div>
        </div>
    )
}
jQuery

Структура каталогов

./src
├── pages          
│   └── abc
│   └───── index.html
│   └───── index.scss
│   └───── index.js

index.js

import { getSchema } from "../../utils"
import './index.scss'

window.refresh = function () {
    const schema = getSchema()
    const { name, position, infos } = schema

    clearPage()
    renderHeader(name, position)
    renderInfos(infos)
}

function clearPage() {
    $('#app').empty()
}

function renderHeader(name, position) {
    const html = `
    <header>
        <h1>${name}</h1>
        <h2>${position}</h2>
    </header>`
    $('#app').append(html)
}

function renderInfos(infos = []) {
    if (infos?.length === 0) {
        return
    }
    const html = `
    <ul class="infos">
    ${infos.map(info => {
        return `<li>${info}</li>`
    }).join('')}
    </ul>`
    $('#app').append(html)
}

window.onload = function () {
    refresh()
}

Если вы чувствуете, что отображение abc в панели навигации не является дружественным, вы, конечно, можете изменить его.

./src
├── constants    
│   ├── index.js    存放路径与中文title的映射

./src/constants/index.jsдобавить псевдоним к

export const navTitle = {
    'abc': '开发示例'
}

图片

Обновление подстраницы

Существует предыдущее при создании экземпляра редактораupdatePageметод

Если у подстраницы есть метод обновления, вызовите его напрямую, чтобы обновить страницу.Конечно, родительская страница будет хранить последние данные в localStorage перед обновлением.

Таким образом, нет прямого обмена данными между страницами.Одна отвечает за запись, а другая отвечает за чтение.Даже если запись не удалась, это не повлияет на подстраницу для чтения исходных данных.

function refreshIframePage(isReload = false) {
    const page = document.getElementById('page')
    if (isReload) {
        page.contentWindow.location.reload()
        return
    }
    if (page.contentWindow.refresh) {
        page.contentWindow.refresh()
        return
    }
    page.contentWindow.location.reload()
}

function updatePage(data) {
    setSchema(data, getPageKey())
    refreshIframePage()
}

/**
 * 初始化JSON编辑器
 * @param {string} id 
 */
function initEditor(id) {
    let timer = null
    // 这里做了一个简单的防抖
    const editor = new JSONEditor(document.getElementById(id), {
        // json内容改动时触发
        onChangeJSON(data) {
            if (timer) {
                clearTimeout(timer)
            }
            // updatePage方法用于通知子页面更新
            setTimeout(updatePage, 200, data)
        }
    })
    return editor
}

const editor = initEditor('jsonEditor')

экспортировать pdf

Сторона ПК

Во-первых, браузер ПК поддерживает печать и экспорт pdf.

Как запустить печать?

  • Правая кнопка мыши и выберите печать
  • Ярлык Ctrl + P
  • window.print()

Мы используем третью схему в коде здесь

Как я могу убедиться, что печатается только раздел резюме?

Это использование медиа-запросов

метод первый

@media print {
    /* 此部分书写的样式还在打印时生效 */
}

Способ 2

<!-- 引入的css资源只在打印时生效 -->
<link rel="stylesheet" href="./css/print.css" media="print">

Просто скройте ненужное содержимое в стиле печати.

图片

В основном можно сделать восстановление 1:1

мобильный

использоватьjsPDF + html2canvas

  1. html2canvas отвечает за преобразование страниц в изображения.
  2. jsPDF отвечает за преобразование изображений в PDF.
function getBase64Image(img) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, img.width, img.height);
    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
}
// 导出pdf
// 当然这里确保图片资源被转为了base64,否则导出的简历无法展示图片
html2canvas(document.getElementById('page').contentDocument.body).then(canvas => {
    //返回图片dataURL,参数:图片格式和清晰度(0-1)
    var pageData = canvas.toDataURL('image/jpeg', 1.0);
    //方向默认竖直,尺寸ponits,格式a4[595.28,841.89]
    var doc = new jsPDF('', 'pt', 'a4');
    //addImage后两个参数控制添加图片的尺寸,此处将页面高度按照a4纸宽高比列进行压缩
    // doc.addImage(pageData, 'JPEG', 0, 0, 595.28, 592.28 / canvas.width * canvas.height);
    doc.addImage(pageData, 'JPEG', 0, 0, 595.28, 841.89);
    doc.save(`${Date.now()}.pdf`);
});

Однако в этом методе экспорта все еще есть некоторые проблемы, которые еще не решены, и в будущем будут использоваться другие решения.

  1. Гиперссылки не поддерживаются
  2. шрифт значков не поддерживается
  3. Пробел шрифта будет удален

резюме

На этом прототип всего проекта готов.

  • Панель навигации Переключить шаблон резюме
  • Изменение в редакторе JSONjson-> обновление данных страницы
  • экспортировать pdf
    • Мобильный — jspdf
    • Компьютер - Печать

высокоэнергетическая работа

Выделить измененный контент

Требование: В редакторе json обновлен контент, и ожидается, что измененный контент можно будет выделить в резюме.

Обращаясь к ТЗ, стоит ожидать мониторить измененный дом, а потом подсвечивать

Используйте это местоMutationObserverохватывать

Предоставляет возможность отслеживать изменения, внесенные в дерево DOM.

/**
 * 高亮变化的Dom
 */
function initObserver() {
    // 包含子孙节点
    // 将监视范围扩展至目标节点整个节点树中的所有节点
    // 监视指定目标节点或子节点树中节点所包含的字符数据的变化
    const config = { childList: true, subtree: true, characterData: true };

    // 实例化监听器对象
    const observer = new MutationObserver(debounce(function (mutationsList, observer) {
        for (const e of mutationsList) {
            let target = e.target
            if (e.type === 'characterData') {
                target = e.target.parentElement
            }
            // 高亮
            highLightDom(target)
        }
    }, 100))
    // 监听子页面的body
    observer.observe(document.getElementById('page').contentDocument.body, config);
    // 因为 MutationObserver 是微任务,微任务后面紧接着就是页面渲染
    
    // 停止观察变动
    // 这里使用宏任务,确保此轮Event loop结束
    setTimeout(() => {
        observer.disconnect()
    }, 0)
}

function highLightDom(dom, time = 500, color = '#fff566') {
    if (!dom?.style) return
    if (time === 0) {
        dom.style.backgroundColor = ''
        return
    }
    dom.style.backgroundColor = '#fff566'
    setTimeout(() => {
        dom.style.backgroundColor = ''
    }, time)
}

Когда вызывать initObserver

Разумеется, событие регистрируется до обновления страницы, и мониторинг прекращается после того, как страница завершает отрисовку изменений.

function updatePage(data) {
    // 异步的微任务,本轮event loop结束停止观察
    initObserver()
    // 同步
    setSchema(data, getPageKey())
    // 同步 + 渲染页面
    refreshIframePage()
}

Эффект

图片

нажмите где изменить

желаемый эффект 图片

Обращаться:

  • Нажмите на часть, которую нужно изменить, вы можете изменить ее
  • Результат модификации синхронизируется с содержимым в редакторе json на резюме

Реализация объясняется ниже

1. Получить кликабельный Дом

document.getElementById('page').contentDocument.body.addEventListener('click', function (e) {
    const $target = e.target
})

2. Получите количество и относительное положение содержимого dom на странице.

  1. Подстраница содержит только логику отображения, поэтому родительская страница должна выполнить операцию взлома, чтобы найти соответствующую позицию содержимого клика в json.
  2. Дом с таким же контентом - это более одного, поэтому вам нужно все это найти.
/**
 * 遍历目标Dom树,找出文本内容与目标一致的dom组
 */
function traverseDomTreeMatchStr(dom, str, res = []) {
    // 如果有子节点则继续遍历子节点
    if (dom?.children?.length > 0) {
        for (const d of dom.children) {
            traverseDomTreeMatchStr(d, str, res)
        }
        // 相等则记录下来
    } else if (dom?.textContent?.trim() === str) {
        res.push(dom)
    }

    return res
}

// 监听简历页的点击事件
document.getElementById('page').contentDocument.body.addEventListener('click', function (e) {
    const $target = e.target
    // 点击的内容
    const clickText = $target.textContent.trim()
    // 只包含点击内容的节点
    const matchDoms = traverseDomTreeMatchStr(document.getElementById('page').contentDocument.body, clickText)
    // 点击的节点在 匹配的 节点中的相对位置
    const mathIndex = matchDoms.findIndex(v => v === $target)
    // 不包含则不做处理
    if (mathIndex < 0) {
        return
    }
})

3. Получите соответствующий узел в jsoneditor

  • Аналогично логике выше
  • Сначала отфильтруйте несколько узлов, которые содержат только содержимое этого узла.
  • Затем сопоставьте в соответствии с относительным положением щелкнутого дома в том же списке узлов контента.
// 监听简历页的点击事件
document.getElementById('page').contentDocument.body.addEventListener('click', function (e) {
    // ...省略上述列出的代码

    // 解除上次点击的dom高亮
    highLightDom($textarea.clickDom, 0)
    // 高亮这次的10s
    highLightDom($target, 10000)


    // 更新jsoneditor中的search内容
    editor.searchBox.dom.search.value = clickText
    // 主动触发搜索
    editor.searchBox.dom.search.dispatchEvent(new Event('change'))

    // 将点击内容显示在textarea中
    $textarea.value = clickText
    
    // 自动聚焦输入框
    if (document.getElementById('focus').checked) {
        $textarea.focus()
    }

    // 记录点击的dom,挂载$textarea上
    $textarea.clickDom = e.target

    // jsoneditor 搜索过滤的内容为模糊匹配,比如搜索 a 会匹配 ba,baba,a,aa,aaa
    // 根据上面得到的matchIndex,进行精确匹配全等的json节点
    let i = -1
    for (const r of editor.searchBox.results) {
        // 全等得时候下标才变动
        if (r.node.value === clickText) {
            i++
            // 匹配到json中的节点
            if (i === mathIndex) {
                // 高亮一下$textarea
                $textarea.style.boxShadow = '0 0 1rem yellow'
                setTimeout(() => {
                    $textarea.style.boxShadow = ''
                }, 200)
                return
            }
        }
        // 手动触发jsoneditor的next search match  按钮, 切换jsoneditor中active的节点
        editor.searchBox.dom.input.querySelector('.jsoneditor-next').dispatchEvent(new Event('click'))
        // active的节点可以通过下面方式获取
        // editor.searchBox.activeResult.node
    }
})

4. Обновить содержимое узла

  1. Вышеуказанные два шага получают как дом в резюме, так и дом jsoneditor.
  2. Контент, введенный через textarea
  3. Обновите входное содержимое до двух домов соответственно и запишите последний json в localStorage.
// 监听输入事件,并做一个简单的防抖
 $textarea.addEventListener('input', debounce(function () {
    if (!editor.searchBox?.activeResult?.node) {
        return
    }
    // 激活dom变动事件
    initObserver()

    // 更新点击dom
    $textarea.clickDom.textContent = this.value

    // 更新editor的dom
    editor.searchBox.activeResult.node.value = this.value
    editor.refresh()

    // 更新到本地
    setSchema(editor.get(), getPageKey())

}, 100))

На этом обновление данных с обеих сторон завершено (resume/jsoneditor).

Последующее планирование

  1. Доступ к дополнительной поддержке фреймворка
  2. Оптимизировать экспорт pdf
    1. Гиперссылка
    2. значок шрифта
  3. Оптимизируйте пользовательский опыт
    1. Уменьшите присутствие jsoneditor.Текущие операции добавления и удаления основаны на jsoneditor, что не очень удобно для студентов, не понимающих магию внешнего интерфейса.
    2. Оптимизация взаимодействия на мобильных устройствах
    3. Украсить интерфейс
  4. Добавить директиву автоматически сгенерированного шаблона кода
  5. Перемещение большего количества шаблонов резюме

Спасибо, настаиваю на том, чтобы прочитать это, спасибо, что присоединились, если вы заинтересованы, добро пожаловать в свой свой вклад (CV / Функция)

Ссылки по теме

Прием на работу

Мейтуан21 весенний набор в школуОткрыто, всем радыДоставка

Автор находится вГруппа компаний "Прибытие" - Департамент технологий платформ, добро пожаловать присоединиться