Практика с вами, чтобы написать визуальный редактор страниц Vue с перетаскиванием

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

онлайн-адрес(С лестницей будет быстрее)

Визуальный редактор страниц звучит как мечта, не так ли? Давайте сначала взглянем на анимацию!

Прежде чем реализовать эту функцию, я перерыл много информации в Интернете, но в итоге ничего не нашел, а во всевозможных статьях говорилось о моем прежнем «я»!

Итак, в это время вам нужно подумать о том, как этого добиться?

Необходимо учитывать:

  • Перетащите реализацию
  • Определение структуры данных
  • Разделение компонентов
  • Ремонтопригодность и расширяемость

ссылка на объект: Вот самая крутая техника, которую я чувствую, давайте объясним детали по порядку! !

Перетащите реализацию

событие перетаскивания

использовать здесьСобытие перетаскивания H5, в основном используется:

dragstart   // 开始拖拽一个元素时触发
draggable   // 指定可被拖拽的元素
dragend     // 当拖拽操作结束时触发
dragover    // 当拖拽元素在可释放目标上移动时触发
drop        // 当拖拽元素在可释放目标上被释放时触发

Давайте посмотрим, как использовать эти события:

<!-- 拖拽元素列表数据 -->
<script>
// com 为对应的视图组件,在释放区域显示
typeList: {
    banner: {
        name: '轮播图',
        icon: 'el-icon-picture',
        com: Banner
    },
    product: {
        name: '商品',
        icon: 'el-icon-s-goods',
        com: Product
    },
    images: {
        name: '图片',
        icon: 'el-icon-picture',
        com: Images
    },
}
</script>
<!-- 拖拽元素 -->
<ul 
    @dragstart="dragStart"
    @dragend="dragEnd"
>
    <li 
        v-for="(val, key, index) in typeList"
        draggable 
        :data-type="key"
        :key="index + 1"
    >
        <span :class="val.icon"></span>
        <p>{{val.name}}</p>
    </li>
</ul>
<!-- 释放区域 -->
<div 
    class="view-content"
    @drop="drog"
    @dragover="dragOver"
>
</div>

перетащите, чтобы начать

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

// 拖拽类型
dragStart(e) {
    this.type = e.target.dataset.type
}

После определения типа войдите в следующую область выпуска

двигаться в зоне освобождения

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

// 'view-content': 外层盒子的class,直接 push
// 'item': 盒子内部的元素,需计算位置,进行变换操作
dragOver() {
    let className = e.target.className
    let name = className !== 'view-content' ? 'item' : 'view-content'

    // 组件的默认数据
    const defaultData = {
        type: this.type,    // 组件类型
        status: 2,          // 默认状态
        data: [],           // 基本数据
        options: {}         // 其他操作
    }

    if (name == 'view-content') {
        //...
    } else if (name == 'item') {
        //...
    }
}

Обработка границ, расчет углов

Основные переменные:

  • isPush: Был ли перетаскиваемый элемент перемещен в данные страницы.
  • index: конечное значение индекса перетаскиваемого элемента.
  • curIndex: значение индекса элемента, в котором находится мышь.
  • direction: верхняя/нижняя часть элемента, над которым находится мышь

когдаname=='view-content', указывающий, что перетаскиваемый элемент находится во внешней и пустой освобождаемой области, если он не добавлен, напрямуюpushПросто

if (name == 'view-content') {
    if (!this.isPush) {
        this.index = this.view.length
        this.isPush = true
        this.view.push(defaultData)
    }
}

когдаname=='item', то есть над существующим элементом нужно вычислить положение, вверх/вниз, добавить или переместить

