Визуальный редактор страниц с перетаскиванием | Обзор проекта

Vue.js

предисловие

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

Прежде чем начать статью, вы можете испытать:

онлайн предварительный просмотр

Адрес GitHub

1.png

Основные функции редактора

  • Свободно перетаскивайте элементы, увеличивайте, уменьшайте, вращайте
  • Вы можете добавлять изображения, текст, прямоугольники, фоны. Несколько функций редактирования (шрифт, фон, размер, поля и т. д.)
  • Автоматическая привязка компонентов, направляющие в реальном времени (компоненты могут автоматически привязываться и выравниваться с холстом, пользовательскими направляющими и другими компонентами, при этом будут отображаться направляющие в реальном времени, которые можно временно отключить, нажав клавишу alt во время перетаскивания)
  • Линейка, контрольная линия, настраиваемая контрольная линия (нажмите на линейку, чтобы создать контрольную линию, перетащите контрольную линию, чтобы изменить положение, дважды щелкните, чтобы удалить контрольную линию)
  • Отмена, повтор (поддержка сочетаний клавиш, настраиваемое количество шагов для отмены)
  • Копирование компонентов, вставка, замок, скрытие и т. Д.
  • Ctrl + Перетащите компоненты для быстрого скопирования компонентов
  • Меню правой кнопки мыши, меню можно настроить и гибко генерировать в соответствии с текущим состоянием компонента (то есть разные компоненты могут генерировать разные меню)
  • Панель слоев, вы можете перетаскивать, чтобы изменить слои компонентов, вы можете переименовывать, вы можете быстро блокировать, удалять и скрывать компоненты на панели слоев
  • Выберите несколько компонентов одновременно (нажмите Ctrl + левая кнопка), чтобы выровнять несколько компонентов.
  • Резервное копирование данных, сохраненных локально через базу данных indexDB (автоматическое резервное копирование, резервное копирование вручную), и данные могут быть восстановлены из резервной копии
  • Сгенерируйте код h5 одним щелчком мыши
  • Изменить размер холста
  • Различные сочетания клавиш
  • Центр настроек, вы можете установить функцию отмены, функцию резервного копирования и т. д.
  • Вторичная разработка через систему плагинов

Так как деталей в нем много, я не могу рассказать обо всех пунктах, я выберу несколько основных для написания, некоторые из них могут быть относительно краткими, а конкретную реализацию можно найти в исходниках на GitHub.

Общая структура

2.png

Этот тип редактора обычно делится на 3 области: левая, средняя и правая, добавление компонентов слева, работа посередине и редактирование некоторых свойств компонентов справа.Я имею в виду дизайн Yiqixiu.Есть один в середине и справа Панель быстрого доступа имеет некоторые часто используемые настройки.

Эти разные области соответствуют разным функциям, поэтому нам также необходимо разделить эти разные функциональные области в коде:

<!-- 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 />
    </div>
    <!-- 图层面板 -->
    <transition name="el-zoom-in-top">
      <layer-panel v-show="layerPanelOpened" />
    </transition>
  </div>
</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свойство, которое представляет собой массив, содержащий все текущие компоненты. Добавление компонентов заключается в том, чтобы вставить данные в этот массив, а затем пройтиposterItemsПоместите компонент на холст, чтобы его можно было редактировать.

Реализация компонента

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

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)
  }
}

В этом конструкторе вы можете инициализировать некоторые конфигурации, а затем использовать новую, когда вы ее используете, а затем добавить ее в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.

Обратите внимание, что здесь представлен миксин, этоTextWidget.widgetMixinНа самом деле этоWidgetВверх:

export default class Widget {
  constructor(config) {
    // ...
  }

  // 组件mixin
  static widgetMixin(options) {
    // ...
  }
}

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

Перетащите, чтобы увеличить функцию

Это нужно для прямого использования компонента Vue: vue-draggable-resizable, просто вызовите этот компонент напрямую:

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

Хотя это выполнимо, но есть проблема, что у нас есть не только текстовые компоненты, но и в будущем могут быть добавлены картинки, прямоугольники, фоны и другие компоненты, и это перетаскивание не просто набор, мы должны написать другие A много логики, например, обновление данных осей 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>

Это фактически дополнительный слой, логика, связанная с перетаскиванием, написана в контейнере перетаскивания, нам нужно только реализовать логику внутреннего «компонента».

Установить свойства компонента

После того, как на холсте есть компоненты, вы можете редактировать компоненты в области редактирования справа, такие как размер шрифта, фон, граница и т. д., прямо сейчасTextWidgetEсть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:

5.png

4.png

Предположим, что в это время есть два компонента A и B, и теперь мы перетаскиваем компонент B. В процессе перетаскивания нам нужно в реальном времени следить, достаточно ли близко левая, средняя и правая стороны B к A. Нам нужно взять левую и правую стороны B. Сравнить левую, среднюю и правую части A соответственно, затем сравнить среднюю сторону B с левой, средней и правой сторонами A, а затем сравнить правую сторону B с слева, посередине и справа от A, всего три раунда сравнения, какое из сравнений в середине находит расстояние между двумя сторонами Заданное значение достигнуто Например, разница между левой стороной B и правая сторона A составляет 5 пикселей. В настоящее время вы можете вручную изменить координаты оси X B, чтобы выровнять B и A:

3.png

Соответственно, верхняя, средняя и нижняя стороны также сравниваются отдельно, и если они совпадают, изменяется координата оси Y точки B.

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

Суммировать

Некоторые части этой статьи могут быть написаны кратко, потому что это не учебный тип, а обмен предыдущими проектами, поэтому написание относительно краткое, если у вас есть какие-либо идеи или вы хотите узнать конкретные детали реализации того, какая функциональная точка , добро пожаловать в общение со мной.


Если вы считаете, что этот проект неплох, пожалуйста, поставьте лайк, спасибо~