可视化拖拽组件库一些技术要点原理分析(二)(下)

简介: 可视化拖拽组件库一些技术要点原理分析(二)(下)

自动吸附

自动吸附是根据组件的四个属性 topleftwidthheight 计算的,在将组件进行旋转后,这些属性的值是不会变的。所以无论组件旋转多少度,吸附时仍然按未旋转时计算。这样就会有一个问题,虽然实际上组件的 topleftwidthheight 属性没有变化。但在外观上却发生了变化。下面是两个同样的组件:一个没旋转,一个旋转了 45 度。

可以看出来旋转后按钮的 height 属性和我们从外观上看到的高度是不一样的,所以在这种情况下就出现了吸附不正确的 BUG。

解决方案

如何解决这个问题?我们需要拿组件旋转后的大小及位移来做吸附对比。也就是说不要拿组件实际的属性来对比,而是拿我们看到的大小和位移做对比。

从上图可以看出,旋转后的组件在 x 轴上的投射长度为两条红线长度之和。这两条红线的长度可以通过正弦和余弦算出,左边的红线用正弦计算,右边的红线用余弦计算:

const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)

同理,高度也是一样:

const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)

新的宽度和高度有了,再根据组件原有的 topleft 属性,可以得出组件旋转后新的 topleft 属性。下面附上完整代码:

translateComponentStyle(style) {
    style = { ...style }
    if (style.rotate != 0) {
        const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
        const diffX = (style.width - newWidth) / 2
        style.left += diffX
        style.right = style.left + newWidth
        const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
        const diffY = (newHeight - style.height) / 2
        style.top -= diffY
        style.bottom = style.top + newHeight
        style.width = newWidth
        style.height = newHeight
    } else {
        style.bottom = style.top + style.height
        style.right = style.left + style.width
    }
    return style
}

经过修复后,吸附也可以正常显示了。

光标

光标和可拖动的方向不对,是因为八个点的光标是固定设置的,没有随着角度变化而变化。

解决方案

由于 360 / 8 = 45,所以可以为每一个方向分配 45 度的范围,每个范围对应一个光标。同时为每个方向设置一个初始角度,也就是未旋转时组件每个方向对应的角度。

pointList: ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'], // 八个方向
initialAngle: { // 每个点对应的初始角度
    lt: 0,
    t: 45,
    rt: 90,
    r: 135,
    rb: 180,
    b: 225,
    lb: 270,
    l: 315,
},
angleToCursor: [ // 每个范围的角度对应的光标
    { start: 338, end: 23, cursor: 'nw' },
    { start: 23, end: 68, cursor: 'n' },
    { start: 68, end: 113, cursor: 'ne' },
    { start: 113, end: 158, cursor: 'e' },
    { start: 158, end: 203, cursor: 'se' },
    { start: 203, end: 248, cursor: 's' },
    { start: 248, end: 293, cursor: 'sw' },
    { start: 293, end: 338, cursor: 'w' },
],
cursors: {},

计算方式也很简单:

  1. 假设现在组件已旋转了一定的角度 a。
  2. 遍历八个方向,用每个方向的初始角度 + a 得出现在的角度 b。
  3. 遍历 angleToCursor 数组,看看 b 在哪一个范围中,然后将对应的光标返回。

经常上面三个步骤就可以计算出组件旋转后正确的光标方向。具体的代码如下:

getCursor() {
    const { angleToCursor, initialAngle, pointList, curComponent } = this
    const rotate = (curComponent.style.rotate + 360) % 360 // 防止角度有负数,所以 + 360
    const result = {}
    let lastMatchIndex = -1 // 从上一个命中的角度的索引开始匹配下一个,降低时间复杂度
    pointList.forEach(point => {
        const angle = (initialAngle[point] + rotate) % 360
        const len = angleToCursor.length
        while (true) {
            lastMatchIndex = (lastMatchIndex + 1) % len
            const angleLimit = angleToCursor[lastMatchIndex]
            if (angle < 23 || angle >= 338) {
                result[point] = 'nw-resize'
                return
            }
            if (angleLimit.start <= angle && angle < angleLimit.end) {
                result[point] = angleLimit.cursor + '-resize'
                return
            }
        }
    })
    return result
},

从上面的动图可以看出来,现在八个方向上的光标是可以正确显示的。

15. 复制粘贴剪切

相对于拖拽旋转功能,复制粘贴就比较简单了。