if (name == 'item') {
    let target = e.target
    let [ y, h, curIndex ] = [ e.offsetY, target.offsetHeight, target.dataset.index ]
    let direction = y < (h / 2) // 计算鼠标处于当前元素的位置,来决定拖拽元素的上/下

    if (!this.isPush) {
        // first
        if (direction) {
            if (curIndex == 0) {
                this.view.unshift(defaultData)
            } else {
                this.view.splice(curIndex, 0, defaultData)
            }
        } else {
            curIndex = +curIndex + 1
            this.view.splice(curIndex, 0, defaultData)
        }
    } else {
        // Moving
        if (direction) {
            var i = curIndex == 0 ? 0 : curIndex - 1
            var result = this.view[i]['status'] == 2
        } else {
            var i = +curIndex + 1
            var result = this.view.length > i && this.view[i]['status'] == 2
        }
        
        // 拖拽元素是否需变换位置
        if (result) return

        const temp = this.view.splice(this.index, 1)
        this.view.splice(curIndex, 0, temp[0])
    }
    this.index = curIndex   // 拖拽元素位置
    this.isPush = true      // 进入则push,即true
}
  • first:еще нетpush, то по текущемуindexиdirectionчтобы определить положение перетаскиваемого элемента
  • Moving:ужеpushА мобильное состояние, по текущимindexиdirectionузнать состояние соответствующего значения, является ли он перетаскиваемым элементом, и если даreturn, иначе изменить положение

в заключении:Получить индекс элемента, в котором находится текущая мышь, а затем вычислить элемент, в котором находится мышь.上半部分все еще下半部分, чтобы определить положение перетаскиваемого элемента! ! !

Кусок пирога:

вышеname=='item',EventСобытия должны предотвращать события по умолчанию, избегатьtargetДля внутреннего элемента позицию нельзя вычислить, а только использовать событие для предотвращения здесь не получится, и я не знаю почему, нужно поставить.itemВсе дочерние элементы плюсаpointer-events: noneхарактеристики!

e.preventDefault()
e.stopPropagation()

.item div{
    pointer-events: none;
}

конец перетаскивания

То есть отпустите мышь или покиньте область отпускания, состояние по умолчанию будет восстановлено.

здесьstatusЧто это делает

  1. Приведенные выше правила расчета используются для определения того, является ли элемент перетаскиваемым.
  2. Метод отображения страницы, при перетаскивании отображается только имя компонента, а после отпускания восстанавливается нормальное отображаемое содержимое.
// 结束拖拽
dragEnd(e) {
    this.$delete(this.view[this.index], 'status')
    this.isPush = false
    this.type = null
},
// 已放置到指定位置
drog(e) {
    e.preventDefault()
    e.stopPropagation()
    this.dragEnd()
},

Реализация перетаскивания блока контента

Из-за нехватки времени я здесь ленив и использую более совершенный плагин для перетаскивания списка.Vue.Draggable (star 14.2k)

После некоторого исследования его логика связана с перетаскиванием, реализованным выше, и конкретные методы реализации аналогичны Я считаю, что с приведенными выше реальными боевыми случаями вы тоже можете это сделать!

Почему бы тебе не попробовать?

может основываться наVue.DraggableКак использовать для реализации компонента перетаскивания, который будет использовать (перетаскивание, слот, DOM) и другие операции.

(Есть время позже, я вернусь и упакую один)

Разделение компонентов

Компонент среднего вида и компонент редактирования справа представляют собой набор наборов. Это действительно набор наборов. Это действительно набор наборов!

page=>indexуправляет содержимым всей страницы

.
├── components
|   ├── Edit           ## 右边编辑
|   |    ├── Info       # 基本信息
|   |    ├── Image      # 广告图
|   |    ├── Product    # 商品
|   |    └── Index      # 管理编辑组件的信息
|   └── View           ## 中间视图
|   |    ├── Banner     # 轮播图
|   |    ├── Images     # 广告图
|   |    └── Product    # 产品列表
└── page
    └── index          ## 主页面

Для достижения эффекта предварительного просмотра страницы используйте непосредственноcomponents=>ViewСледующие компоненты могут использоваться сpage=>indexИспользуйте тот же метод, не нужно слишком много модифицировать!

Определение структуры данных

Чтобы получить яркую и расширяемую функцию, необходимо определить квалифицированную структуру данных! В то же время он также может определять качество вашего кода!

Конечно, вам все равно придется решать путем собственного обучения и логического мышления!

Вот самый очевидный способ справиться с этим:Заимствуя отношения объектов, передача значения между компонентами должна передаваться только в одном направлении!

