Code前端首页关于Code前端联系我们

Vue3 Grid Layout 怎么用才不踩坑?响应式拖拽缩放自由布局这几点没人告诉你吧?

terry 1小时前 阅读数 15 #Vue

你是不是刚上手Vue3做管理后台、个性化仪表盘、电商装修页这类需要自由布局的项目,搜到一堆教程但要么版本不对要么讲得太浅,改完一放大缩小或者换设备就全乱?甚至连组件安装都有坑?别慌,我上个月刚用它完成了两个SaaS平台的仪表盘模块,踩过的坑能攒一小箩筐,今天就用实操经验给你理明白从安装到优化的全流程,还有三个教程里很少提但超实用的进阶技巧。

第一步先搞懂:Vue3 Grid Layout 到底是啥,它和传统布局有啥区别?

很多人第一次搜这个组件,看到Grid、Layout就以为是CSS Grid的Vue封装——其实完全不是!CSS Grid是静态的、规则式的网格布局,只能靠预设的行列和响应式断点调整,根本做不了用户随意拖拽组件、调整大小、保存布局这种交互;而Vue3 Grid Layout是基于JavaScript计算+DOM操作实现的自由响应式网格布局组件,它的核心是给每个可拖拽缩放的元素分配“网格坐标”(行、列、宽高占的网格数),不管用户怎么操作,组件都会自动贴合网格对齐,不会出现上下左右歪歪扭扭的情况,而且可以非常轻松地把用户自定义的布局存到数据库,下次打开直接还原。

现在主流的Vue3自由布局组件里,它的生态最成熟,更新也比较稳定——不像有些小组件用着用着就没人维护了,不过这里要注意第一个没人直接说的小细节:它有两个分支!一个是最早的vue-grid-layout,官方适配的是Vue2;另一个是vue3-grid-layout(或者叫@jamesdiane/vue3-grid-layout,不过现在很多维护好的库直接用vue3-grid-layout),专门给Vue3用的,千万不要装错,不然会报一堆Vue2/Vue3语法不兼容的错误,连基础Demo都跑不起来。

第二步基础安装配置:避坑第一步,选对正确的包,搞懂核心配置项

刚才说了分支的问题,那具体怎么装呢?打开你的终端,直接输入这行命令就行:

npm install vue3-grid-layout --save

或者用yarn/pnpm也可以,这个包是目前GitHub上star最多的Vue3适配版本,功能和Vue2的原版几乎一致,还修复了原版一些小bug。

装完之后,先别急着写代码,先搞懂它的几个核心必须配置项,不然你的布局要么拖不动,要么缩不了,要么占不满页面:

核心容器配置项(GridLayout组件)

  1. col-num:整个布局容器横向分成多少个网格,默认是12,这个要根据你的设计稿来,比如管理后台侧边栏占2格、内容区占10格,那可以设成12;如果是电商装修页手机端,设成6或者8比较合适;
  2. row-height:每个网格的高度是多少px,默认是150,这个也可以调整,不过要注意:行高决定了整个布局的高度计算逻辑,比如某个组件占2行,那它的高度就是row-height 2 + gap (2-1);
  3. is-draggable:整个容器里的组件能不能拖拽,布尔值,默认是true;
  4. is-resizable:整个容器里的组件能不能缩放,布尔值,默认是true;
  5. layout:核心中的核心!这是一个数组,每个元素对应一个可拖拽缩放的子组件,里面必须包含i(唯一标识符)、x(横向网格起始坐标,从0开始)、y(纵向网格起始坐标,从0开始)、w(横向占的网格数)、h(纵向占的网格数)这五个属性,其他属性比如minW、maxW、minH、maxH是可选的,用来限制组件的最小最大宽高;
  6. vertical-compact:布尔值,默认是true,意思是组件会不会自动“向上吸附”填补空白——比如你把上面的一个组件拖走了,下面的组件如果有空位就会自动填上去;如果是做电商装修页这种需要固定顺序的,这个要设成false!

核心子组件配置项(GridItem组件)

GridItem组件其实没啥必须配置项,你只需要给它套在你要布局的内容外面就行,不过要注意:GridItem的i属性必须和layout数组里对应元素的i属性完全一致!不然会报错组件找不到对应的数据。

