В этой статье в основном анализируются следующие технические моменты:
- редактор
- пользовательский компонент
- тянуть
- Удалить компоненты, настроить уровни слоев
- приблизить
- отменить повторить
- Настройки свойств компонента
- адсорбция
- Предварительный просмотр и сохранение кода
- привязать событие
- Связать анимацию
- Импорт PSD
- режим телефона
Чтобы сделать эту статью более понятной, я объединил приведенные выше технические моменты, чтобы написать ДЕМО-версию визуальной библиотеки компонентов перетаскивания:
Рекомендуется читать вместе с исходным кодом, эффект лучше (в этом DEMO используется стек технологий Vue).
1. Редактор
Давайте сначала посмотрим на общую структуру страницы.
Редактор, который будет обсуждаться в этом разделе, на самом деле является средним холстом. Его функция такова: когда компонент перетаскивается из списка компонентов слева и помещается на холст, холст отображает компонент.
Идея реализации этого редактора такова:
- использовать массив
componentData
Сохраняйте данные в редакторе. - При перетаскивании компонента на холст используйте
push()
метод для добавления новых данных компонента вcomponentData
. - использование редактора
v-for
обход инструкцииcomponentData
, отображать каждый компонент на холсте один за другим (также можно комбинировать с использованием синтаксиса JSXrender()
метод вместо этого).
Основной код для рендеринга редактора выглядит следующим образом:
<component
v-for="item in componentData"
:key="item.id"
:is="item.component"
:style="item.style"
:propValue="item.propValue"
/>
Данные для каждого компонента выглядят следующим образом:
{
component: 'v-text', // 组件名称,需要提前注册到 Vue
label: '文字', // 左侧组件列表中显示的名字
propValue: '文字', // 组件所使用的值
icon: 'el-icon-edit', // 左侧组件列表中显示的名字
animations: [], // 动画列表
events: {}, // 事件列表
style: { // 组件样式
width: 200,
height: 33,
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: '',
},
}
пересекаяcomponentData
данные о компонентах, в основном полагаются наis
свойство, чтобы определить, какой компонент на самом деле рендерится.
Например, данные компонента для рендеринга{ component: 'v-text' }
,но<component :is="item.component" />
будет преобразован в<v-text />
. Конечно, ваш компонент также должен быть заранее зарегистрирован во Vue.
Если вы хотите узнать большеis
информацию о собственности см.официальная документация.
2. Пользовательские компоненты
В принципе, можно использовать и сторонние компоненты, но рекомендуется их лучше инкапсулировать. Будь то сторонний компонент или пользовательский компонент, свойства, требуемые каждым компонентом, могут быть разными, поэтому данные каждого компонента могут раскрывать свойство.propValue
для передачи значений.
Например, компоненту требуется только одно свойство, вашеpropValue
Это можно написать так:propValue: 'aaa'
. Если требуется несколько свойств,propValue
может быть объектом:
propValue: {
a: 1,
b: 'text'
}
В этой библиотеке компонентов DEMO я определил три компонента.
компонент изображенияPicture
:
<template>
<div style="overflow: hidden">
<img :src="propValue">
</div>
</template>
<script>
export default {
props: {
propValue: {
type: String,
require: true,
},
},
}
</script>
компонент кнопкиVButton
:
<template>
<button class="v-button">{{ propValue }}</button>
</template>
<script>
export default {
props: {
propValue: {
type: String,
default: '',
},
},
}
</script>
текстовый компонентVText
:
<template>
<textarea
v-if="editMode == 'edit'"
:value="propValue"
class="text textarea"
@input="handleInput"
ref="v-text"
></textarea>
<div v-else class="text disabled">
<div v-for="(text, index) in propValue.split('\n')" :key="index">{{ text }}</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
props: {
propValue: {
type: String,
},
element: {
type: Object,
},
},
computed: mapState([
'editMode',
]),
methods: {
handleInput(e) {
this.$emit('input', this.element, e.target.value)
},
},
}
</script>
3. Перетащите
От списка компонентов к холсту
Если элемент должен быть перетаскиваемым, он должен быть добавлен сdraggable
Атрибуты. Кроме того, при перетаскивании компонентов в списке компонентов на холст есть два других события, играющих ключевую роль:
-
dragstart
Событие, срабатывающее при начале перетаскивания. Он в основном используется для передачи информации о перетаскиваемом компоненте на холст. -
drop
Событие, срабатывающее при завершении перетаскивания. В основном используется для получения информации о перетаскиваемом компоненте.
Давайте сначала посмотрим на код списка компонентов слева:
<div @dragstart="handleDragStart" class="component-list">
<div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index">
<i :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
</div>
handleDragStart(e) {
e.dataTransfer.setData('index', e.target.dataset.index)
}
Вы можете видеть, что каждый компонент в списке установленdraggable
Атрибуты. Также при срабатыванииdragstart
событие, использованиеdataTransfer.setData()
передавать данные. Давайте посмотрим на код, который получает данные:
<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent">
<Editor />
</div>
handleDrop(e) {
e.preventDefault()
e.stopPropagation()
const component = deepCopy(componentList[e.dataTransfer.getData('index')])
this.$store.commit('addComponent', component)
}
вызыватьdrop
событие, использованиеdataTransfer.getData()
Получите переданные данные индекса, найдите соответствующие данные компонента в соответствии с индексом и добавьте их на холст для визуализации компонента.
Компонент перемещается на холсте
Сначала вам нужно установить относительное позиционирование холста.position: relative
, затем сделайте каждый компонент абсолютно позиционированнымposition: absolute
. В дополнение к этому движение производится путем прослушивания трех событий:
-
mousedown
Событие, когда мышь нажимает на компонент, записывает текущую позицию компонента, то есть координаты xy (для удобства пояснения используемая здесь ось координат фактически соответствует xy в css.left
а такжеtop
. -
mousemove
Событие: каждый раз, когда мышь перемещается, вычтите первую координату xy из текущей последней координаты xy, чтобы вычислить расстояние перемещения, а затем измените положение компонента. -
mouseup
Событие, движение заканчивается, когда мышь поднята.
handleMouseDown(e) {
e.stopPropagation()
this.$store.commit('setCurComponent', { component: this.element, zIndex: this.zIndex })
const pos = { ...this.defaultStyle }
const startY = e.clientY
const startX = e.clientX
// 如果直接修改属性,值的类型会变为字符串,所以要转为数值型
const startTop = Number(pos.top)
const startLeft = Number(pos.left)
const move = (moveEvent) => {
const currX = moveEvent.clientX
const currY = moveEvent.clientY
pos.top = currY - startY + startTop
pos.left = currX - startX + startLeft
// 修改当前组件样式
this.$store.commit('setShapeStyle', pos)
}
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
}
4. Удалите компоненты, отрегулируйте уровни слоев
Изменить уровень слоя
Поскольку существует порядок перетаскивания компонентов на холст, иерархия слоев может быть назначена в соответствии с порядком данных.
Например, на холст добавляются пять новых компонентов abcde, и их порядок в данных холста следующий:[a, b, c, d, e]
уровень слоя и индекс соответствует одному к одному, то есть ихz-index
Значение атрибута 01234 (последнее). Выражается в коде следующим образом:
<div v-for="(item, index) in componentData" :zIndex="index"></div>
Если вы не понимаетеz-index
атрибут, пожалуйста, взглянитеДокументация MDN.
Как только вы поймете это, изменить уровень слоя будет легко. Изменение уровня слоя означает изменение данных компонента вcomponentData
порядок в массиве. Например, есть[a, b, c]
Три компонента, их уровни слоев расположены в порядке abc от низкого к высокому (чем больше индекс, тем выше уровень).
Если вы хотите переместить компонент b вверх, просто замените его на c:
const temp = componentData[1]
componentData[1] = componentData[2]
componentData[2] = temp
Точно так же то же самое верно для верхнего и нижнего.Например, если я хочу поместить компонент сверху, мне просто нужно изменить порядок a и последнего компонента:
const temp = componentData[0]
componentData[0] = componentData[componentData.lenght - 1]
componentData[componentData.lenght - 1] = temp
удалить компонент
Удалить компонент очень просто, с помощью одной строки кода:componentData.splice(index, 1)
.
5. Увеличение и уменьшение масштаба
Внимательные пользователи сети могут обнаружить, что если щелкнуть компонент на холсте, на нем появятся 8 маленьких точек. Эти 8 маленьких точек используются для увеличения и уменьшения масштаба. Принцип реализации следующий:
1. Оберните слой вокруг каждого компонентаShape
компоненты,Shape
Компонент содержит 8 маленьких точек и<slot>
Прорези для размещения компонентов.
<!--页面组件列表展示-->
<Shape v-for="(item, index) in componentData"
:defaultStyle="item.style"
:style="getShapeStyle(item.style, index)"
:key="item.id"
:active="item === curComponent"
:element="item"
:zIndex="index"
>
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</Shape>
Shape
Внутренняя структура компонента:
<template>
<div class="shape" :class="{ active: this.active }" @click="selectCurComponent" @mousedown="handleMouseDown"
@contextmenu="handleContextMenu">
<div
class="shape-point"
v-for="(item, index) in (active? pointList : [])"
@mousedown="handleMouseDownOnPoint(item)"
:key="index"
:style="getPointStyle(item)">
</div>
<slot></slot>
</div>
</template>
2. Нажмите на компонент, появятся восемь маленьких точек.
Что сработало, так это строка кода:active="item === curComponent"
.
3. Рассчитайте положение каждой маленькой точки.
Давайте сначала посмотрим на код, который вычисляет положение маленькой точки:
const pointList = ['t', 'r', 'b', 'l', 'lt', 'rt', 'lb', 'rb']
getPointStyle(point) {
const { width, height } = this.defaultStyle
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
let newLeft = 0
let newTop = 0
// 四个角的点
if (point.length === 2) {
newLeft = hasL? 0 : width
newTop = hasT? 0 : height
} else {
// 上下两点的点,宽度居中
if (hasT || hasB) {
newLeft = width / 2
newTop = hasT? 0 : height
}
// 左右两边的点,高度居中
if (hasL || hasR) {
newLeft = hasL? 0 : width
newTop = Math.floor(height / 2)
}
}
const style = {
marginLeft: hasR? '-4px' : '-3px',
marginTop: '-3px',
left: `${newLeft}px`,
top: `${newTop}px`,
cursor: point.split('').reverse().map(m => this.directionKey[m]).join('') + '-resize',
}
return style
}
Для вычисления положения маленькой точки требуется некоторая информация:
- высота компонента
height
,ширинаwidth
Обратите внимание, что маленькие точки также расположены абсолютно относительноShape
компоненты. Таким образом, положение четырех маленьких точек хорошо установлено:
- Маленькая точка в верхнем левом углу, координаты
left: 0, top: 0
- Маленькая точка в правом верхнем углу, координаты
left: width, top: 0
- Маленькая точка в левом нижнем углу, координаты
left: 0, top: height
- Маленькая точка в правом нижнем углу, координаты
left: width, top: height
Остальные четыре маленькие точки необходимо вычислить косвенным путем. Например, маленькая точка посередине слева, формула расчетаleft: 0, top: height / 2
, и то же самое для других маленьких точек.
4. Когда вы нажимаете на маленькую точку, вы можете увеличивать и уменьшать масштаб.
handleMouseDownOnPoint(point) {
const downEvent = window.event
downEvent.stopPropagation()
downEvent.preventDefault()
const pos = { ...this.defaultStyle }
const height = Number(pos.height)
const width = Number(pos.width)
const top = Number(pos.top)
const left = Number(pos.left)
const startX = downEvent.clientX
const startY = downEvent.clientY
// 是否需要保存快照
let needSave = false
const move = (moveEvent) => {
needSave = true
const currX = moveEvent.clientX
const currY = moveEvent.clientY
const disY = currY - startY
const disX = currX - startX
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
const newHeight = height + (hasT? -disY : hasB? disY : 0)
const newWidth = width + (hasL? -disX : hasR? disX : 0)
pos.height = newHeight > 0? newHeight : 0
pos.width = newWidth > 0? newWidth : 0
pos.left = left + (hasL? disX : 0)
pos.top = top + (hasT? disY : 0)
this.$store.commit('setShapeStyle', pos)
}
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
needSave && this.$store.commit('recordSnapshot')
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
}
Его принцип таков:
- При щелчке по маленькой точке записываются координаты xy щелчка.
- Предполагая, что теперь мы перетаскиваем вниз, координата y увеличится.
- Вычтите исходную координату y из новой координаты y, и вы сможете узнать, какое расстояние составляет перемещение в направлении вертикальной оси.
- Наконец, добавьте расстояние перемещения к высоте исходного компонента, чтобы получить новую высоту компонента.
- Если это положительное число, это означает, что он тянет вниз, а высота компонента увеличивается. Если это отрицательное число, это означает, что он подтягивается, а высота компонента уменьшается.
6. Отменить, повторить
Принцип реализации undo redo на самом деле довольно прост, давайте сначала посмотрим на код:
snapshotData: [], // 编辑器快照数据
snapshotIndex: -1, // 快照索引
undo(state) {
if (state.snapshotIndex >= 0) {
state.snapshotIndex--
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
redo(state) {
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotIndex++
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
setComponentData(state, componentData = []) {
Vue.set(state, 'componentData', componentData)
},
recordSnapshot(state) {
// 添加新的快照
state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
// 在 undo 过程中,添加新的快照时,要将它后面的快照清理掉
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)
}
},
Используйте массив для хранения данных снимка редактора. Чтобы сохранить снимок, нужно продолжать выполнениеpush()
Действие, помещает текущие данные редактора вsnapshotData
массив и увеличить индекс моментального снимкаsnapshotIndex
. В настоящее время следующие действия запускают операцию сохранения моментального снимка:
- новые компоненты
- удалить компонент
- Изменить уровень слоя
- Когда перетаскивание компонента заканчивается
...
отозвать
Предположим теперьsnapshotData
保存了 4 个快照。 который[a, b, c, d]
, соответствующий индекс моментального снимка равен 3. Если в это время выполняется операция отмены, нам нужно уменьшить индекс моментального снимка на 1, а затем присвоить холсту соответствующие данные моментального снимка.
Например, текущие данные холста — d, после отмены индекс равен -1, а текущие данные холста — c.
переделывать
Поймите отмену, затем легко понять повтор, то есть добавьте 1 к индексу снимка, а затем назначьте соответствующие данные снимка холсту.
Однако следует отметить еще один момент, то есть во время операции отмены выполняется новая операция, что мне делать? Есть два решения:
- Новая операция заменяет все данные, следующие за индексом текущего моментального снимка. Все еще использовать данные только сейчас
[a, b, c, d]
Например, предположим, что сейчас выполняются две операции отмены, индекс моментального снимка становится равным 1, а соответствующие данные моментального снимка — b.Если в это время выполняется новая операция, соответствующие данные моментального снимка — e. Затем e удалит компакт-диск, и текущие данные снимка будут[a, b, e]
. - Не сбрасывайте данные и добавьте новую запись к исходному снимку. Используя пример только что, e не будет удалять компакт-диск, а вставит его перед компакт-диском, то есть данные снимка станут
[a, b, e, c, d]
.
Я использовал первый вариант.
7. Адсорбция
Что такое адсорбция? То есть при перетаскивании компонента, если он находится рядом с другим компонентом, он автоматически соединится вместе.
Адсорбированный код составляет около 300 строк, для просмотра рекомендуется открыть исходный файл (путь к файлу:src\components\Editor\MarkLine.vue
). Я не буду выкладывать сюда код, в основном рассказываю о том, как реализован принцип.
Маркировка
Создайте на странице 6 линий, три горизонтальные и три вертикальные. Цель этих 6 линий - выровнять, когда они появятся?
- Вертикальные линии появятся, когда два компонента в верхнем и нижнем направлениях выровнены слева, посередине и справа.
- Горизонтальные линии появятся, когда верхняя, средняя и нижняя части двух компонентов в левом и правом направлениях выровнены.
Конкретная формула расчета в основном рассчитывается на основе координат xy, ширины и высоты каждого компонента. Например, чтобы определить, выровнена ли левая сторона двух компонентов ab, вам нужно знать координату x каждого из их компонентов; если вы хотите знать, выровнены ли они справа, в дополнение к знанию x -coordinate, вам также необходимо знать их соответствующую ширину.
// 左对齐的条件
a.x == b.x
// 右对齐的条件
a.x + a.width == b.x + b.width
При выравнивании отображается сетка.
Кроме того, необходимо судить о том, «достаточно ли» близки две составляющие ab. Если они достаточно близко, они соединяются. Достаточно ли близко зависит от переменной:
diff: 3, // 相距 dff 像素将自动吸附
меньше или равноdiff
Пиксели автоматически фиксируются.
адсорбция
Как достигается эффект адсорбции?
Предположим, что теперь есть компонент ab, координаты xy компонента a равны 0, а ширина и высота равны 100. Теперь предположим, что компонент a не движется, а мы перетаскиваем компонент b. При перетаскивании компонента b в координатыx: 0, y: 103
когда из-за103 - 100 <= 3(diff)
, так что можно судить, что они достаточно близки. В это время вам нужно вручную установить значение координаты y компонента b равным 100, чтобы компоненты ab были совмещены.
оптимизация
Было бы неприглядно, если бы при перетаскивании отображались все 6 строк. Таким образом, мы можем сделать некоторую оптимизацию, чтобы отображать не более одной строки в вертикальном и горизонтальном направлениях одновременно. Принцип реализации следующий:
- Компонент а не перемещается влево, и мы перетаскиваем компонент b, чтобы приблизиться к компоненту а.
- В это время они сначала выравниваются справа от a и слева от b, поэтому нужна только одна линия.
- Если компонент ab уже близок, а компонент b продолжает двигаться влево, то необходимо определить, совмещена ли середина из двух.
- Компонент B продолжает перетаскивать. В это время необходимо судить, выровнена ли левая сторона компонента с правой стороной компонента B, и требуется только одна строка.
Может быть обнаружено, что ключевой момент в том, что нам нужно знать направление двух компонентов. То есть два компонента AB близки, и нам нужно знать, находится ли B слева или справа от.
Об этом можно судить по событию движения мыши, как я уже говорил, когда объяснял перетаскивание,mousedown
Координаты начальной точки записываются при срабатывании события. Поэтому каждый раз, когда триггерmousemove
Когда происходит событие, вычтите исходные координаты из текущих координат, чтобы определить направление компонента. Например, в направлении х, еслиb.x - a.x
Разница положительна, что указывает на то, что b находится справа от a, в противном случае — слева.
// 触发元素移动事件,用于显示标线、吸附功能
// 后面两个参数代表鼠标移动方向
// currY - startY > 0 true 表示向下移动 false 表示向上移动
// currX - startX > 0 true 表示向右移动 false 表示向左移动
eventBus.$emit('move', this.$el, currY - startY > 0, currX - startX > 0)
8. Настройки свойств компонента
Каждый компонент имеет некоторые общие свойства и уникальные свойства, нам нужно предоставить место для отображения и изменения свойств.
// 每个组件数据大概是这样
{
component: 'v-text', // 组件名称,需要提前注册到 Vue
label: '文字', // 左侧组件列表中显示的名字
propValue: '文字', // 组件所使用的值
icon: 'el-icon-edit', // 左侧组件列表中显示的名字
animations: [], // 动画列表
events: {}, // 事件列表
style: { // 组件样式
width: 200,
height: 33,
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: '',
},
}
я определяюAttrList
Компоненты для отображения свойств каждого компонента.
<template>
<div class="attr-list">
<el-form>
<el-form-item v-for="(key, index) in styleKeys" :key="index" :label="map[key]">
<el-color-picker v-if="key == 'borderColor'" v-model="curComponent.style[key]"></el-color-picker>
<el-color-picker v-else-if="key == 'color'" v-model="curComponent.style[key]"></el-color-picker>
<el-color-picker v-else-if="key == 'backgroundColor'" v-model="curComponent.style[key]"></el-color-picker>
<el-select v-else-if="key == 'textAlign'" v-model="curComponent.style[key]">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-input type="number" v-else v-model="curComponent.style[key]" />
</el-form-item>
<el-form-item label="内容" v-if="curComponent && curComponent.propValue && !excludes.includes(curComponent.component)">
<el-input type="textarea" v-model="curComponent.propValue" />
</el-form-item>
</el-form>
</div>
</template>
Логика кода очень проста, это обход компонентовstyle
объект, обходя каждое свойство. И он должен отображаться с различными компонентами в соответствии с конкретными свойствами, такими как свойства цвета, которые должны отображаться с помощью селектора цвета; свойства числовых классов должны отображаться с помощьюtype=number
Компонент ввода отображает и так далее.
Чтобы облегчить пользователю изменение значения атрибута, я используюv-model
Свяжите компоненты и значения вместе.
9. Предварительный просмотр и сохранение кода
Принцип рендеринга предварительного просмотра и редактирования одинаков, разница в том, что функция редактирования не требуется. Так что вам просто нужно немного изменить код исходного компонента рендеринга.
<!--页面组件列表展示-->
<Shape v-for="(item, index) in componentData"
:defaultStyle="item.style"
:style="getShapeStyle(item.style, index)"
:key="item.id"
:active="item === curComponent"
:element="item"
:zIndex="index"
>
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</Shape>
После введения только что мы знаем, чтоShape
Компонент имеет функции перетаскивания, увеличения и уменьшения масштаба. Теперь просто нужноShape
Компонент удаляется, а внешний вид можно изменить на общий DIV (на самом деле этот DIV не нужен, но для того, чтобы привязать функцию события, его нужно добавить).
<!--页面组件列表展示-->
<div v-for="(item, index) in componentData" :key="item.id">
<component
class="component"
:is="item.component"
:style="getComponentStyle(item.style)"
:propValue="item.propValue"
/>
</div>
Функция сохранения кода тоже очень проста, нужно только сохранить данные на канвеcomponentData
Вот и все. Сохранить иметь два варианта:
- сохранить на сервер
- сохранить локально
В DEMO я используюlocalStorage
Сохраняйте локально.
10. Обязательные события
Каждый компонент имеетevents
Объект для хранения связанных событий. В настоящее время у меня определены только два события:
- оповещение о событии
- событие перенаправления
// 编辑器自定义事件
const events = {
redirect(url) {
if (url) {
window.location.href = url
}
},
alert(msg) {
if (msg) {
alert(msg)
}
},
}
const mixins = {
methods: events,
}
const eventList = [
{
key: 'redirect',
label: '跳转事件',
event: events.redirect,
param: '',
},
{
key: 'alert',
label: 'alert 事件',
event: events.alert,
param: '',
},
]
export {
mixins,
events,
eventList,
}
Однако его нельзя активировать во время редактирования, но можно активировать во время предварительного просмотра.
Добавить событие
пройти черезv-for
Директива отображает список событий:
<el-tabs v-model="eventActiveName">
<el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :name="item.key" style="padding: 0 20px">
<el-input v-if="item.key == 'redirect'" v-model="item.param" type="textarea" placeholder="请输入完整的 URL" />
<el-input v-if="item.key == 'alert'" v-model="item.param" type="textarea" placeholder="请输入要 alert 的内容" />
<el-button style="margin-top: 20px;" @click="addEvent(item.key, item.param)">确定</el-button>
</el-tab-pane>
</el-tabs>
Добавьте событие в компонентevents
объект.
триггерное событие
При предварительном просмотре или фактическом рендеринге страницы также необходимо обернуть слой DIV снаружи каждого компонента, чтобы событие щелчка можно было привязать к DIV, а только что добавленное событие срабатывало при нажатии.
<template>
<div @click="handleClick">
<component
class="conponent"
:is="config.component"
:style="getStyle(config.style)"
:propValue="config.propValue"
/>
</div>
</template>
handleClick() {
const events = this.config.events
// 循环触发绑定的事件
Object.keys(events).forEach(event => {
this[event](events[event])
})
}
11. Привязать анимацию
Принцип анимации и события одинаков, сначала пропустите все анимации черезv-for
Инструкция отображается, а затем щелкните анимацию, чтобы добавить соответствующую анимацию в компонент.animations
в массиве. Как и события, при выполнении он обходит все анимации компонента и выполняет их.
Для удобства мы использовалиanimate.cssБиблиотека анимации.
// main.js
import '@/styles/animate.css'
Теперь мы заранее определяем все данные анимации:
export default [
{
label: '进入',
children: [
{ label: '渐显', value: 'fadeIn' },
{ label: '向右进入', value: 'fadeInLeft' },
{ label: '向左进入', value: 'fadeInRight' },
{ label: '向上进入', value: 'fadeInUp' },
{ label: '向下进入', value: 'fadeInDown' },
{ label: '向右长距进入', value: 'fadeInLeftBig' },
{ label: '向左长距进入', value: 'fadeInRightBig' },
{ label: '向上长距进入', value: 'fadeInUpBig' },
{ label: '向下长距进入', value: 'fadeInDownBig' },
{ label: '旋转进入', value: 'rotateIn' },
{ label: '左顺时针旋转', value: 'rotateInDownLeft' },
{ label: '右逆时针旋转', value: 'rotateInDownRight' },
{ label: '左逆时针旋转', value: 'rotateInUpLeft' },
{ label: '右逆时针旋转', value: 'rotateInUpRight' },
{ label: '弹入', value: 'bounceIn' },
{ label: '向右弹入', value: 'bounceInLeft' },
{ label: '向左弹入', value: 'bounceInRight' },
{ label: '向上弹入', value: 'bounceInUp' },
{ label: '向下弹入', value: 'bounceInDown' },
{ label: '光速从右进入', value: 'lightSpeedInRight' },
{ label: '光速从左进入', value: 'lightSpeedInLeft' },
{ label: '光速从右退出', value: 'lightSpeedOutRight' },
{ label: '光速从左退出', value: 'lightSpeedOutLeft' },
{ label: 'Y轴旋转', value: 'flip' },
{ label: '中心X轴旋转', value: 'flipInX' },
{ label: '中心Y轴旋转', value: 'flipInY' },
{ label: '左长半径旋转', value: 'rollIn' },
{ label: '由小变大进入', value: 'zoomIn' },
{ label: '左变大进入', value: 'zoomInLeft' },
{ label: '右变大进入', value: 'zoomInRight' },
{ label: '向上变大进入', value: 'zoomInUp' },
{ label: '向下变大进入', value: 'zoomInDown' },
{ label: '向右滑动展开', value: 'slideInLeft' },
{ label: '向左滑动展开', value: 'slideInRight' },
{ label: '向上滑动展开', value: 'slideInUp' },
{ label: '向下滑动展开', value: 'slideInDown' },
],
},
{
label: '强调',
children: [
{ label: '弹跳', value: 'bounce' },
{ label: '闪烁', value: 'flash' },
{ label: '放大缩小', value: 'pulse' },
{ label: '放大缩小弹簧', value: 'rubberBand' },
{ label: '左右晃动', value: 'headShake' },
{ label: '左右扇形摇摆', value: 'swing' },
{ label: '放大晃动缩小', value: 'tada' },
{ label: '扇形摇摆', value: 'wobble' },
{ label: '左右上下晃动', value: 'jello' },
{ label: 'Y轴旋转', value: 'flip' },
],
},
{
label: '退出',
children: [
{ label: '渐隐', value: 'fadeOut' },
{ label: '向左退出', value: 'fadeOutLeft' },
{ label: '向右退出', value: 'fadeOutRight' },
{ label: '向上退出', value: 'fadeOutUp' },
{ label: '向下退出', value: 'fadeOutDown' },
{ label: '向左长距退出', value: 'fadeOutLeftBig' },
{ label: '向右长距退出', value: 'fadeOutRightBig' },
{ label: '向上长距退出', value: 'fadeOutUpBig' },
{ label: '向下长距退出', value: 'fadeOutDownBig' },
{ label: '旋转退出', value: 'rotateOut' },
{ label: '左顺时针旋转', value: 'rotateOutDownLeft' },
{ label: '右逆时针旋转', value: 'rotateOutDownRight' },
{ label: '左逆时针旋转', value: 'rotateOutUpLeft' },
{ label: '右逆时针旋转', value: 'rotateOutUpRight' },
{ label: '弹出', value: 'bounceOut' },
{ label: '向左弹出', value: 'bounceOutLeft' },
{ label: '向右弹出', value: 'bounceOutRight' },
{ label: '向上弹出', value: 'bounceOutUp' },
{ label: '向下弹出', value: 'bounceOutDown' },
{ label: '中心X轴旋转', value: 'flipOutX' },
{ label: '中心Y轴旋转', value: 'flipOutY' },
{ label: '左长半径旋转', value: 'rollOut' },
{ label: '由小变大退出', value: 'zoomOut' },
{ label: '左变大退出', value: 'zoomOutLeft' },
{ label: '右变大退出', value: 'zoomOutRight' },
{ label: '向上变大退出', value: 'zoomOutUp' },
{ label: '向下变大退出', value: 'zoomOutDown' },
{ label: '向左滑动收起', value: 'slideOutLeft' },
{ label: '向右滑动收起', value: 'slideOutRight' },
{ label: '向上滑动收起', value: 'slideOutUp' },
{ label: '向下滑动收起', value: 'slideOutDown' },
],
},
]
затем используйтеv-for
Команда отображает список анимаций.
Добавить анимацию
<el-tabs v-model="animationActiveName">
<el-tab-pane v-for="item in animationClassData" :key="item.label" :label="item.label" :name="item.label">
<el-scrollbar class="animate-container">
<div
class="animate"
v-for="(animate, index) in item.children"
:key="index"
@mouseover="hoverPreviewAnimate = animate.value"
@click="addAnimation(animate)"
>
<div :class="[hoverPreviewAnimate === animate.value && animate.value + ' animated']">
{{ animate.label }}
</div>
</div>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
Анимация Click позвонитaddAnimation(animate)
Добавить анимацию в компонентanimations
множество.
запустить анимацию
Код для запуска анимации:
export default async function runAnimation($el, animations = []) {
const play = (animation) => new Promise(resolve => {
$el.classList.add(animation.value, 'animated')
const removeAnimation = () => {
$el.removeEventListener('animationend', removeAnimation)
$el.removeEventListener('animationcancel', removeAnimation)
$el.classList.remove(animation.value, 'animated')
resolve()
}
$el.addEventListener('animationend', removeAnimation)
$el.addEventListener('animationcancel', removeAnimation)
})
for (let i = 0, len = animations.length; i < len; i++) {
await play(animations[i])
}
}
Для запуска анимации требуются два параметра: элемент DOM, соответствующий компоненту (используется в компонентеthis.$el
fetch) и его данные анимацииanimations
. и нужно следитьanimationend
события иanimationcancel
События: одно запускается, когда анимация заканчивается, другое запускается, когда анимация неожиданно завершается.
Воспользуйтесь этимPromise
При совместном использовании каждую анимацию компонента можно запускать отдельно.
12. Импорт PSD
Из-за нехватки времени я еще не сделал эту функцию. Теперь кратко опишем, как сделать эту функцию. то есть использоватьpsd.jsБиблиотека, которая может анализировать файлы PSD.
использоватьpsd
Данные из библиотеки, разбирающей файл PSD, следующие:
{ children:
[ { type: 'group',
visible: false,
opacity: 1,
blendingMode: 'normal',
name: 'Version D',
left: 0,
right: 900,
top: 0,
bottom: 600,
height: 600,
width: 900,
children:
[ { type: 'layer',
visible: true,
opacity: 1,
blendingMode: 'normal',
name: 'Make a change and save.',
left: 275,
right: 636,
top: 435,
bottom: 466,
height: 31,
width: 361,
mask: {},
text:
{ value: 'Make a change and save.',
font:
{ name: 'HelveticaNeue-Light',
sizes: [ 33 ],
colors: [ [ 85, 96, 110, 255 ] ],
alignment: [ 'center' ] },
left: 0,
top: 0,
right: 0,
bottom: 0,
transform: { xx: 1, xy: 0, yx: 0, yy: 1, tx: 456, ty: 459 } },
image: {} } ] } ],
document:
{ width: 900,
height: 600,
resources:
{ layerComps:
[ { id: 692243163, name: 'Version A', capturedInfo: 1 },
{ id: 725235304, name: 'Version B', capturedInfo: 1 },
{ id: 730932877, name: 'Version C', capturedInfo: 1 } ],
guides: [],
slices: [] } } }
Как видно из приведенного выше кода, эти данные очень похожи на css. В соответствии с этим нам нужно только написать функцию преобразования для преобразования этих данных в данные, требуемые нашим компонентом, а затем мы можем реализовать функцию преобразования файлов PSD в компоненты рендеринга. В настоящее времяquark-h5а такжеluban-h5Так реализована функция преобразования PSD.
13. Режим телефона
Поскольку размер холста можно изменять, мы можем использовать разрешение iphone6 для разработки мобильных страниц.
Страницы, разработанные таким образом, также можно нормально просматривать на мобильных телефонах, но возможны отклонения в стиле. Поскольку три компонента, которые я настроил, не адаптированы, если вам нужно разработать мобильные страницы, пользовательские компоненты должны использовать библиотеку компонентов мобильного пользовательского интерфейса. Или разработайте свои собственные компоненты, предназначенные для мобильного терминала.
Суммировать
Поскольку в DEMO много кода, при объяснении каждой функциональной точки я вставляю только код ключа. Таким образом, каждый обнаружит, что исходный код DEMO и код, который я разместил, будут немного отличаться, пожалуйста, не обращайте внимания.
Кроме того, стиль DEMO также относительно прост, в основном потому, что в последнее время было много всего, и у меня не так много времени, чтобы написать лучше, пожалуйста, простите меня.
Визуальное перетаскивание серии статей:
- Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания
- Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания (2)
- Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания (3)