Appearance
BizDashboardGrid 仪表盘网格
可配置仪表盘网格:卡片以 x/y/w/h 网格单位布局(v-model),editable 开启后支持拖拽移动(标题栏)与右下角拉伸缩放(pointer events 实现,零第三方依赖),落点与其他卡片碰撞自动回弹;布局变化不可变回写。数据可视化首页、监控大屏、个人工作台的可配置卡片底座——内容经 #item 插槽渲染。
基础布局
核心指标
核心指标(4×2)
销售趋势
销售趋势(8×2)
区域排行
区域排行(6×3)
待办
待办(6×3)
静态布局:卡片按 x/y/w/h 网格单位摆放,内容走 #item 插槽。
<script setup lang="ts">
/** 场景:基础——静态网格布局 + #item 插槽内容 */
import { ref } from 'vue'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const cards = ref<BizDashboardCard[]>([
{ id: 'kpi', title: '核心指标', x: 0, y: 0, w: 4, h: 2 },
{ id: 'trend', title: '销售趋势', x: 4, y: 0, w: 8, h: 2 },
{ id: 'rank', title: '区域排行', x: 0, y: 2, w: 6, h: 3 },
{ id: 'todo', title: '待办', x: 6, y: 2, w: 6, h: 3 },
])
</script>
<template>
<BizDashboardGrid v-model="cards">
<template #item="{ card }">
<div :style="{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '6px',
background: card.id === 'kpi' ? 'var(--el-color-primary-light-9)' : 'var(--el-fill-color-lighter)',
color: 'var(--el-text-color-secondary)',
fontSize: '13px',
}">
{{ card.title }}({{ card.w }}×{{ card.h }})
</div>
</template>
</BizDashboardGrid>
</template>
显示代码
编辑态拖拽缩放
卡片 A拖拽移动
卡片 A
卡片 B拖拽移动
卡片 B
卡片 C拖拽移动
卡片 C
editable 开启后拖标题栏移动、拉右下角缩放;碰撞回弹。
<script setup lang="ts">
/** 场景:编辑态——拖拽移动 + 右下角缩放,碰撞回弹 */
import { ref } from 'vue'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const cards = ref<BizDashboardCard[]>([
{ id: 'a', title: '卡片 A', x: 0, y: 0, w: 6, h: 2 },
{ id: 'b', title: '卡片 B', x: 6, y: 0, w: 6, h: 3 },
{ id: 'c', title: '卡片 C', x: 0, y: 2, w: 6, h: 3 },
])
</script>
<template>
<BizDashboardGrid v-model="cards" editable>
<template #item="{ card }">
<div style="height: 100%; display: flex; align-items: center; justify-content: center; color: var(--el-text-color-placeholder); font-size: 13px">
{{ card.title }}
</div>
</template>
</BizDashboardGrid>
</template>
显示代码
布局持久化
看板 A拖拽移动
看板 A
看板 B拖拽移动
看板 B
保存后刷新页面布局仍在
change 后存 localStorage,刷新还原(resetLayout 恢复初始)。
<script setup lang="ts">
/** 场景:布局持久化——localStorage 存取 + resetLayout */
import { onMounted, ref } from 'vue'
import { ElButton } from 'element-plus'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard, BizDashboardGridInstance } from 'vue3-ep-biz-lib'
const STORAGE_KEY = 'demo-dashboard-layout'
const initial: BizDashboardCard[] = [
{ id: 'a', title: '看板 A', x: 0, y: 0, w: 6, h: 2 },
{ id: 'b', title: '看板 B', x: 6, y: 0, w: 6, h: 2 },
]
const cards = ref<BizDashboardCard[]>(initial.map((c) => ({ ...c })))
const gridRef = ref<BizDashboardGridInstance | null>(null)
onMounted(() => {
try {
const saved = window.localStorage.getItem(STORAGE_KEY)
if (saved !== null) gridRef.value?.setLayout(JSON.parse(saved) as BizDashboardCard[])
} catch {
/* 忽略损坏数据 */
}
})
const persist = (): void => {
/* 实际项目在 change 里持久化;此处按钮演示 */
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(gridRef.value?.getLayout() ?? []))
}
const reset = (): void => {
gridRef.value?.resetLayout(initial)
window.localStorage.removeItem(STORAGE_KEY)
}
</script>
<template>
<div>
<BizDashboardGrid ref="gridRef" v-model="cards" editable>
<template #item="{ card }">
<div style="height: 100%; display: grid; place-items: center; color: var(--el-text-color-placeholder); font-size: 13px">
{{ card.title }}
</div>
</template>
</BizDashboardGrid>
<div style="margin-top: 10px; display: flex; gap: 8px">
<ElButton size="small" @click="persist">保存布局</ElButton>
<ElButton size="small" @click="reset">恢复初始</ElButton>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">保存后刷新页面布局仍在</span>
</div>
</div>
</template>
显示代码
卡片内容
今日 GMV
¥ 1,286,430↑ 12.6% 环比昨日
渠道占比
实时订单
- SO-58231 · 已支付
- SO-58230 · 已发货
- SO-58229 · 待审核
#item 插槽渲染任意卡片内容(作用域 card 含位置尺寸与业务字段)。
<script setup lang="ts">
/** 场景:卡片内容——插槽渲染业务组件(数字 / 图形示意) */
import { ref } from 'vue'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const cards = ref<BizDashboardCard[]>([
{ id: 'gmv', title: '今日 GMV', x: 0, y: 0, w: 4, h: 2 },
{ id: 'bar', title: '渠道占比', x: 4, y: 0, w: 8, h: 3 },
{ id: 'list', title: '实时订单', x: 0, y: 2, w: 4, h: 3 },
])
</script>
<template>
<BizDashboardGrid v-model="cards">
<template #item="{ card }">
<div v-if="card.id === 'gmv'" style="display: flex; flex-direction: column; gap: 4px">
<span style="font-size: 24px; font-weight: 600; color: var(--el-color-primary)">¥ 1,286,430</span>
<span style="font-size: 12px; color: var(--el-color-success)">↑ 12.6% 环比昨日</span>
</div>
<div v-else-if="card.id === 'bar'" style="display: flex; align-items: flex-end; gap: 8px; height: 100%; padding-bottom: 4px">
<div v-for="(v, i) in [62, 85, 45, 96, 70]" :key="i" :style="{ height: `${v}%`, flex: 1, background: 'var(--el-color-primary-light-7)', borderRadius: '4px 4px 0 0' }" />
</div>
<ul v-else style="margin: 0; padding-left: 16px; font-size: 12px; color: var(--el-text-color-secondary); line-height: 22px">
<li>SO-58231 · 已支付</li>
<li>SO-58230 · 已发货</li>
<li>SO-58229 · 待审核</li>
</ul>
</template>
</BizDashboardGrid>
</template>
显示代码
编辑开关
指标卡
指标卡
图表卡
图表卡
预览 / 编辑双态切换的典型工作台用法。
<script setup lang="ts">
/** 场景:预览 / 编辑双态切换 */
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const editable = ref(false)
const cards = ref<BizDashboardCard[]>([
{ id: 'a', title: '指标卡', x: 0, y: 0, w: 4, h: 2 },
{ id: 'b', title: '图表卡', x: 4, y: 0, w: 8, h: 2 },
])
</script>
<template>
<div>
<div style="margin-bottom: 10px">
<ElButton size="small" :type="editable ? 'primary' : ''" @click="editable = !editable">
{{ editable ? '完成编辑' : '自定义布局' }}
</ElButton>
</div>
<BizDashboardGrid v-model="cards" :editable="editable">
<template #item="{ card }">
<div style="height: 100%; display: grid; place-items: center; color: var(--el-text-color-placeholder); font-size: 13px">
{{ card.title }}
</div>
</template>
</BizDashboardGrid>
</div>
</template>
显示代码
事件
卡片 A拖拽移动
卡片 A
卡片 B拖拽移动
卡片 B
change / drag-end 事件日志。
<script setup lang="ts">
/** 场景:change / drag-end 事件日志 */
import { ref } from 'vue'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const cards = ref<BizDashboardCard[]>([
{ id: 'a', title: '卡片 A', x: 0, y: 0, w: 6, h: 2 },
{ id: 'b', title: '卡片 B', x: 6, y: 0, w: 6, h: 2 },
])
const logs = ref<string[]>([])
const onDragEnd = (card: BizDashboardCard): void => {
logs.value.unshift(`drag-end:${card.title} → x:${card.x} y:${card.y} ${card.w}×${card.h}`)
if (logs.value.length > 5) logs.value.pop()
}
</script>
<template>
<div>
<BizDashboardGrid v-model="cards" editable @drag-end="onDragEnd">
<template #item="{ card }">
<div style="height: 100%; display: grid; place-items: center; color: var(--el-text-color-placeholder); font-size: 13px">
{{ card.title }}
</div>
</template>
</BizDashboardGrid>
<p v-for="(log, i) in logs" :key="i" style="margin: 6px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
{{ log }}
</p>
</div>
</template>
显示代码
网格参数
卡片 A拖拽移动
卡片 A
卡片 B拖拽移动
卡片 B
卡片 C拖拽移动
卡片 C
卡片 D拖拽移动
卡片 D
col-count=6 / row-height=60 / gap=6 紧凑网格,min-w 约束最小占列。<script setup lang="ts">
/** 场景:网格参数——colCount 6 列 / rowHeight 60 紧凑行高 / minW 约束 */
import { ref } from 'vue'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const cards = ref<BizDashboardCard[]>([
{ id: 'a', title: '卡片 A', x: 0, y: 0, w: 2, h: 2 },
{ id: 'b', title: '卡片 B', x: 2, y: 0, w: 4, h: 1 },
{ id: 'c', title: '卡片 C', x: 2, y: 1, w: 2, h: 2 },
{ id: 'd', title: '卡片 D', x: 4, y: 1, w: 2, h: 2 },
])
</script>
<template>
<BizDashboardGrid v-model="cards" editable :col-count="6" :row-height="60" :gap="6" :min-w="2" :min-h="1">
<template #item="{ card }">
<div style="height: 100%; display: grid; place-items: center; color: var(--el-text-color-placeholder); font-size: 12px">
{{ card.title }}
</div>
</template>
</BizDashboardGrid>
</template>
显示代码
动态卡片
当前 2 张
指标卡拖拽移动
指标卡
图表卡拖拽移动
图表卡
外部按钮新增 / 移除卡片:布局数据驱动,画布高度自动扩展。
<script setup lang="ts">
/** 场景:动态卡片——外部按钮新增 / 移除卡片,布局实时重排 */
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import { BizDashboardGrid } from 'vue3-ep-biz-lib'
import type { BizDashboardCard } from 'vue3-ep-biz-lib'
const cards = ref<BizDashboardCard[]>([
{ id: 'a', title: '指标卡', x: 0, y: 0, w: 6, h: 2 },
{ id: 'b', title: '图表卡', x: 6, y: 0, w: 6, h: 2 },
])
let seq = 0
const addCard = (): void => {
seq += 1
/* 追加到最底部一行,避免与现有卡片碰撞 */
const maxY = cards.value.reduce((max, c) => Math.max(max, c.y + c.h), 0)
cards.value = [...cards.value, { id: `new-${seq}`, title: `新卡片 ${seq}`, x: 0, y: maxY, w: 6, h: 2 }]
}
const removeLast = (): void => {
if (cards.value.length === 0) return
cards.value = cards.value.slice(0, -1)
}
</script>
<template>
<div>
<div style="margin-bottom: 10px; display: flex; gap: 8px">
<ElButton size="small" type="primary" plain @click="addCard">新增卡片</ElButton>
<ElButton size="small" :disabled="cards.length === 0" @click="removeLast">移除末位</ElButton>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">当前 {{ cards.length }} 张</span>
</div>
<BizDashboardGrid v-model="cards" editable>
<template #item="{ card }">
<div style="height: 100%; display: grid; place-items: center; color: var(--el-text-color-placeholder); font-size: 13px">
{{ card.title }}
</div>
</template>
</BizDashboardGrid>
</div>
</template>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| v-model | BizDashboardCard[] | [] | 卡片布局(id / title / x / y / w / h) |
| editable | boolean | false | 编辑态(可拖拽缩放) |
| col-count | number | 12 | 网格列数 |
| row-height | number | 80 | 单行高度 px |
| gap | number | 8 | 间隙 px |
| min-w / min-h | number | 2 / 1 | 最小占列 / 行 |
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| update:modelValue | (cards) | 布局变化(拖拽 / 缩放落定后) |
| change | (cards) | 同 update |
| drag-end | (card) | 单卡片操作落定(含最终 x/y/w/h) |
Slots
| 插槽名 | 作用域 | 说明 |
|---|---|---|
| item | { card, editable } | 卡片内容 |
Expose
| 名称 | 类型 | 说明 |
|---|---|---|
| getLayout | () => BizDashboardCard[] | 取布局(深拷贝) |
| setLayout | (cards) => void | 整表写入 |
| resetLayout | (initial) => void | 恢复初始布局快照 |
TIP
布局为纯数据(网格单位而非像素),持久化直接 JSON 存储;拖拽落点与其他卡片重叠时放弃本次操作(不做自动挤压),复杂碰撞编排建议叠加栅格参考线由业务层处理。