💯前言
原插件vue3-grid-layout-picker
,因为一些原因改名为 vue3-draggable-grid
。
许多应用程序都需要实现可拖拽的布局以方便用户自定义布局。虽然这似乎是一个简单而基本的功能,但原生拖拽还存在许多问题,如兼容性、用户体验和可访问性等。此外,每次遇到这种需求时,手动实现可能较为麻烦且浪费时间。
因此,在本文中,我们将介绍如何使用 Vue 3
和 Typescript
开发一个简单的拖拽布局插件,以解决上述问题并巩固 Vue 3
的相关知识点。
🔱项目的环境依赖
"dependencies": {
"vue": "^3.2.37"
},
"devDependencies": {
"@types/node": "^18.11.4",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vitejs/plugin-vue": "^3.1.0",
"eslint": "^8.26.0",
"eslint-plugin-vue": "^9.9.0",
"typescript": "^4.9.5",
"vite": "^3.1.0",
"vite-plugin-dts": "^2.3.0",
"vue-tsc": "^0.40.4"
}
🤺插件的特性
- 可拖拽、可调整大小
- 网格吸附
- 碰撞检测
- 兼容移动端(采用
pointer
事件编写,兼容移动端) - 干净的依赖关系,除了
vue
没有其它的依赖项(也就意味着后面兼容其它框架会变得简单)。
🤖编写组件
以下组件设计的一些选择和决策:
- 使用
provide/inject
进行组件通信,而不是使用vuex
或者pinia
,以减少依赖项。 - 采用
grid
布局作为基础布局,但是方块中的布局使用定位实现,主要是为了实现缩放效果。 - 使用
canvas
进行网格线的绘制,因为暂时没有找到更好的绘制方法。同时,欢迎大家提出更好的建议。 - 子组件只需要一个
id
作为唯一的标识就可以实现数据关联(该灵感来源Element-Plus
的table
组件)。 - 事件使用
pointer
指针事件进行开发,完美适配移动端。
总之,这些选择和决策都是经过慎重考虑的,旨在提高代码质量和效率。如果读者有更好的建议或想法,欢迎提供反馈,共同探讨。
📦 安装
# 选择一个你喜欢的包管理器
# Npm
npm install vue3-draggable-grid --save
# Yarn
yarn add vue3-draggable-grid -D
# Pnpm
pnpm add vue3-draggable-grid -D
💡 用法
全量引入
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import Vue3DraggableGrid from 'vue3-draggable-grid'
import "vue3-draggable-grid-drop/dist/style.css"
createApp(App).use(Vue3DraggableGrid).mount('#app')
按需引入
// 组件中
import { GridLayout, GridItem } from 'vue3-draggable-grid'
import 'vue3-draggable-grid/dist/style.css'
组件中使用
<template>
<div class="layout-box">
<grid-layout
v-model:data="layout"
@draggableStart="draggableStart"
@draggableHandle="draggableHandle"
@draggableEnd="draggableEnd"
@remove="remove"
>
<grid-item v-for="item in layout" :key="item.id" :id="item.id">
{{ item.id }}
</grid-item>
</grid-layout>
</div>
</template>
<script setup lang="ts">
import { GridLayout, GridItem, type Layout, type LayoutItem } from 'vue3-draggable-grid'
import 'vue3-draggable-grid/dist/style.css'
import { ref, watch } from 'vue'
const layout = ref<Layout>([
{ id: '1', x: 1, y: 1, h: 1, w: 1 },
{ id: '2', x: 2, y: 1, h: 1, w: 1 },
{ id: '3', x: 3, y: 1, h: 1, w: 1 },
{ id: '4', x: 4, y: 1, h: 1, w: 1 },
{ id: '5', x: 1, y: 2, h: 1, w: 1 },
{ id: '6', x: 1, y: 3, h: 1, w: 1 },
{ id: '7', x: 1, y: 4, h: 1, w: 1 },
{ id: '8', x: 1, y: 5, h: 4, w: 1 },
{ id: '9', x: 2, y: 2, h: 1, w: 1 },
{ id: '10', x: 2, y: 3, h: 1, w: 1 },
{ id: '11', x: 2, y: 4, h: 1, w: 1 },
{ id: '12', x: 5, y: 5, h: 1, w: 2 },
])
// 验证更新数据是否正确
watch(layout, () => {
console.log('数据更新', layout.value)
}, {deep: true})
const draggableStart = (id: string) => {
console.log('拖拽开始', id)
}
const draggableHandle = (id: string, data: LayoutItem) => {
console.log('拖拽中', id, data)
}
const draggableEnd = (data: Layout) => {
console.log('拖拽结束', data)
}
const remove = (id: string) => {
console.log('删除', id)
}
</script>
<style>
.layout-box {
width: 1000px;
}
</style>
这里需要注意的一点是,在组件的外层或者组件本身需要指定宽度,不然宽度会计算为0
🎁 Apis
参数类型
interface LayoutItem {
id: string
x: number
y: number
h: number
w: number
static?: boolean
}
type Layout = LayoutItem[]
GridLayout
Props
interface PropsData {
data: Layout // 布局数据
col?: number | string, // 列数
rowH?: number | string // 行高
gutter?: number | string // 网格间距
drage?: boolean // 是否可拖拽
resize?: boolean // 是否可拖拽
remove?: boolean // 是否可拖拽
isDrawGridLines?: boolean // 是否绘制网格线
isCollision?: boolean // 是否碰撞
}
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | Layout | [] | 布局数据,支持双向绑定(v-model:data="layoutData") |
col | number/string | 12 | 列数 |
rowH | number/string | 50 | 行高 |
gutter | number/string | 10 | 网格间距 |
drage | boolean | true | 是否可拖拽 |
resize | boolean | true | 是否可缩放 |
remove | boolean | true | 是否可删除 |
isDrawGridLines | boolean | true | 是否绘制网格线 |
isCollision | boolean | true | 是否碰撞 |
🪢 事件
事件名 | 说明 | 类型 |
---|---|---|
draggableStart | 拖拽开始时触发 | (index: string) => void |
draggableHandle | 拖拽中触发 | (id: string, newItem: LayoutItem) => void |
draggableEnd | 拖拽结束时触发 | (id: string, newItem: LayoutItem) => void |
remove | 删除方块时触发 | (index: string) => void |
GridItem
Props
interface GridItem {
id: string // 唯一标识
draggableFrom?: string // 拖拽源,触发拖拽的元素id
}
名称 | 类型 | 默认值 | 说明 |
---|---|---|---|
id | string | `` | 子元素的唯一标识 |
draggableFrom | string | `` | 触发拖拽的元素id |
🎍 插槽
名称 | 说明 |
---|---|
default | 自定义每个元素的内容 |
resize | 自定义缩放 |
remove | 自定义删除 |
🎡待办事项
- 拖拽算法优化(是否需要多种算法兼容去计算布局呢?)
- 初始化布局数据计算算法优化(添加紧凑布局?、限定宽高时元素超出范围的处理方法)
- 缩放、删除的图标自适应大小(兼容各种屏幕)
- 还有一些功能暂时没想到,希望大家提出意见跟批评
结语
欢迎大家试用,并提出问题。
博客主要记录一些学习的文章,如有不足,望大家指出,谢谢。