const ctrlKey = 17, vKey = 86, cKey = 67, xKey = 88
let isCtrlDown = false
window.onkeydown = (e) => {
    if (e.keyCode == ctrlKey) {
        isCtrlDown = true
    } else if (isCtrlDown && e.keyCode == cKey) {
        this.$store.commit('copy')
    } else if (isCtrlDown && e.keyCode == vKey) {
        this.$store.commit('paste')
    } else if (isCtrlDown && e.keyCode == xKey) {
        this.$store.commit('cut')
    }
}
window.onkeyup = (e) => {
    if (e.keyCode == ctrlKey) {
        isCtrlDown = false
    }
}

监听用户的按键操作,在按下特定按键时触发对应的操作。

复制操作

在 vuex 中使用 copyData 来表示复制的数据。当用户按下 ctrl + c 时,将当前组件数据深拷贝到 copyData

copy(state) {
    state.copyData = {
        data: deepCopy(state.curComponent),
        index: state.curComponentIndex,
    }
},

同时需要将当前组件在组件数据中的索引记录起来,在剪切中要用到。

粘贴操作

paste(state, isMouse) {
    if (!state.copyData) {
        toast('请选择组件')
        return
    }
    const data = state.copyData.data
    if (isMouse) {
        data.style.top = state.menuTop
        data.style.left = state.menuLeft
    } else {
        data.style.top += 10
        data.style.left += 10
    }
    data.id = generateID()
    store.commit('addComponent', { component: data })
    store.commit('recordSnapshot')
    state.copyData = null
},

粘贴时,如果是按键操作 ctrl+v。则将组件的 topleft 属性加 10,以免和原来的组件重叠在一起。如果是使用鼠标右键执行粘贴操作,则将复制的组件放到鼠标点击处。

剪切操作

cut(state) {
    if (!state.curComponent) {
        toast('请选择组件')
        return
    }
    if (state.copyData) {
        store.commit('addComponent', { component: state.copyData.data, index: state.copyData.index })
        if (state.curComponentIndex >= state.copyData.index) {
            // 如果当前组件索引大于等于插入索引,需要加一,因为当前组件往后移了一位
            state.curComponentIndex++
        }
    }
    store.commit('copy')
    store.commit('deleteComponent')
},

剪切操作本质上还是复制,只不过在执行复制后,需要将当前组件删除。为了避免用户执行剪切操作后,不执行粘贴操作,而是继续执行剪切。这时就需要将原先剪切的数据进行恢复。所以复制数据中记录的索引就起作用了,可以通过索引将原来的数据恢复到原来的位置中。

右键操作

右键操作和按键操作是一样的,一个功能两种触发途径。

<li @click="copy" v-show="curComponent">复制</li>
<li @click="paste">粘贴</li>
<li @click="cut" v-show="curComponent">剪切</li>
cut() {
    this.$store.commit('cut')
},
copy() {
    this.$store.commit('copy')
},
paste() {
    this.$store.commit('paste', true)
},

16. 数据交互

方式一

提前写好一系列 ajax 请求API,点击组件时按需选择 API,选好 API 再填参数。例如下面这个组件,就展示了如何使用 ajax 请求向后台交互:

<template>
    <div>{{ propValue.data }}</div>
</template>
<script>
export default {
    // propValue: {
    //     api: {
    //             request: a,
    //             params,
    //      },
    //     data: null
    // }
    props: {
        propValue: {
            type: Object,
            default: () => {},
        },
    },
    created() {
        this.propValue.api.request(this.propValue.api.params).then(res => {
            this.propValue.data = res.data
        })
    },
}
</script>

方式二

方式二适合纯展示的组件,例如有一个报警组件,可以根据后台传来的数据显示对应的颜色。在编辑页面的时候,可以通过 ajax 向后台请求页面能够使用的 websocket 数据:

const data = ['status', 'text'...]

然后再为不同的组件添加上不同的属性。例如有 a 组件,它绑定的属性为 status

// 组件能接收的数据
props: {
    propValue: {
        type: String,
    },
    element: {
        type: Object,
    },
    wsKey: {
        type: String,
        default: '',
    },
},

在组件中通过 wsKey 获取这个绑定的属性。等页面发布后或者预览时,通过 weboscket 向后台请求全局数据放在 vuex 上。组件就可以通过 wsKey 访问数据了。