第三步快速跑通一个小Demo:从静态布局到可拖拽缩放

现在给你写一个最简单但功能完整的Demo,你可以直接复制到你的项目里试试: 在你的Vue3组件(比如Dashboard.vue)里引入GridLayout和GridItem:

<script setup>
import { ref } from 'vue'
import { GridLayout, GridItem } from 'vue3-grid-layout'
import 'vue3-grid-layout/dist/style.css' // 必须引入样式,不然组件会显示成方块块!
// 定义核心布局数据
const layout = ref([
  { i: 'chart1', x: 0, y: 0, w: 6, h: 2, minW: 3, minH: 1 },
  { i: 'chart2', x: 6, y: 0, w: 6, h: 2, minW: 3, minH: 1 },
  { i: 'table1', x: 0, y: 2, w: 12, h: 3, minW: 6, minH: 2 },
])
</script>

然后写模板部分:

<template>
  <div class="dashboard-container">
    <h2>我的个性化仪表盘</h2>
    <GridLayout
      :col-num="12"
      :row-height="100"
      :is-draggable="true"
      :is-resizable="true"
      :vertical-compact="true"
      :layout="layout"
      class="grid-container"
    >
      <GridItem v-for="item in layout" :key="item.i" :i="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :minW="item.minW" :minH="item.minH">
        <!-- 这里放你自己的内容,比如ECharts图表、表格、卡片 -->
        <div class="grid-item-content">{{ item.i }}</div>
      </GridItem>
    </GridLayout>
  </div>
</template>

最后加一点基础的样式:

<style scoped>
.dashboard-container {
  padding: 20px;
  max-width: 1440px;
  margin: 0 auto;
}
.grid-container {
  background-color: #f5f7fa;
  border-radius: 8px;
  padding: 10px;
}
.grid-item-content {
  width: 100%;
  height: 100%;
  background-color: #409eff;
  color: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 18px;
  font-weight: bold;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>

现在你打开浏览器看看,是不是已经能看到三个可拖拽、可缩放、自动向上吸附的组件了?如果不行,检查一下有没有引入样式、i属性对不对、layout是不是ref响应式数据——这三个是新手最容易踩的基础坑。

第四步进阶技巧:三个教程里很少提但90%项目都会用到的功能

刚才的Demo只是入门,真正做项目的时候,你肯定会遇到这三个问题:

进阶技巧1:怎么实现真正的响应式?换设备自动调整col-num和row-height?

很多教程里的响应式只是“说说而已”——只会说col-num可以动态改,但具体怎么监听窗口大小变化、怎么根据断点调整,根本没讲清楚,这里给你一个完整的方案: 定义一个响应式的配置对象,包含不同断点下的col-num和row-height:

const responsiveConfig = ref({
  lg: { col: 12, row: 100 }, // 大屏,≥1200px
  md: { col: 8, row: 90 },   // 中屏,≥992px且<1200px
  sm: { col: 6, row: 80 },   // 小屏,≥768px且<992px
  xs: { col: 4, row: 70 },   // 超小屏,<768px
})
const currentBreakpoint = ref('lg')
const currentCol = ref(12)
const currentRow = ref(100)

写一个函数来获取当前的断点:

const getCurrentBreakpoint = () => {
  const width = window.innerWidth
  if (width >= 1200) return 'lg'
  if (width >= 992) return 'md'
  if (width >= 768) return 'sm'
  return 'xs'
}

写一个函数来更新配置和布局:

const updateLayoutConfig = () => {
  const newBreakpoint = getCurrentBreakpoint()
  if (newBreakpoint === currentBreakpoint.value) return // 断点没变就不用更新
  const oldConfig = responsiveConfig.value[currentBreakpoint.value]
  const newConfig = responsiveConfig.value[newBreakpoint]
  // 这里是核心!缩放网格的时候,要把原来的布局数据按比例转换
  // 比如原来占lg屏12格中的6格,换md屏8格就应该占4格
  // 原来占lg屏2行,行高100,换md屏行高90,h要不要变?其实h不用,因为组件会自动适应行高,只需要变x和w
  layout.value = layout.value.map(item => ({
    ...item,
    x: Math.round(item.x * (newConfig.col / oldConfig.col)),
    w: Math.max(Math.round(item.w * (newConfig.col / oldConfig.col)), item.minW || 1), // 不能小于最小宽度
  }))
  currentBreakpoint.value = newBreakpoint
  currentCol.value = newConfig.col
  currentRow.value = newConfig.row
}

在组件挂载的时候添加窗口大小监听事件,在组件卸载的时候移除:

import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
  updateLayoutConfig() // 组件挂载的时候先执行一次
  window.addEventListener('resize', updateLayoutConfig)
})
onUnmounted(() => {
  window.removeEventListener('resize', updateLayoutConfig)
})