view: [
    {
        type: 'info',
        title: '页面标题',
        remarks: '页面备注',
        backgroundColor: 'red'
    },
    {
        type: 'banner',
        data: [
            { url: '1.jpg', name: '轮播图1', link: 'https://轮播图跳转地址.cn' },
            { url: '2.jpg', name: '轮播图2', link: 'https://轮播图跳转地址.cn' }
        ]
    },
    {
        type: 'images',
        data: [
            { url: '1.jpg', name: '广告图1', link: 'https://广告图跳转地址.cn' },
            { url: '2.jpg', name: '广告图2', link: 'https://广告图跳转地址.cn' }
        ]
    },
    {
        type: 'product',
        data: [
            { id: '1', name: '商品1', image: '1.jpg' }, 
            { id: '2', name: '商品2', image: '2.jpg' }
        ],
        options: {
            originalPrice: true,    // 划线价
            goodRatio: true,        // 好评率
            volumeStr: false,       // 销量数
        }
    }
]

это массив, массивitemпредставляет собой модуль

  • type: тип модуля
  • data:Основная информация
  • options: другие операции

.... Вы можете обратиться к оригинальным модулям компонентов и расширить их в соответствии с вашими потребностями.

Изменить передачу компонента по значению

При выборе компонента представления поместитеviewуказано вitemОбъект передается в качестве параметра компоненту редактирования!

Объект указывает на один и тот же адрес памяти, и существует ссылочное отношение, и разнонаправленное обновление данных может быть достигнуто только путем его изменения один раз!

<section class="r">
    <EditForm
        :data="props"
        v-if="isRight"
    ></EditForm>
</section>
<script>
// 切换视图组件
selectType(index) {
    this.isRight = false
    this.props = this.view[index]
    this.$nextTick(() => this.isRight = true)
}
</script>

загрузить изображение

Просто на нем есть компонент для загрузки изображений, здесь я делюсь своими навыками использования! !

использоватьElement-uiДрузья, которые приносят свои компоненты загрузки, приходите сюда (стучите по доске)

Давайте сначала реализуем упрощенную версию:

<!-- 禁用所有默认方法 -->
<el-upload
    :http-request="upload"
    :show-file-list="false"
    multiple
    action
>
    <img :src="item.url" v-for="(item, index) in list" :key="index">
</el-upload>
<script>
upload(params) {
    const file = params.file;
    const form = new FormData();
    form.append("file", file);
    form.append("clientType", "multipart/form-data");

    const index = this.imageIndex   // 编辑图片的索引
    const data = { 
        url: URL.createObjectURL(file), 
        form
    }
    if (index !== null) {
        // this.list => 图片集合
        this.$set(this.list, index, data)
    } else {
        this.list.push(data)
    }
}
</script>
  • Переопределить метод загрузки
  • использоватьURL.createObjectURL(file)Создайте локальный адрес предварительного просмотра
  • ПучокformСохраните объект и загрузите его при отправке
// 根据上面的代码,使用Promise实现上传功能
const request = []
this.list.forEach(item => {
    request.push(
        new Promise((resolve, reject) => {
            /**
             * 上传接口
             * 替换原 url
             * 删除 form
             */
            imageUpload(item.form).then(res => {
                item.url = res.data.url
                delete item.form
                resolve(res)
            }).catch(err => {
                reject(err)
            })
        })
    )
})
Promise.all(request).then(res => {
    // ... submit ...
})

Когда данные будут отправлены на последнем шаге, загрузите все картинки, а затем вызовите интерфейс для отправки данных после завершения загрузки! !

Это самый правильный подход в ситуации, когда отправляется несколько форм!

окончательное резюме

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

Другие, такие как добавление компонентов, добавление операций и другие операции расширения, остальные проблемы больше не проблема!

Это можно рассматривать только как короткую версию, вы можете оптимизировать, обдумывать и улучшать в соответствии с вашими потребностями и впитывать собственные знания!

По крайней мере, я удовлетворил потребности своей работы, вау, хахахаха~~~

Для получения более подробной информации, пожалуйста, проверьте исходный код, адрес Github, чтобы предложить, спасибо за вашу звезду, я Ли Бай, который не ест чай.