vue-gridstack
v1.0.1
Published
A Vue 3 draggable responsive grid layout component built on Gridstack.js, designed for quickly creating draggable, resizable, and configurable dashboard/workbench interfaces.
Maintainers
Readme
链接
为什么使用 vue-gridstack?
vue-gridstack 将 Gridstack.js 封装成更适合 Vue 的受控组件模型。应用通过 v-model:layout 持有布局数据,GridstackItem 负责保持 Vue slot 内容稳定渲染,拖拽、缩放、碰撞检测和响应式布局则交给 Gridstack 处理。
这种模型让仪表盘布局更容易持久化、恢复、对比和同步到业务数据。
特性
- 支持 Vue 3 组件导入和插件式全局安装。
- 通过
v-model:layout管理受控布局。 - 任意 Vue 组件都可以作为 widget 内容稳定渲染。
- 支持拖拽、缩放、只读、锁定、最小/最大尺寸、自动落位和子网格配置。
- 内置
heightMode="fill",适合固定高度仪表盘、视频墙和控制台。 - 转发 Gridstack 常用事件,并在
change中提供计算后的最新布局。 - 导出完整 TypeScript 类型。
- 可获取底层
GridStack实例,用于高级能力扩展。
安装
安装 Vue 封装组件:
npm install vue-gridstackpnpm add vue-gridstackyarn add vue-gridstackvue-gridstack 的入口会引入组件样式和 Gridstack 样式。如果你的构建环境需要显式样式入口,可以在应用入口额外引入:
import "vue-gridstack/style.css";运行时依赖:
| 包 | 版本 |
| --- | --- |
| gridstack | 已随 vue-gridstack 内置 |
Peer dependencies:
| 包 | 版本 |
| --- | --- |
| vue | ^3.5.0 |
快速开始
<template>
<Gridstack v-model:layout="layout" :options="gridOptions">
<GridstackItem id="sales">
<SalesCard />
</GridstackItem>
<GridstackItem id="traffic">
<TrafficChart />
</GridstackItem>
</Gridstack>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Gridstack, GridstackItem } from "vue-gridstack";
import type { GridstackLayoutItem, GridstackOptions } from "vue-gridstack";
const gridOptions = ref<GridstackOptions>({
column: 12,
margin: 8,
disableDrag: false,
disableResize: false
});
const layout = ref<GridstackLayoutItem[]>([
{ id: "sales", x: 0, y: 0, w: 4, h: 3 },
{ id: "traffic", x: 4, y: 0, w: 8, h: 3 }
]);
</script>也可以全局注册:
import { createApp } from "vue";
import VueGridstack from "vue-gridstack";
import App from "./App.vue";
createApp(App).use(VueGridstack).mount("#app");核心模型
新代码推荐始终使用下面的模型:
Gridstack接收容器级配置,例如column、margin、staticGrid、disableDrag、disableResize。Gridstack通过v-model:layout管理每个 widget 的位置和尺寸。GridstackItem只接收稳定的id,并渲染 Vue slot 内容。- 每个
GridstackItem id都应与layout中的某一项对应。
<Gridstack v-model:layout="layout" :options="{ column: 12 }">
<GridstackItem v-for="widget in widgets" :key="widget.id" :id="widget.id">
<WidgetCard :widget="widget" />
</GridstackItem>
</Gridstack>const layout = ref<GridstackLayoutItem[]>([
{ id: "cpu", x: 0, y: 0, w: 3, h: 2 },
{ id: "memory", x: 3, y: 0, w: 3, h: 2, minW: 2 },
{ id: "logs", w: 6, h: 3, autoPosition: true }
]);API
Gridstack Props
| Prop | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| layout | GridstackLayoutItem[] | [] | 受控布局数据,推荐使用 v-model:layout。 |
| options | GridstackOptions | {} | Gridstack 容器级配置,会传给 GridStack.init() 和 grid.updateOptions()。 |
| heightMode | "native" \| "fill" | "native" | 控制组件如何处理网格高度和 cellHeight。 |
GridstackItem Props
| Prop | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| id | string \| number | 必填 | 稳定的 widget 唯一标识,需要和 layout 中对应项一致。 |
不要把 x、y、w、h、noResize、noMove 等几何或行为配置传给 GridstackItem。这些配置应统一放在 layout 中。
<!-- 推荐 -->
<Gridstack v-model:layout="layout">
<GridstackItem id="chart">
<ChartPanel />
</GridstackItem>
</Gridstack>const layout = ref([{ id: "chart", x: 0, y: 0, w: 4, h: 3 }]);Layout Item 字段
每一项都需要稳定的 id。组件会把下面的 widget 字段透传给 Gridstack:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | string \| number | widget 唯一标识,内部会统一转为字符串。 |
| x | number \| string | 起始列,从 0 开始。 |
| y | number \| string | 起始行,从 0 开始。 |
| w | number \| string | 宽度,占用列数,最小会规范为 1。 |
| h | number \| string | 高度,占用行数,最小会规范为 1。 |
| autoPosition | boolean \| string | 交给 Gridstack 自动落位。为 true 时,组件会忽略 x 和 y,直到落位完成。 |
| minW / maxW | number \| string | 最小宽度和最大宽度。 |
| minH / maxH | number \| string | 最小高度和最大高度。 |
| noResize | boolean \| string | 禁止当前 widget 缩放。 |
| noMove | boolean \| string | 禁止当前 widget 拖拽。 |
| locked | boolean \| string | 按 Gridstack 行为锁定当前 widget。 |
| content | string | Gridstack 原生内容字段。Vue 场景推荐使用 slot。 |
| sizeToContent | boolean \| number \| string | 透传 Gridstack 内容自适应能力。 |
| lazyLoad | boolean \| string | Gridstack 原生懒加载字段。 |
| resizeToContentParent | string | Gridstack 内容自适应时使用的父级选择器。 |
| subGridOpts | object | Gridstack 原生子网格配置。 |
布局同步规则:
- 父组件修改
layout后,组件会同步到 Gridstack。 - 用户拖拽或缩放后,组件会触发
update:layout。 - 组件回写
x、y、w、h时会保留已有业务字段。 autoPosition: true在 Gridstack 完成落位后会被移除。- 未列在上表中的业务字段可以放在
layout中,但不会传给 Gridstack。
options
options 接收 Gridstack 原生容器配置。组件只补充一个默认值:
{
auto: false
}auto: false 用于避免 Gridstack 在 Vue slot 渲染完成前扫描 DOM。组件会根据 layout 和已渲染的 GridstackItem 主动创建、更新 widget。
常用配置示例:
const gridOptions = computed<GridstackOptions>(() => ({
column: 12,
margin: 8,
float: false,
animate: true,
disableDrag: !editing.value,
disableResize: !editing.value,
draggable: {
handle: ".grid-stack-item-content",
appendTo: "body"
},
resizable: {
handles: "all",
autoHide: true
}
}));heightMode
heightMode="native"
默认模式。网格高度遵循 Gridstack 原生行为,以及你传入的 options.cellHeight。
<Gridstack v-model:layout="layout" height-mode="native" :options="{ column: 12, cellHeight: 80 }">
<GridstackItem id="a">
<Widget />
</GridstackItem>
</Gridstack>适合普通页面流、长仪表盘和需要自然增长的布局。
heightMode="fill"
组件会填满父容器高度,并根据父容器高度、行数和间距计算 cellHeight。
<template>
<section class="dashboard">
<Gridstack v-model:layout="layout" height-mode="fill" :options="{ row: 6, column: 12 }">
<GridstackItem id="camera">
<CameraPlayer />
</GridstackItem>
</Gridstack>
</section>
</template>
<style scoped>
.dashboard {
height: 100%;
min-height: 560px;
}
</style>适合固定画布、视频墙、监控大屏和需要铺满指定容器的工作台。父元素必须有明确高度,建议设置 options.row 或 options.minRow。
事件
Gridstack 会转发 Gridstack 常用事件,并在 change 事件中额外提供计算后的 nextLayout。
| 事件 | 参数 | 说明 |
| --- | --- | --- |
| ready | (grid) | Gridstack 实例初始化完成。 |
| change | (items, event, grid, nextLayout) | 布局发生变化,nextLayout 是计算后的受控布局。 |
| added | (items, event, grid) | widget 被添加。 |
| removed | (items, event, grid) | widget 被移除。 |
| dragstart | (element, event, grid) | 开始拖拽。 |
| drag | (element, event, grid) | 拖拽中。 |
| dragstop | (element, event, grid) | 拖拽结束。 |
| resizestart | (element, event, grid) | 开始缩放。 |
| resize | (element, event, grid) | 缩放中。 |
| resizestop | (element, event, grid) | 缩放结束。 |
| resizecontent | (items, event, grid) | Gridstack 内容自适应事件。 |
| dropped | (previousNode, newNode, event, grid) | 外部元素拖入网格。 |
| enable | (event, grid) | Gridstack 被启用。 |
| disable | (event, grid) | Gridstack 被禁用。 |
<Gridstack v-model:layout="layout" @ready="onReady" @change="onChange">
<GridstackItem id="a">
<Widget />
</GridstackItem>
</Gridstack>function onReady(grid: GridStack) {
grid.compact();
}
function onChange(items, event, grid, nextLayout) {
console.log(nextLayout);
}大多数场景不需要在 change 中手动给 layout 赋值,因为 v-model:layout 已经会接收组件回写。change 更适合用于保存、埋点或联动其它状态。
暴露方法
可以通过模板 ref 访问公开方法:
<template>
<Gridstack ref="gridRef" v-model:layout="layout">
<GridstackItem id="a">
<Widget />
</GridstackItem>
</Gridstack>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { GridstackExpose } from "vue-gridstack";
const gridRef = ref<GridstackExpose | null>(null);
function saveCurrentLayout() {
const nextLayout = gridRef.value?.getLayout() ?? [];
return api.saveLayout(nextLayout);
}
function compact() {
gridRef.value?.getGridStack()?.compact();
}
</script>| 方法 | 返回值 | 说明 |
| --- | --- | --- |
| getGridStack() | GridStack \| null | 获取 Gridstack 原生实例。 |
| getGridElement() | HTMLElement \| null | 获取内部 .grid-stack-inner 元素。 |
| getShellElement() | HTMLElement \| null | 获取外层 shell 元素。 |
| syncWidgets() | void | 手动同步已渲染 widget 到 Gridstack。 |
| updateGridCellHeight() | void | 在 heightMode="fill" 下重新计算 cellHeight。 |
| getLayout() | GridstackLayoutItem[] | 从当前 Gridstack 实例读取布局。 |
| getOptions() | GridstackOptions | 获取组件处理后的 Gridstack 配置。 |
常见场景
只读仪表盘
<Gridstack v-model:layout="layout" :options="{ column: 12, staticGrid: true }">
<GridstackItem v-for="item in widgets" :key="item.id" :id="item.id">
<WidgetCard :data="item" />
</GridstackItem>
</Gridstack>也可以使用 disableDrag 和 disableResize:
<Gridstack
v-model:layout="layout"
:options="{ column: 12, disableDrag: true, disableResize: true }"
>
<GridstackItem id="a">
<Widget />
</GridstackItem>
</Gridstack>编辑布局
<template>
<button type="button" @click="editing = !editing">切换编辑</button>
<Gridstack v-model:layout="layout" :options="gridOptions" @change="dirty = true">
<GridstackItem v-for="item in widgets" :key="item.id" :id="item.id">
<WidgetCard :data="item" />
</GridstackItem>
</Gridstack>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import type { GridstackOptions } from "vue-gridstack";
const editing = ref(false);
const dirty = ref(false);
const gridOptions = computed<GridstackOptions>(() => ({
column: 12,
margin: 8,
disableDrag: !editing.value,
disableResize: !editing.value,
resizable: { handles: "all" }
}));
</script>动态添加 widget
新增时建议同时追加业务数据和 layout 项。使用 autoPosition: true 可以让 Gridstack 自动寻找位置。
function addWidget() {
const id = `widget-${Date.now()}`;
widgets.value.push({
id,
title: "New widget"
});
layout.value.push({
id,
w: 3,
h: 2,
autoPosition: true
});
}Gridstack 完成落位后,组件会回写真实的 x、y、w、h。
删除 widget
保持渲染列表和 layout 同步:
function removeWidget(id: string) {
widgets.value = widgets.value.filter(item => item.id !== id);
layout.value = layout.value.filter(item => String(item.id) !== id);
}锁定单个 widget
const layout = ref<GridstackLayoutItem[]>([
{
id: "fixed-panel",
x: 0,
y: 0,
w: 4,
h: 3,
noMove: true,
noResize: true
}
]);铺满父容器
<template>
<section class="video-wall">
<Gridstack v-model:layout="layout" height-mode="fill" :options="gridOptions">
<GridstackItem v-for="camera in cameras" :key="camera.id" :id="camera.id">
<CameraPlayer :camera="camera" />
</GridstackItem>
</Gridstack>
</section>
</template>
<script setup lang="ts">
import { ref } from "vue";
import type { GridstackOptions } from "vue-gridstack";
const gridOptions = ref<GridstackOptions>({
row: 2,
column: 4,
margin: 6,
float: true,
disableDrag: true,
disableResize: true
});
</script>
<style scoped>
.video-wall {
height: 100%;
min-height: 560px;
}
</style>使用 Gridstack 高级 API
<Gridstack ref="gridRef" v-model:layout="layout" @ready="onReady">
<GridstackItem id="a">
<Widget />
</GridstackItem>
</Gridstack>function onReady(grid: GridStack) {
grid.compact();
}
function batchUpdate() {
const grid = gridRef.value?.getGridStack();
if (!grid) return;
grid.batchUpdate();
// 在这里调用 Gridstack 原生 API
grid.batchUpdate(false);
layout.value = gridRef.value?.getLayout() ?? layout.value;
}原生 DOM 兼容
Vue 项目推荐使用 GridstackItem。如果你需要迁移 Gridstack 原生 DOM 写法,也可以直接传入原生节点:
<Gridstack :options="{ column: 12 }">
<div class="grid-stack-item" gs-id="native-a" gs-x="0" gs-y="0" gs-w="4" gs-h="2">
<div class="grid-stack-item-content">Native A</div>
</div>
<div class="grid-stack-item" gs-id="native-b" gs-w="3" gs-h="2" gs-auto-position="true">
<div class="grid-stack-item-content">Native B</div>
</div>
</Gridstack>组件可以读取这些属性:gs-id、gs-x、gs-y、gs-w、gs-h、gs-min-w、gs-max-w、gs-min-h、gs-max-h、gs-auto-position、gs-no-resize、gs-no-move、gs-locked、gs-size-to-content。
TypeScript 导出
import type {
GridstackExpose,
GridstackHeightMode,
GridstackItemId,
GridstackLayoutItem,
GridstackOptions
} from "vue-gridstack";本地开发
npm install
npm run dev
npm run typecheck
npm run build
npm run build:demo
npm run build:all
npm run pack:check脚本说明:
| 脚本 | 说明 |
| --- | --- |
| npm run dev | 启动本地 demo。 |
| npm run typecheck | 执行 Vue 和 TypeScript 类型检查。 |
| npm run build | 构建 npm 组件库产物到 dist。 |
| npm run build:demo | 构建可部署 demo 到 dist-demo。 |
| npm run build:all | 同时构建组件库和 demo。 |
| npm run pack:check | dry-run 检查 npm 包内容。 |
常见问题
DOM 中有 widget,但位置不符合预期
确认 GridstackItem id 是否能在 layout 中找到对应项。如果没有对应 layout,组件会使用 { id, w: 1, h: 1, autoPosition: true } 作为兜底。
heightMode="fill" 高度异常
heightMode="fill" 依赖父元素有真实高度。请为父元素设置 height、min-height,或通过布局规则让父元素拥有稳定高度。
Vue 内容被意外替换
Vue 场景推荐通过 slot 渲染内容,不建议使用 Gridstack 原生 content 字段。
持久化布局时业务字段丢失或未生效
组件回写时会保留已有 layout 项上的自定义业务字段,但只有文档列出的 Gridstack widget 字段会透传给 Gridstack。
