5分钟剖析简历模板生成原理

有一段时间没有写文章了,前段时间忙着处理自己的事情就耽搁了。

在上星期的时候,看到某个大佬的一个组件拖拽自定义配置生成简历的项目,感觉很酷,想着自己实现一下,这样以后可以自己定制简历模板并且可随时导出pdf,主要是免费的,不用去花钱去买一些简历模板,十分方便。

这是在线体验链接,想看效果在直接在左边的tab选择模板即可。

这是一个基础版本,没有很多拖拽交互,目前应该有bug,后续有时间也会优化交互,增加各种功能。欢迎各位大佬提出问题,一起沟通交流。感兴趣的可以通过右上角的github图标star支持一下。

实现功能点

  • 拖拽
  • 标线
  • 属性配置
  • 生成pdf

拖拽

其中拖拽的原理不赘述,可以参考这位大佬的文章,很全面,条理清晰。

标线

标线的功能就是当拖拽组件与画布的组件边界相近的时候会出现的线。

总共有六条对齐的线

  1. 垂直方向的左中右三条线,左边界 中间,右边界对齐
  2. 水平方向的上中下三条线,上边界 中间,下边界对齐

通过html先绘制6条线

js 复制代码
// js
const lines = ['xt', 'xc', 'xb', 'yl', 'yc', 'yr']
const lineStatus = {
  xt: {
    status: false,
    top: 0
  },
  xc: {
    status: false,
    top: 0
  },
  xb: {
    status: false,
    top: 0
  },
  yl: {
    status: false,
    left: 0
  },
  yc: {
    status: false,
    left: 0
  },
  yr: {
    status: false,
    left: 0
  },
}

// html
<div 
  class="line"
  v-for="line in lines"
  :key="line"
  v-show="lineStatus[line].status"
  :class="line.includes('x') ? 'x-line' : 'y-line'"
  :style="line.includes('x') ? `top: ${lineStatus[line].top}px` : `left:${lineStatus[line].left}px`"
>
</div>

// style
.x-line {
    width: 100%;
    height: 1px;
  }
 .y-line {
    height: 100%;
    width: 1px;
  }

线有了,现在只需要在拖拽的时候将拖拽组件的坐标和宽高与画布中的组件进行对比。示例下图:当拖拽组件的右边与文本组件的左边对齐时,条件如下

js 复制代码
// 当拖拽组件的右边与文本组件的左边对齐时
(dragCom.left + dragCom.width) === textCom.left

以此类推,我们可以很快得出六条线的出现条件,其中dragShift代表组件需要改变的值,lineShift代表标线需要改变的值,其中的diff是为了实现 吸附 ,当边界对齐的差值小于等于diff的时候,就直接对齐(即直接改变拖拽组件的位置)如图

主要功能代码如下

js 复制代码
const diff = 3 // 吸附的边界值
const showLine = () => {
    const conditions = {
      top: [
        {
          isNearly: isNearly(dragComStyle.top, curComStyle.top), // 拖拽组件上边界与画布组件上边界对齐
          line: 'xt',
          dragShift: curComStyle.top,
          lineShift: curComStyle.top
        },
        {
          isNearly: isNearly(dragComStyle.bottom, curComStyle.top), // 拖拽组件下边界与画布组件上边界对齐
          line: 'xt',
          dragShift: curComStyle.top - dragComStyle.height,
          lineShift: curComStyle.top
        },
        {
          isNearly: isNearly(dragComStyle.top + dragComStyle.halfHeight, curComStyle.top + curComStyle.halfHeight), // 拖拽组件与画布组件水平中间对齐
          line: 'xc',
          dragShift: curComStyle.top + curComStyle.halfHeight - dragComStyle.halfHeight,
          lineShift: curComStyle.top + curComStyle.halfHeight
        },
        {
          isNearly: isNearly(dragComStyle.top, curComStyle.bottom), // 拖拽组件上边界与画布组件下边界对齐
          line: 'xb',
          dragShift: curComStyle.bottom,
          lineShift: curComStyle.bottom
        },
        {
          isNearly: isNearly(dragComStyle.bottom, curComStyle.bottom), // 拖拽组件下边界与画布组件下边界对齐
          line: 'xb',
          dragShift: curComStyle.bottom - dragComStyle.height,
          lineShift: curComStyle.bottom
        },
      ],
      left: [
        {
          isNearly: isNearly(dragComStyle.left, curComStyle.left), // 拖拽组件左边界与画布组件左边界对齐
          line: 'yl',
          dragShift: curComStyle.left,
          lineShift: curComStyle.left
        },
        {
          isNearly: isNearly(dragComStyle.right, curComStyle.left), // 拖拽组件右边界与画布组件左边界对齐
          line: 'yl',
          dragShift: curComStyle.left - dragComStyle.width,
          lineShift: curComStyle.left
        },
        {
          isNearly: isNearly(dragComStyle.left + dragComStyle.halfWidth, curComStyle.left + curComStyle.halfWidth), // 拖拽组件与画布组件垂直中间对齐
          line: 'yc',
          dragShift: curComStyle.left + curComStyle.halfWidth - dragComStyle.halfWidth,
          lineShift: curComStyle.left + curComStyle.halfWidth
        },
        {
          isNearly: isNearly(dragComStyle.left, curComStyle.right), // 拖拽组件左边界与画布组件右边界对齐
          line: 'yr',
          dragShift: curComStyle.right,
          lineShift: curComStyle.right
        },
        {
          isNearly: isNearly(dragComStyle.right, curComStyle.right), // 拖拽组件右边界与画布组件右边界对齐
          line: 'yr',
          dragShift: curComStyle.right - dragComStyle.width,
          lineShift: curComStyle.right
        },
      ]
    }
    Object.keys(conditions).forEach(dire => {
      conditions[dire].forEach((condition) => {
        if (!condition.isNearly) return
        lineStatus[condition.line][dire]。status = true // 标线展示
        lineStatus[condition.line][dire] = condition.lineShift // 改变标线的位置
        dragCom.style[dire] = condition.dragShift // 改变拖拽组件的位置
      })
    })
  });
}

