添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
急躁的毛巾  ·  掌阅电纸书阅读器·  5 月前    · 
潇洒的雪糕  ·  datatables with ...·  7 月前    · 
有腹肌的扁豆  ·  恋系纪念日动画·  8 月前    · 
  • 元素自由拖拽,放大,缩小,旋转
  • 可添加图片,文本,矩形,背景。多种编辑功能(字体,背景,大小,边距等)
  • 组件自动吸附,实时参考线(组件可以和画布,自定义参考线以及其他组件进行自动吸附对齐,并显示实时参考线,拖动过程中按下 alt 键可暂时关闭)
  • 标尺,参考线,可自定义参考线(在标尺上点击即可生成参开线,可拖动参考线更改位置,双击删除参考线)
  • 撤销,重做(支持快捷键,可配置撤销的步数)
  • 组件复制,粘贴,锁定,隐藏等
  • ctrl + 拖动组件可快速复制组件
  • 右键菜单,菜单可配置,可针对组件当前状态灵活生成(即不同的组件可产生不同的菜单)
  • 图层面板,可拖拽更改组件图层,可重命名,可在图层面板快速锁定,删除,隐藏组件
  • 同时选中多组件(按 ctrl + 左键),可进行多组件对齐
  • 数据备份,通过 indexDB 数据库保存在本地(可自动备份,手动备份),并可从备份中恢复数据
  • 一键生成 h5 代码
  • 编辑画布大小
  • 多种快捷键
  • 设置中心,可设置撤销功能,备份功能等
  • 可通过插件系统二次开发
  • 由于里面的细节比较多,肯定不能将所有点都讲到,我就挑几个主要的写写,有些可能写的比较简略,具体的实现可以看 GitHub 上的源码。

    这种编辑器一般都是分为 3 个区域,左中右,在左边添加组件,中间操作,右侧可以编辑组件的一些属性,我这个是参考易企秀的设计,中部和右部有一个快捷操作栏,有一些常用的设置。

    这些不同的区域对应不同的功能,那么在代码里我们也要将这些不同的功能区域分开:

    <!-- index.vue -->
    <template>
      <div class="poster-editor" :class="{ 'init-loading': initLoading }">
        <div class="base">
          <!-- 左侧添加组件栏 -->
          <left-side />
          <!-- 主要操作区域 -->
          <main-component ref="main" />
          <!-- 常用功能栏 -->
          <extend-side-bar />
          <!-- 组件编辑区域 -->
          <control-component />
        <!-- 图层面板 -->
        <transition name="el-zoom-in-top">
          <layer-panel v-show="layerPanelOpened" />
        </transition>
    </template>
    

    然后还要有数据,这数据包含了画布属性,组件属性,当前编辑器状态等等,存在 vuex 中:

    const state = {
      activityId: '',
      pageConfigId: '',
      pageTitle: '',
      canvasSize: {
        width: 338,
        height: 600
      canvasPosition: {
        top: null,
        left: null
      background: null,
      posterItems: [], // 组件列表
      activeItems: [], // 当前选中的组件
      assistWidgets: [], // 辅助组件
      layerPanelOpened: true, // 是否打开图层面板
      referenceLineOpened: true, // 是否打开参考线
      copiedWidgets: null, // 当前复制的组件 WidgetItem[]
      referenceLine: {
        // 参考线,用户定义的参考线
        row: [],
        col: []
      matchedLine: null, // 匹配到的参考线 {row:[],col:[]}
      mainPanelScrollY: 0,
      isUnsavedState: false // 是否处于未保存状态
    

    这里最主要的就是 posterItems 属性,是保存当前所有组件的数组。添加组件就是往这个数组中 push 数据,然后遍历 posterItems 将组件放在画布上,进而可以编辑这个组件。

    因为一共有很多种组件,并且这些组件都有一些相同属性,比如位置大小信息,是否锁定,是否隐藏等等,这些相同的属性不可能每个组件都写一遍,所以要就要实现一个基础的组件,这个基础组件包含了所有组件通用的属性,然后其他组件就通过这个组件扩展,我这里通过 class 的方式实现:

    const defaultWidgetConfig = () => {
      return {
        id: '', // 组件id
        type: '', // 类型
        typeLabel: '', // 类型标签
        componentName: '', // 动态component的name
        icon: '', // 图标class
        wState: {}, // 组件内部状态数据,样式属性等信息
        dragInfo: { w: 100, h: 100, x: 0, y: 0, rotateZ: 0 }, // 组件的位置、大小、旋转角度
        rename: '', // typeLabel重命名
        lock: false, // 是否处于锁定状态
        visible: true, // 是否可见
        initHook: null, // Function 组件初始化时候(created)执行
        layerPanelVisible: true, // 是否在图片面板中可见
        replicable: true, // 是否可复制
        isCopied: false, // 是否是复制的组件(通过复制操作获得的组件)
        removable: true, // 是否可删除
        couldAddToActive: true, // 是否可被添加进activeItems
        componentState: null // Function 复制组件时有效,返回结果为为复制时原组件内部的data;componentState.count为复制的次数
         * @property {Int} _copyCount 复制的次数
         * @property {String} _copyFrom 复制来源 command | drag
         * @property {Boolean} _isBackup 是否是通过备份恢复的组件
         * @property {Int} _widgetCountLimit 该组件的数量限制
         * @property {Int} _sort 组件图层排序
    // 组件父类
    export default class Widget {
      constructor(config) {
        const item = _merge(defaultWidgetConfig(), config, {
          id: uniqueId(config.typeLabel + '-')
        // this._config = item
        Object.keys(item).forEach((key) => {
          this[key] = item[key]
      // 组件mixin
      static widgetMixin(options) {
        // ...一会讲
    

    defaultWidgetConfig 是组件的配置项, Widget 其实就是初始化这些配置,然后其他组件继承 Widget 就可以了,比如我们要实现一个文本组件:

    // 文本Widget
    export default class TextWidget extends Widget {
      constructor(config) {
        config = _merge(
            type: 'text',
            typeLabel: '文本',
            componentName: 'text-widget',
            icon: 'icon-text',
            lock: false,
            visible: true,
            wState: {
              text: '双击编辑文本',
              style: {
                margin: '10px',
                wordBreak: 'break-all',
                color: '#000',
                textAlign: 'center',
                fontSize: '14px', // px
                padding: 0, // px
                borderColor: '#000',
                borderWidth: 0, // px
                borderStyle: 'solid',
                lineHeight: '100%', // %
                letterSpacing: 0, // %
                backgroundColor: '',
                fontWeight: '',
                fontStyle: '',
                textDecoration: ''
          config
        super(config)
    

    这个构造函数里面就可以初始化一些配置,然后使用时就 new 一个,然后添加到 posterItems 中,简化后大概是这样的:

    // 添加文本组件
    store.dispatch('poster/addItem', new TextWidget())
    const actions = {
      addItem(state, item) {
        if (item instanceof Widget) {
          state.posterItems.push(item)
    

    添加完之后,按照上文所说,编辑器中部的操作区域是遍历这个 posterItems 的:

    <component
      v-for="item in posterItems"
      :key="item.id"
      :item="item"
      :is="item.componentName"
    

    这里是通过 componentName 来调用不同的组件,刚才的 TextWidget 已经配置了 componentName text-widget ,现在我们就来实现这个组件:

    <!-- textWidget.vue -->
    <template>
      <div class="text-widget">demo</div>
    </template>
    <script>
      import { TextWidget } from 'poster/widgetConstructor'
      export default {
        mixins: [TextWidget.widgetMixin()],
        data() {
          return {}
    </script>
    <style lang="scss" scoped></style>
    

    现在添加组件后,你就能在画布上看到 demo 了,这只是个示例,更加详细的内容可以看 GitHub 的源码。

    注意这里引入了一个 mixin,这个 TextWidget.widgetMixin 其实就是 Widget 上的:

    export default class Widget {
      constructor(config) {
        // ...
      // 组件mixin
      static widgetMixin(options) {
        // ...
    

    就是一些组件公共的逻辑,其实这里用高阶组件的方式比较好,用 mixin 的话需要每个组件都要写一遍,比较繁琐,只是当时没想清楚。这个 mixin 一会就会用到,用到再说,这里有个印象即可。

    拖拽缩放功能

    这个是直接用了一个 Vue 组件:vue-draggable-resizable,直接调用这个组件就行:

    <!-- textWidget.vue -->
    <template>
      <vue-draggable-resizable>
        <div class="text-widget">demo</div>
      </vue-draggable-resizable>
    </template>
    

    这样虽然是可行的,但是有个问题就是我们不仅有文本组件,还有图片,矩形、背景,以后可能还要添加其他组件,而且这个拖动不是套一下就可以的,我们还要写其他的很多逻辑,比如拖动时将 x、y 轴的数据实时更新到组件的属性中,还要有吸附对齐等功能,肯定不能每个组件都去写一遍这些东西,这个时候其实可以把“拖拽”和“组件”拆开,“拖拽”作为一个容器,然后里面嵌套“组件”

    <vue-draggable-resizable v-for="item in posterItems" :key="item.id">
      <component
        :is="item.componentName"
        ref="widget"
        :item="item"
        :is-active="isActive"
        v-on="$listeners"
        @draggableChange="draggable = $event"
    </vue-draggable-resizable>
    

    这样其实就是多了一层,把拖拽相关的逻辑都写在拖拽容器里,我们只需要实现内层“组件”的逻辑就可以了。

    设置组件属性

    画布上有了组件后,还要可以在右侧的编辑区域编辑组件,比如字号,背景,边框等等,刚才的 TextWidget 里面有一个 style 属性,字号什么的就是更改这个属性。

    那么我们要做的就是点击这个组件时,将这个组件置为 active 状态,然后右侧的区域就显示对应的属性编辑器,不同组件的设置项肯定也不一样,比如 TextWidget 需要字号,字体颜色,但是如果我们要做一个矩形的组件,设置项肯定和 TextWidget 不同,那么我们就需要单独实现各个组件的属性编辑器,然后判断当前 active 状态的组件的类型,根据不同的类型调用不同的属性编辑器。

    编辑器最必不可少的一个功能就是复制组件,复制组件分为“复制”、“粘贴”两个步骤,复制时就把当前这个组件的配置全部保存下来,,粘贴时就把这个配置拿出来,通过这个配置创建一个组件,添加到 posterItems 中去,下面是简化后的代码:

    // 复制组件
    const mutations = {
      [MTS.COPY_WIDGET](state, item) {
        const config = _.cloneDeep(item)
        state.copiedWidgets = config
    
    export default class CopiedWidget extends Widget {
      constructor(config) {
        config._copyCount += 1
        const configCopy = Object.assign({}, _.cloneDeep(config), {
          typeLabel: config.typeLabel + '-copy',
          isCopied: true
        super(configCopy)
    

    然后粘贴的时候只需要 state.posterItems.push(new CopiedWidget(state.copiedWidgets)) ,就可以了。

    我这里说一下我的实现思路,代码就不贴了,因为比较多,感兴趣的小伙伴可以查看源码。

    其实思路很简单,组件在X轴方向上对应“上”“中”“下”三条线,在Y轴上对应“左”“中”“右”三条线:

    假设此时有A和B两个组件,现在在拖动B组件,拖动过程中,我们需要实时监测B的左中右三条边和A的是否靠的足够近,就要拿B的左边和A的左中右分别对比,再拿B的中边和A的左中右对比,再拿B的右边和A的左中右对比,总共对比三轮,中间哪一次对比发现两条边的距离达到预设值了,比如说B的左边和A的右边相差了5像素,这时候就可以手动更改B的X轴坐标将B和A对齐:

    对应的,上中下三条边也是分别对比,然后匹配到的话就修改B的Y轴坐标。

    就是这个思路,实际情况可能比较复杂点,因为不可能只有两个组件,而且除了要组件之间对齐,还要支持自定义参考线,还要和画布的边对齐,感兴趣的同学可以直接看Github的源码。

    这篇文章有些地方写的可能比较简略,因为不是一个教程类型的,是对以前的项目做个分享,所以写的比较简略,如果你有什么想法,或者想知道哪个功能点的具体实现细节的,欢迎和我交流。

    如果你觉得这个项目还不错的话,欢迎点个赞,感谢~

    版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: zhangzp 原文链接: https://juejin.im/post/6941657971650887716