<template>
    <div>{{ wsData[wsKey] }}</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
    props: {
        propValue: {
            type: String,
        },
        element: {
            type: Object,
        },
        wsKey: {
            type: String,
            default: '',
        },
    },
    computed: mapState([
        'wsData',
    ]),
</script>

和后台交互的方式有很多种,不仅仅包括上面两种,我在这里仅提供一些思路,以供参考。

17. 发布

页面发布有两种方式:一是将组件数据渲染为一个单独的 HTML 页面;二是从本项目中抽取出一个最小运行时 runtime 作为一个单独的项目。

这里说一下第二种方式,本项目中的最小运行时其实就是预览页面加上自定义组件。将这些代码提取出来作为一个项目单独打包。发布页面时将组件数据以 JSON 的格式传给服务端,同时为每个页面生成一个唯一 ID。

假设现在有三个页面,发布页面生成的 ID 为 a、b、c。访问页面时只需要把 ID 带上,这样就可以根据 ID 获取每个页面对应的组件数据。

www.test.com/?id=a

www.test.com/?id=c

www.test.com/?id=b

按需加载

如果自定义组件过大,例如有数十个甚至上百个。这时可以将自定义组件用 import 的方式导入,做到按需加载,减少首屏渲染时间:

import Vue from 'vue'
const components = [
    'Picture',
    'VText',
    'VButton',
]
components.forEach(key => {
    Vue.component(key, () => import(`@/custom-component/${key}`))
})

按版本发布

自定义组件有可能会有更新的情况。例如原来的组件使用了大半年,现在有功能变更,为了不影响原来的页面。建议在发布时带上组件的版本号:

- v-text
  - v1.vue
  - v2.vue

例如 v-text 组件有两个版本,在左侧组件列表区使用时就可以带上版本号:

{
  component: 'v-text',
  version: 'v1'
  ...
}

这样导入组件时就可以根据组件版本号进行导入:

import Vue from 'vue'
import componentList from '@/custom-component/component-list`
componentList.forEach(component => {
    Vue.component(component.name, () => import(`@/custom-component/${component.name}/${component.version}`))
})

参考资料

目录
相关文章
|
3月前
|
JavaScript
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
其实这个从零有点歧义,因为本质上是要基于`tdesign-vue-next`来进行二次封装为一套广告UI组件库,现在让我们在一起快乐的搭建自己的广告UI库之前,先对以下内容做出共识:
96 0
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
|
4月前
|
C# 开发者 数据处理
WPF开发者必备秘籍:深度解析数据网格最佳实践,轻松玩转数据展示与编辑大揭秘!
【8月更文挑战第31天】数据网格控件是WPF应用程序中展示和编辑数据的关键组件,提供排序、筛选等功能,显著提升用户体验。本文探讨WPF中数据网格的最佳实践,通过DevExpress DataGrid示例介绍其集成方法,包括添加引用、定义数据模型及XAML配置。通过遵循数据绑定、性能优化、自定义列等最佳实践,可大幅提升数据处理效率和用户体验。
68 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(二)(上)
可视化拖拽组件库一些技术要点原理分析(二)
110 1
|
数据可视化 API
可视化拖拽组件库一些技术要点原理分析(三)(二)
可视化拖拽组件库一些技术要点原理分析(三)(二)
108 0
|
数据可视化 索引
可视化拖拽组件库一些技术要点原理分析(二)
可视化拖拽组件库一些技术要点原理分析(二)
155 0
|
前端开发 C# 开发工具
Unity快手上手【熟悉unity编辑器,C#脚本控制组件一些属性之类的】
Unity快手上手【熟悉unity编辑器,C#脚本控制组件一些属性之类的】
160 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(四)(上)
可视化拖拽组件库一些技术要点原理分析(四)
97 0
可视化拖拽组件库一些技术要点原理分析(四)(上)
|
数据可视化 API 索引
可视化拖拽组件库一些技术要点原理分析(三)(三)
可视化拖拽组件库一些技术要点原理分析(三)(三)
142 0
|
数据可视化 API
可视化拖拽组件库一些技术要点原理分析(三)(一)
可视化拖拽组件库一些技术要点原理分析(三)
107 0
|
数据可视化 前端开发 JavaScript
可视化拖拽组件库一些技术要点原理分析(四)(下)
可视化拖拽组件库一些技术要点原理分析(四)(下)
78 0