// 判断两个拖拽组件和画布组件的目标值边界是否对齐
const isNearly = (dragValue, targetValue) => {
  return Math.abs(dragValue - targetValue) <= diff
}
优化

这样子就大致完成了标线的功能啦。我们来实操一下,如图,当两个组件高度一致,右对齐的时候会出现四条线,非常的不美观,需要进行优化一下

当拖拽组件向拖拽的时候,最先对齐的是文本组件的左边,然后中间对齐,再然后就是右边对齐

这里的重点就是得出拖拽的方向,因为在dragstart事件中我们有记录起始坐标,我们只需要将拖拽的坐标起始坐标对比既可以得出组件拖拽的方向。

js 复制代码
// 移动x坐标大于起始的x坐标即向右移动
const isRight = moveX-startX > 0

结合上面的已知条件可以类推得出结论

js 复制代码
if (isRight) {
      if (needShowLines.includes('yl')) {
        lineStatus.value['yl'].status = true
      } else if (needShowLines.includes('yc')) {
        lineStatus.value['yc'].status = true
      } else if (needShowLines.includes('yr')) {
        lineStatus.value['yr'].status = true
      }
    } else {
      if (needShowLines.includes('yr')) {
        lineStatus.value['yr'].status = true
      } else if (needShowLines.includes('yc')) {
        lineStatus.value['yc'].status = true
      } else if (needShowLines.includes('yl')) {
        lineStatus.value['yl'].status = true
      }
    }
    if (isDown) {
      if (needShowLines.includes('xt')) {
        lineStatus.value['xt'].status = true
      } else if (needShowLines.includes('xc')) {
        lineStatus.value['yc'].status = true
      } else if (needShowLines.includes('xb')) {
        lineStatus.value['xb'].status = true
      }
    } else {
      if (needShowLines.includes('xb')) {
        lineStatus.value['xb'].status = true
      } else if (needShowLines.includes('xc')) {
        lineStatus.value['yc'].status = true
      } else if (needShowLines.includes('xt')) {
        lineStatus.value['xt'].status = true
      }
    }

这样标线的功能就完整实现啦

属性配置

因为考虑到后面可能会增加组件,并且组件可能会增加属性的配置,所以每个组件的属性就做成可配置的了,如下是一个配置demo

css 复制代码
// 边框组件的属性配置
[COM_TYPE.FRAME]:[
  {
    key: 'border-color',
    label: '颜色',
    type: 'com',
    component: 'colorPicker',
    value: '#000000',
  },
  {
    key: 'border-width',
    label: '宽度',
    type: 'input',
    inputType: 'number',
    value: 1,
  }
]

可以配置引入的组件或者input等内置的组件,在html渲染当前组件属性配置的时候区分一下,例如type==='com'则通过<component>标签渲染,内置的则直接渲染即可。

引入的组件或者内置的组件需要emit change事件,因为很多属性配置的值需要通过转换,变成style可以识别的样式,例如border-radius可以配置四个角的圆角,配置的值是[10,10,10,10],需要转变成border-radius: 10px 10px 10px 10px,这时候就可以通过change事件来自定义的去改变组件值。

生成pdf

生成pdf的话是通过html2canvasjspdf插件实现的。如下是实现导出的代码

js 复制代码
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf'
export const htmlToPDF = async (htmlId: string, title: string = "报表", bgColor = "#fff") => {
    let pdfDom: HTMLElement | null = document.getElementById(htmlId) as HTMLElement
    pdfDom.style.padding = '0 10px !important'
    const A4Width = 592.28;
    const A4Height = 841.89;
    let canvas = await html2canvas(pdfDom, {
        scale: 2,
        useCORS: true,
        backgroundColor: bgColor,
    });
    let pageHeight = (canvas.width / A4Width) * A4Height;
    let leftHeight = canvas.height;
    let position = 0;
    let imgWidth = A4Width;
    let imgHeight = (A4Width / canvas.width) * canvas.height;
    /*
     根据自身业务需求  是否在此处键入下方水印代码
    */
    let pageData = canvas.toDataURL("image/jpeg", 1.0);
    let PDF = new jsPDF("p", 'pt', 'a4');
    if (leftHeight < pageHeight) {
        PDF.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight);
    } else {
        while (leftHeight > 0) {
            PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight);
            leftHeight -= pageHeight;
            position -= A4Height;
            if (leftHeight > 0) PDF.addPage();
        }
    }
    PDF.save(title + ".pdf");
}

导出展示问题

下载最新的html2canvas版本的时候,导出成pdf的文字总是向下偏移,看了很多网友的遇到的问题,将版本下载至"html2canvas": "1.0.0-alpha.12"就正常了。

总结

这是一个单纯学习的demo,功能简陋,有bug,喜欢的大佬可以给⭐支持一下 体验链接----github链接

相关推荐
BigYe程普4 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠17 分钟前
如何通过js加载css和html
javascript·css·html
余生H21 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
花花鱼22 分钟前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
程序员-珍24 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai28 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默40 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
与衫1 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql