Appearance
BizCardList 卡片列表
与 BizTable 平行的列表形态——卡片墙:响应式栅格(断点 4/3/2/1 列或固定列数)、本地 / 远程分页双模式、单选 / 多选选择态、空态兜底。卡片内容完全交给 card 作用域插槽,适用于商品、设备、工单、素材等「一卡一物」的数据列表。
基础用法(远程分页)
暂无数据
request 注入即远程模式({ page, pageSize } 契约同 BizCrudPage),内置 loading、分页条与空态。<script setup lang="ts">
/** 基础用法:远程分页卡片墙(request 注入,响应式 4/3/2/1 列) */
import { BizCardList } from 'vue3-ep-biz-lib'
interface Device {
id: number
name: string
status: 'online' | 'offline'
place: string
}
/** 42 台设备 mock,支持分页 */
const ALL: Device[] = Array.from({ length: 42 }, (_, i) => ({
id: i + 1,
name: `监控设备-${String(i + 1).padStart(3, '0')}`,
status: i % 3 === 0 ? 'offline' : 'online',
place: `${['东门', '西门', '南门', '北门'][i % 4]}岗亭 ${i + 1} 号点位`,
}))
const request = async ({ page, pageSize }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 300))
return { list: ALL.slice((page - 1) * pageSize, page * pageSize), total: ALL.length }
}
</script>
<template>
<BizCardList :request="request" :page-size="8">
<template #card="{ item }">
<div class="device">
<div class="name">{{ item.name }}</div>
<div class="meta">
<span :class="['dot', item.status]" />
{{ item.status === 'online' ? '在线' : '离线' }} · {{ item.place }}
</div>
</div>
</template>
</BizCardList>
</template>
<style scoped>
.device {
padding: 14px;
}
.name {
font-size: 14px;
font-weight: 600;
}
.meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.dot.online {
background: var(--el-color-success);
}
.dot.offline {
background: var(--el-text-color-disabled);
}
</style>
显示代码
本地数据 · 多选 · 固定列数
机械键盘 87 键
¥399
无线鼠标静音款
¥129
4K 显示器 27 寸
¥1899
升降桌 1.4m
¥2199
人体工学椅
¥1599
桌面理线槽
¥59
已选 0 件 · 合计 ¥0(点击卡片切换选中)
data 本地模式 + selectable="multiple":卡片点击切换选中,v-model 同步 key 数组;columns=3 固定三列。<script setup lang="ts">
/** 进阶:本地数据 + 多选(v-model key 数组)+ 固定 3 列 */
import { computed, ref } from 'vue'
import { BizCardList } from 'vue3-ep-biz-lib'
interface Goods {
id: number
name: string
price: number
}
const goods = ref<Goods[]>([
{ id: 1, name: '机械键盘 87 键', price: 399 },
{ id: 2, name: '无线鼠标静音款', price: 129 },
{ id: 3, name: '4K 显示器 27 寸', price: 1899 },
{ id: 4, name: '升降桌 1.4m', price: 2199 },
{ id: 5, name: '人体工学椅', price: 1599 },
{ id: 6, name: '桌面理线槽', price: 59 },
])
const selected = ref<number[]>([])
const total = computed(() => selected.value.reduce((sum, id) => sum + (goods.value.find((g) => g.id === id)?.price ?? 0), 0))
</script>
<template>
<div>
<BizCardList v-model="selected" :data="goods" selectable="multiple" :columns="3">
<template #card="{ item, selected: checked }">
<div class="goods" :class="{ checked }">
<div class="name">{{ item.name }}</div>
<div class="price">¥{{ item.price }}</div>
</div>
</template>
</BizCardList>
<div class="tip">已选 {{ selected.length }} 件 · 合计 ¥{{ total }}(点击卡片切换选中)</div>
</div>
</template>
<style scoped>
.goods {
padding: 14px;
}
.name {
font-size: 14px;
}
.price {
margin-top: 6px;
font-size: 15px;
font-weight: 600;
color: var(--el-color-danger);
}
.tip {
margin-top: 8px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
单选卡片
eth0 主网卡
1000Mbps · 点击选中(再点取消)
eth1 备网卡
1000Mbps · 点击选中(再点取消)
wlan0 无线
300Mbps · 点击选中(再点取消)
当前绑定:id=2(selection-change 携带行对象)
selectable=single 再点取消;selection-change 携带行对象(设备绑定场景)。
<script setup lang="ts">
/** 单选卡片:设备绑定主网卡场景(再点取消选择) */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizCardList } from 'vue3-ep-biz-lib'
const devices = [
{ id: 1, name: 'eth0 主网卡', speed: '1000Mbps' },
{ id: 2, name: 'eth1 备网卡', speed: '1000Mbps' },
{ id: 3, name: 'wlan0 无线', speed: '300Mbps' },
]
const selected = ref<number | undefined>(2)
const onSelect = (rows: unknown[]): void => {
const row = rows[0] as { name: string } | undefined
if (row !== undefined) ElMessage.success(`已绑定:${row.name}`)
}
</script>
<template>
<div>
<BizCardList v-model="selected" :data="devices" selectable="single" :columns="3" @selection-change="onSelect">
<template #card="{ item }">
<div class="dev">
<div class="name">{{ item.name }}</div>
<div class="meta">{{ item.speed }} · 点击选中(再点取消)</div>
</div>
</template>
</BizCardList>
<div class="tip">当前绑定:id={{ selected ?? '(未选)' }}(selection-change 携带行对象)</div>
</div>
</template>
<style scoped>
.dev {
padding: 14px;
}
.name {
font-size: 14px;
font-weight: 600;
}
.meta {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.tip {
margin-top: 8px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
商品卡片墙
列数: 间距:
无线降噪耳机
¥899库存 132
机械键盘 87 键
¥399库存 58
4K 显示器
¥1899库存 12
人体工学椅
¥1599缺货
升降桌
¥2199库存 31
桌面理线槽
¥59库存 480
columns / gutter 实时切换;card-click 进详情,卡内嵌状态标签。
<script setup lang="ts">
/** 商品卡片墙:列数/间距切换 + 卡内操作按钮(card-click 详情) */
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizCardList } from 'vue3-ep-biz-lib'
const columns = ref(3)
const gutter = ref(16)
const goods = [
{ id: 1, name: '无线降噪耳机', price: 899, stock: 132 },
{ id: 2, name: '机械键盘 87 键', price: 399, stock: 58 },
{ id: 3, name: '4K 显示器', price: 1899, stock: 12 },
{ id: 4, name: '人体工学椅', price: 1599, stock: 0 },
{ id: 5, name: '升降桌', price: 2199, stock: 31 },
{ id: 6, name: '桌面理线槽', price: 59, stock: 480 },
]
const onClick = (item: { name: string }): void => {
ElMessage.info(`进入「${item.name}」详情(card-click)`)
}
</script>
<template>
<div>
<div class="ops">
列数:<ElSlider v-model="columns" :min="1" :max="4" style="width: 120px" />
间距:<ElSlider v-model="gutter" :min="0" :max="32" style="width: 120px" />
</div>
<BizCardList :data="goods" :columns="columns" :gutter="gutter" @card-click="onClick">
<template #card="{ item }">
<div class="goods">
<div class="name">{{ item.name }}</div>
<div class="row2">
<span class="price">¥{{ item.price }}</span>
<ElTag v-if="item.stock === 0" size="small" type="danger">缺货</ElTag>
<ElTag v-else size="small" type="info">库存 {{ item.stock }}</ElTag>
</div>
</div>
</template>
</BizCardList>
</div>
</template>
<style scoped>
.ops {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.goods {
padding: 14px;
}
.name {
font-size: 14px;
}
.row2 {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.price {
color: var(--el-color-danger);
font-weight: 600;
}
</style>
显示代码
工单池(远程 + 多选批量)
暂无数据
已选:(空)
远程分页 + 多选联动 BizBatchActionsBar 批量领单;首屏自动骨架。
<script setup lang="ts">
/** 远程分页 + 多选批量领单:工单池场景(首屏骨架自动出现) */
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { BizBatchActionsBar, BizCardList } from 'vue3-ep-biz-lib'
const ALL = Array.from({ length: 35 }, (_, i) => ({
id: i + 1,
title: `工单 #${String(i + 1).padStart(4, '0')}`,
level: (['紧急', '高', '中', '低'] as const)[i % 4],
}))
const selected = ref<number[]>([])
const selectedRows = computed(() => ALL.filter((t) => selected.value.includes(t.id)))
const request = async ({ page, pageSize }: { page: number; pageSize: number }) => {
await new Promise((resolve) => setTimeout(resolve, 600))
return { list: ALL.slice((page - 1) * pageSize, page * pageSize), total: ALL.length }
}
const claim = (): void => {
ElMessage.success(`批量领取 ${selected.value.length} 单(首屏 600ms 骨架占位)`)
selected.value = []
}
</script>
<template>
<div>
<BizBatchActionsBar :count="selected.length" @clear="selected = []">
<ElButton size="small" type="primary" @click="claim">批量领取</ElButton>
</BizBatchActionsBar>
<BizCardList v-model="selected" :request="request" :page-size="8" selectable="multiple">
<template #card="{ item }">
<div class="ticket">
<span class="title">{{ item.title }}</span>
<ElTag size="small" :type="item.level === '紧急' ? 'danger' : item.level === '高' ? 'warning' : 'info'">
{{ item.level }}
</ElTag>
</div>
</template>
</BizCardList>
<div class="tip">已选:{{ selectedRows.map((t) => t.title).join('、') || '(空)' }}</div>
</div>
</template>
<style scoped>
.ticket {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
}
.title {
font-size: 14px;
}
.tip {
margin-top: 8px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
</style>
显示代码
空态与 refresh
暂无内容,点击下方按钮恢复数据
空数据自定义文案;expose refresh() 重查(筛选条件变化后常用)。
<script setup lang="ts">
/** 空态与重试:远程返回空列表 + 刷新按钮(expose refresh) */
import { ref } from 'vue'
import { BizCardList } from 'vue3-ep-biz-lib'
import type { BizCardListInstance, BizCardListRequest } from 'vue3-ep-biz-lib'
const empty = ref(true)
const listRef = ref<BizCardListInstance | null>(null)
const request: BizCardListRequest<{ id: number; title: string }> = async () => {
await new Promise((resolve) => setTimeout(resolve, 400))
return empty.value
? { list: [], total: 0 }
: { list: [{ id: 1, title: '恢复服务后的第一个数据项' }], total: 1 }
}
const toggle = async (): Promise<void> => {
empty.value = !empty.value
listRef.value?.refresh()
}
</script>
<template>
<div>
<BizCardList ref="listRef" :request="request" empty-text="暂无内容,点击下方按钮恢复数据" :pagination="false">
<template #card="{ item }">
<div style="padding: 14px">{{ item.title }}</div>
</template>
</BizCardList>
<ElButton size="small" @click="toggle">{{ empty ? '恢复数据(refresh)' : '清空数据' }}</ElButton>
</div>
</template>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| data | T[] | [] | 本地数据源(与 request 二选一) |
| request | (params: { page, pageSize }) => Promise<{ list, total }> | — | 远程数据源(传了即远程模式) |
| page-size | number | 12 | 远程模式分页大小 |
| loading | boolean | false | 本地模式加载态(远程模式内部管理) |
| gutter | number | 16 | 卡片间距,横向列距与纵向行距保持一致,设 0 时卡片紧贴 |
| columns | number | 响应式 | 固定列数(1~6);缺省 ≥1200 四列 / ≥992 三列 / ≥768 两列 / 手机单列 |
| selectable | 'multiple' | 'single' | — | 选择模式,缺省不可选 |
| model-value / v-model | key[] | key | — | 选中项(multiple 数组 / single 单值) |
| item-key | string | 'id' | 行 key 字段名 |
| pagination | boolean | true | 远程模式底部分页条 |
| empty-text | string | '暂无数据' | 空态文案 |
其余属性经
v-bind="$attrs"透传根节点。
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| update:model-value | (keys) | 选择变化 |
| selection-change | (rows: T[]) | 选择变化(选中行对象数组) |
| card-click | (item, index) | 卡片点击(不可选模式下主交互入口) |
Slots
| 插槽名 | 说明 |
|---|---|
| card | 卡片内容(作用域:item、index、selected) |
Expose
| 属性/方法 | 说明 |
|---|---|
| refresh | () => void 远程模式回第一页重查 |
| page / total | 当前页码 / 总数(ref) |