然后把模板里的GridLayout的col-num和row-height改成currentCol和currentRow就行啦,这个方案里,我用到了按比例转换x和w的逻辑,这个是Vue3 Grid Layout官方文档里提到的,但很少有教程详细写出来,而且还加了不能小于最小宽度的判断,避免组件缩成看不见。

进阶技巧2:怎么实现布局的保存和还原?存到localStorage或者数据库?

这个功能太常用了——做管理后台的话,用户肯定希望自己调整的仪表盘布局下次打开还能用;做电商装修页的话,商家肯定希望保存装修草稿,实现起来其实非常简单,因为layout数组就是纯JSON数据,直接存就行。 写一个保存布局的函数:

const saveLayout = () => {
  // 存到localStorage的例子
  localStorage.setItem('dashboardLayout', JSON.stringify(layout.value))
  // 存到数据库的例子(伪代码)
  // axios.post('/api/save-dashboard-layout', { userId: 123, layout: layout.value })
  alert('布局保存成功!')
}

在组件挂载的时候还原布局:

onMounted(() => {
  // 先从localStorage里取
  const savedLayout = localStorage.getItem('dashboardLayout')
  if (savedLayout) {
    try {
      layout.value = JSON.parse(savedLayout)
    } catch (e) {
      console.error('布局数据解析失败,使用默认布局', e)
    }
  }
  // 然后再执行响应式配置更新
  updateLayoutConfig()
  window.addEventListener('resize', updateLayoutConfig)
})

还要在模板里加一个保存按钮:

<template>
  <div class="dashboard-container">
    <div class="header">
      <h2>我的个性化仪表盘</h2>
      <button @click="saveLayout" class="save-btn">保存布局</button>
    </div>
    <!-- 其他内容不变 -->
  </div>
</template>

加个header的样式:

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.save-btn {
  padding: 8px 16px;
  background-color: #67c23a;
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}
.save-btn:hover {
  background-color: #5daf34;
}

是不是很简单?不过这里要注意一个小坑:如果你的项目里有用户登录的功能,存localStorage的话,换个账号登录就会看到别人的布局,所以最好存到数据库里,关联用户ID。

进阶技巧3:怎么在GridItem里放ECharts图表?拖拽缩放的时候图表不会变形或者不更新?

这个也是90%做管理后台的人都会遇到的问题——ECharts图表需要一个固定的容器宽高才能正常渲染,但是GridItem的宽高是动态变化的,拖拽缩放之后图表要么变扁变宽,要么完全不显示,解决方法其实就是监听GridItem的宽高变化,然后调用ECharts的resize()方法。 给GridItem里的图表容器加一个ref:

<GridItem v-for="item in layout" :key="item.i" :i="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :minW="item.minW" :minH="item.minH">
  <!-- 把刚才的内容换成图表容器 -->
  <div v-if="item.i === 'chart1'" ref="chart1Ref" class="chart-container"></div>
  <div v-else-if="item.i === 'chart2'" ref="chart2Ref" class="chart-container"></div>
  <div v-else class="grid-item-content">{{ item.i }}</div>
</GridItem>

引入ECharts,定义图表实例和图表容器的ref:

import * as echarts from 'echarts'
// 在script setup里,ref的名字要和模板里的一致
const chart1Ref = ref(null)
const chart2Ref = ref(null)
const chart1Instance = ref(null)
const chart2Instance = ref(null)

写一个初始化图表的函数,还有一个监听GridItem宽高变化的函数:

