Принципиальный анализ некоторых технических моментов библиотеки компонентов визуального перетаскивания

внешний интерфейс JavaScript

В этой статье в основном анализируются следующие технические моменты:

  1. редактор
  2. пользовательский компонент
  3. тянуть
  4. Удалить компоненты, настроить уровни слоев
  5. приблизить
  6. отменить повторить
  7. Настройки свойств компонента
  8. адсорбция
  9. Предварительный просмотр и сохранение кода
  10. привязать событие
  11. Связать анимацию
  12. Импорт PSD
  13. режим телефона

Чтобы сделать эту статью более понятной, я объединил приведенные выше технические моменты, чтобы написать ДЕМО-версию визуальной библиотеки компонентов перетаскивания:

Рекомендуется читать вместе с исходным кодом, эффект лучше (в этом DEMO используется стек технологий Vue).

1. Редактор

Давайте сначала посмотрим на общую структуру страницы.

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

Идея реализации этого редактора такова:

  1. использовать массивcomponentDataСохраняйте данные в редакторе.
  2. При перетаскивании компонента на холст используйтеpush()метод для добавления новых данных компонента вcomponentData.
  3. использование редактора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Атрибуты. Кроме того, при перетаскивании компонентов в списке компонентов на холст есть два других события, играющих ключевую роль:

  1. dragstartСобытие, срабатывающее при начале перетаскивания. Он в основном используется для передачи информации о перетаскиваемом компоненте на холст.
  2. 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. В дополнение к этому движение производится путем прослушивания трех событий:

  1. mousedownСобытие, когда мышь нажимает на компонент, записывает текущую позицию компонента, то есть координаты xy (для удобства пояснения используемая здесь ось координат фактически соответствует xy в css.leftа такжеtop.
  2. mousemoveСобытие: каждый раз, когда мышь перемещается, вычтите первую координату xy из текущей последней координаты xy, чтобы вычислить расстояние перемещения, а затем измените положение компонента.
  3. 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компоненты. Таким образом, положение четырех маленьких точек хорошо установлено:

  1. Маленькая точка в верхнем левом углу, координатыleft: 0, top: 0
  2. Маленькая точка в правом верхнем углу, координатыleft: width, top: 0
  3. Маленькая точка в левом нижнем углу, координатыleft: 0, top: height
  4. Маленькая точка в правом нижнем углу, координаты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)
}

Его принцип таков:

  1. При щелчке по маленькой точке записываются координаты xy щелчка.
  2. Предполагая, что теперь мы перетаскиваем вниз, координата y увеличится.
  3. Вычтите исходную координату y из новой координаты y, и вы сможете узнать, какое расстояние составляет перемещение в направлении вертикальной оси.
  4. Наконец, добавьте расстояние перемещения к высоте исходного компонента, чтобы получить новую высоту компонента.
  5. Если это положительное число, это означает, что он тянет вниз, а высота компонента увеличивается. Если это отрицательное число, это означает, что он подтягивается, а высота компонента уменьшается.

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 к индексу снимка, а затем назначьте соответствующие данные снимка холсту.

Однако следует отметить еще один момент, то есть во время операции отмены выполняется новая операция, что мне делать? Есть два решения:

  1. Новая операция заменяет все данные, следующие за индексом текущего моментального снимка. Все еще использовать данные только сейчас[a, b, c, d]Например, предположим, что сейчас выполняются две операции отмены, индекс моментального снимка становится равным 1, а соответствующие данные моментального снимка — b.Если в это время выполняется новая операция, соответствующие данные моментального снимка — e. Затем e удалит компакт-диск, и текущие данные снимка будут[a, b, e].
  2. Не сбрасывайте данные и добавьте новую запись к исходному снимку. Используя пример только что, e не будет удалять компакт-диск, а вставит его перед компакт-диском, то есть данные снимка станут[a, b, e, c, d].

Я использовал первый вариант.

7. Адсорбция

Что такое адсорбция? То есть при перетаскивании компонента, если он находится рядом с другим компонентом, он автоматически соединится вместе.

Адсорбированный код составляет около 300 строк, для просмотра рекомендуется открыть исходный файл (путь к файлу:src\components\Editor\MarkLine.vue). Я не буду выкладывать сюда код, в основном рассказываю о том, как реализован принцип.

Маркировка

Создайте на странице 6 линий, три горизонтальные и три вертикальные. Цель этих 6 линий - выровнять, когда они появятся?

  1. Вертикальные линии появятся, когда два компонента в верхнем и нижнем направлениях выровнены слева, посередине и справа.
  2. Горизонтальные линии появятся, когда верхняя, средняя и нижняя части двух компонентов в левом и правом направлениях выровнены.

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

  1. Компонент а не перемещается влево, и мы перетаскиваем компонент b, чтобы приблизиться к компоненту а.
  2. В это время они сначала выравниваются справа от a и слева от b, поэтому нужна только одна линия.
  3. Если компонент ab уже близок, а компонент b продолжает двигаться влево, то необходимо определить, совмещена ли середина из двух.
  4. Компонент 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Вот и все. Сохранить иметь два варианта:

  1. сохранить на сервер
  2. сохранить локально

В 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.$elfetch) и его данные анимации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 также относительно прост, в основном потому, что в последнее время было много всего, и у меня не так много времени, чтобы написать лучше, пожалуйста, простите меня.

Визуальное перетаскивание серии статей:

использованная литература