предисловие
В прошлом году в свободное время я разработал визуальный редактор страниц, а в этот раз увидел, что у Nuggets есть активность по обзору проектов, так что я мог просто написать статью и поделиться ею с вами. Я не знаю, смогу ли я догнать событие~
Прежде чем начать статью, вы можете испытать:
онлайн предварительный просмотр
Основные функции редактора
- Свободно перетаскивайте элементы, увеличивайте, уменьшайте, вращайте
- Вы можете добавлять изображения, текст, прямоугольники, фоны. Несколько функций редактирования (шрифт, фон, размер, поля и т. д.)
- Автоматическая привязка компонентов, направляющие в реальном времени (компоненты могут автоматически привязываться и выравниваться с холстом, пользовательскими направляющими и другими компонентами, при этом будут отображаться направляющие в реальном времени, которые можно временно отключить, нажав клавишу alt во время перетаскивания)
- Линейка, контрольная линия, настраиваемая контрольная линия (нажмите на линейку, чтобы создать контрольную линию, перетащите контрольную линию, чтобы изменить положение, дважды щелкните, чтобы удалить контрольную линию)
- Отмена, повтор (поддержка сочетаний клавиш, настраиваемое количество шагов для отмены)
- Копирование компонентов, вставка, замок, скрытие и т. Д.
- Ctrl + Перетащите компоненты для быстрого скопирования компонентов
- Меню правой кнопки мыши, меню можно настроить и гибко генерировать в соответствии с текущим состоянием компонента (то есть разные компоненты могут генерировать разные меню)
- Панель слоев, вы можете перетаскивать, чтобы изменить слои компонентов, вы можете переименовывать, вы можете быстро блокировать, удалять и скрывать компоненты на панели слоев
- Выберите несколько компонентов одновременно (нажмите Ctrl + левая кнопка), чтобы выровнять несколько компонентов.
- Резервное копирование данных, сохраненных локально через базу данных indexDB (автоматическое резервное копирование, резервное копирование вручную), и данные могут быть восстановлены из резервной копии
- Сгенерируйте код h5 одним щелчком мыши
- Изменить размер холста
- Различные сочетания клавиш
- Центр настроек, вы можете установить функцию отмены, функцию резервного копирования и т. д.
- Вторичная разработка через систему плагинов
Так как деталей в нем много, я не могу рассказать обо всех пунктах, я выберу несколько основных для написания, некоторые из них могут быть относительно краткими, а конкретную реализацию можно найти в исходниках на GitHub.
Общая структура
Этот тип редактора обычно делится на 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:
Предположим, что в это время есть два компонента A и B, и теперь мы перетаскиваем компонент B. В процессе перетаскивания нам нужно в реальном времени следить, достаточно ли близко левая, средняя и правая стороны B к A. Нам нужно взять левую и правую стороны B. Сравнить левую, среднюю и правую части A соответственно, затем сравнить среднюю сторону B с левой, средней и правой сторонами A, а затем сравнить правую сторону B с слева, посередине и справа от A, всего три раунда сравнения, какое из сравнений в середине находит расстояние между двумя сторонами Заданное значение достигнуто Например, разница между левой стороной B и правая сторона A составляет 5 пикселей. В настоящее время вы можете вручную изменить координаты оси X B, чтобы выровнять B и A:
Соответственно, верхняя, средняя и нижняя стороны также сравниваются отдельно, и если они совпадают, изменяется координата оси Y точки B.
Это способ мышления, реальная ситуация может быть сложнее, потому что невозможно иметь только два компонента, и помимо выравнивания компонентов, он также поддерживает настраиваемые опорные линии, а также выравнивание по краю холста. Заинтересованные студенты могут непосредственно ознакомиться с исходным кодом Github.
Суммировать
Некоторые части этой статьи могут быть написаны кратко, потому что это не учебный тип, а обмен предыдущими проектами, поэтому написание относительно краткое, если у вас есть какие-либо идеи или вы хотите узнать конкретные детали реализации того, какая функциональная точка , добро пожаловать в общение со мной.
Если вы считаете, что этот проект неплох, пожалуйста, поставьте лайк, спасибо~