// 初始化chart1的函数,随便写个简单的折线图
const initChart1 = () => {
  if (!chart1Ref.value) return
  chart1Instance.value = echarts.init(chart1Ref.value)
  const option = { { text: '折线图示例' },
    tooltip: {},
    xAxis: { data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
    yAxis: {},
    series: [{ name: '销量', type: 'line', data: [820, 932, 901, 934, 1290, 1330, 1320] }]
  }
  chart1Instance.value.setOption(option)
}
// 初始化chart2的函数,随便写个简单的柱状图
const initChart2 = () => {
  if (!chart2Ref.value) return
  chart2Instance.value = echarts.init(chart2Ref.value)
  const option = { { text: '柱状图示例' },
    tooltip: {},
    xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] },
    yAxis: {},
    series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }]
  }
  chart2Instance.value.setOption(option)
}
// 监听GridItem宽高变化,调用resize()
const handleGridItemResize = () => {
  if (chart1Instance.value) chart1Instance.value.resize()
  if (chart2Instance.value) chart2Instance.value.resize()
}

在GridLayout组件上添加resized事件监听——这个事件是Vue3 Grid Layout官方提供的,当任何一个GridItem的宽高变化或者整个容器的大小变化时都会触发:

<GridLayout
  :col-num="currentCol"
  :row-height="currentRow"
  :is-draggable="true"
  :is-resizable="true"
  :vertical-compact="true"
  :layout="layout"
  @resized="handleGridItemResize" <!-- 加上这个事件 -->
  class="grid-container"
>

在组件挂载的时候初始化图表,在组件卸载的时候销毁图表实例:

onMounted(() => {
  // 还原布局和响应式配置的代码不变
  const savedLayout = localStorage.getItem('dashboardLayout')
  if (savedLayout) {
    try {
      layout.value = JSON.parse(savedLayout)
    } catch (e) {
      console.error('布局数据解析失败,使用默认布局', e)
    }
  }
  updateLayoutConfig()
  window.addEventListener('resize', updateLayoutConfig)
  // 初始化图表——注意要等DOM渲染完成!所以要用nextTick
  nextTick(() => {
    initChart1()
    initChart2()
  })
})
onUnmounted(() => {
  window.removeEventListener('resize', updateLayoutConfig)
  // 销毁图表实例,防止内存泄漏
  if (chart1Instance.value) chart1Instance.value.dispose()
  if (chart2Instance.value) chart2Instance.value.dispose()
})

还要加一个图表容器的样式:

.chart-container {
  width: 100%;
  height: 100%;
}

现在你再拖拽缩放chart1或者chart2,图表是不是会自动跟着调整大小,不会变形了?这里的关键有两个:一是用nextTick等DOM渲染完成再初始化图表,二是监听GridLayout的resized事件调用ECharts的resize()方法,还有组件卸载的时候销毁实例防止内存泄漏——这个也是很多教程没讲清楚的。

写在最后:Vue3 Grid Layout的其他小细节和注意事项还有几个小细节要注意:

  1. 拖拽缩放的体验优化:你可以给GridLayout添加use-css-transforms属性(默认是true),这样组件拖拽缩放的时候会用CSS3的transform,比直接修改DOM的top/left/width/height更流畅;
  2. 禁止单个组件拖拽缩放:如果你想让某个组件不能拖拽或者不能缩放,可以在layout数组里对应的元素里加static: true属性,这样这个组件就固定了;
  3. 添加拖拽缩放的手柄:默认情况下,整个GridItem的区域都可以拖拽缩放,如果你想只让某个小区域(比如右上角的三个点)可以拖拽缩放,可以用drag-handledrag-ignore-from属性,具体用法可以看官方文档;
  4. 性能优化:如果你的布局里有很多GridItem(比如超过50个),可以考虑用lazy-render属性(默认是false),只渲染可见区域的组件,不过这个功能可能会有一些小bug,要测试清楚再用。

Vue3 Grid Layout是一个非常强大的自由响应式网格布局组件,只要你掌握了上面的基础配置、核心逻辑和进阶技巧,就能快速完成大部分需要自由布局的项目,如果你还有其他问题,或者遇到了我没提到的坑,欢迎在评论区留言交流!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门