卡片模式
卡片适合成员、资产、商品等以单条信息为中心的列表。字段、字典和插槽沿用 CRUD 配置,使用 grid: true 切换为卡片布局。
版本
卡片模式从 3.4.0 起支持;grid-status-change 从 3.4.7 起支持;下面的跨页保留选择示例请使用 3.9.5 及以上版本。
基本用法
grid 指定初始显示方式,gridBtn 控制卡片 / 表格切换按钮。点击右上方的切换按钮,下方状态会通过 grid-status-change 更新。
<template>
<div>
<avue-crud :option="option"
:data="data"
@grid-status-change="gridStatusChange" />
<p class="grid-status" aria-live="polite">当前显示:{{ grid ? '卡片模式' : '表格模式' }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const grid = ref(true);
const data = ref([
{ id: 1, name: '张三', department: '研发部', role: '前端工程师', province: '110000' },
{ id: 2, name: '李四', department: '产品部', role: '产品经理', province: '130000' }
]);
const option = ref({
rowKey: 'id',
grid: true,
gridBtn: true,
gridSpan: 12,
gridXsSpan: 24,
addBtn: false,
menu: false,
refreshBtn: false,
columnBtn: false,
column: [
{ label: '姓名', prop: 'name', gridRow: true },
{ label: '部门', prop: 'department' },
{ label: '岗位', prop: 'role', gridRow: true },
{
label: '所在地',
prop: 'province',
type: 'select',
dicData: [
{ label: '北京市', value: '110000' },
{ label: '河北省', value: '130000' }
]
}
]
});
function gridStatusChange(value) {
grid.value = value;
}
</script>
<style scoped>
.grid-status { margin: 12px 0 0; color: var(--el-text-color-secondary); }
</style>
跨页保留选择
同时配置 selection: true、reserveSelection: true 和稳定的 rowKey。在第 1 页勾选一张卡片,切换到第 2 页再选择一张,然后回到第 1 页查看状态。「重新加载当前页」会创建新的行对象,但相同 ID 的选中状态仍保留。
<template>
<div class="grid-selection-example">
<div class="grid-selection-actions">
<el-button @click="loadPage(page)">重新加载当前页</el-button>
<el-button :disabled="!selected.length" @click="clearSelection">清空全部选择</el-button>
</div>
<avue-crud ref="crud"
v-model:page="page"
:option="option"
:data="data"
@on-load="loadPage"
@selection-change="selectionChange" />
<div class="grid-selection-result" aria-live="polite">
<strong>已选择 {{ selected.length }} 条</strong>
<p v-if="!selected.length">请勾选卡片,再翻页选择其他记录。</p>
<div v-else class="grid-selection-tags">
<el-tag v-for="row in selected" :key="row.id">#{{ row.id }} {{ row.name }}</el-tag>
</div>
<small>当前第 {{ page.currentPage }} 页 · 已加载 {{ loadCount }} 次</small>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const crud = ref(null);
const selected = ref([]);
const data = ref([]);
const loadCount = ref(0);
const rows = [
{ id: 101, name: '张三', department: '研发部' },
{ id: 102, name: '李四', department: '产品部' },
{ id: 103, name: '王五', department: '运营部' },
{ id: 104, name: '赵六', department: '测试部' },
{ id: 105, name: '周七', department: '设计部' },
{ id: 106, name: '吴八', department: '研发部' }
];
const page = ref({
currentPage: 1,
pageSize: 2,
pageSizes: [2],
total: rows.length,
layout: 'total, prev, pager, next'
});
const option = ref({
rowKey: 'id',
grid: true,
gridBtn: false,
gridSpan: 12,
gridXsSpan: 24,
selection: true,
reserveSelection: true,
addBtn: false,
menu: false,
refreshBtn: false,
columnBtn: false,
column: [
{ label: '编号', prop: 'id', gridRow: true },
{ label: '姓名', prop: 'name', gridRow: true },
{ label: '部门', prop: 'department', gridRow: true }
]
});
function loadPage(currentPage) {
const start = (currentPage.currentPage - 1) * currentPage.pageSize;
// 模拟每次接口返回新对象;选中状态由稳定 id 保持。
data.value = rows.slice(start, start + currentPage.pageSize).map(row => ({ ...row }));
loadCount.value += 1;
}
function selectionChange(rows) {
selected.value = rows;
}
function clearSelection() {
crud.value.clearSelection();
}
</script>
<style scoped>
.grid-selection-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.grid-selection-actions .el-button { margin-left: 0; }
.grid-selection-result {
margin-top: 16px;
padding: 16px;
border-radius: 8px;
background: var(--el-fill-color-light);
line-height: 1.7;
}
.grid-selection-result p { margin: 8px 0; }
.grid-selection-result small { display: block; margin-top: 10px; color: var(--el-text-color-secondary); }
.grid-selection-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
</style>
selection-change 返回保留的已选记录,下方列表会显示所有已选 ID,而不只是当前页。取消当前页某张卡片不会取消其他页的选择;「清空全部选择」调用 clearSelection()。
主键必须在全部分页数据中唯一。不要用页内下标、每次请求重新生成的随机值,或姓名等可能重复的字段。若不需要跨页保留,关闭 reserveSelection;分页和重新加载后只保留当前页面应有的选择状态。
示例固定使用卡片模式,便于验证分页。若业务需要在表格和卡片之间切换并继续保留选中项,请在业务层保存已选主键,并在切换后恢复选择。
布局与颜色
gridSpan 按 24 栅格控制桌面卡片宽度,gridXsSpan 控制窄屏宽度,gridRow 让字段独占一行。本例桌面两列、窄屏一列,使用浅色背景突出内容。
<template>
<avue-crud :option="option" :data="data" />
</template>
<script setup>
import { ref } from 'vue';
const data = ref([
{ id: 1, name: '文档完善', owner: '张三', status: '进行中', description: '补充可交互示例、操作步骤和常见问题。' },
{ id: 2, name: '版本验证', owner: '李四', status: '待开始', description: '确认桌面与手机布局,以及本地演示数据。' }
]);
const option = ref({
rowKey: 'id',
grid: true,
gridBtn: false,
gridSpan: 12,
gridXsSpan: 24,
gridBackground: 'linear-gradient(135deg, #eff6ff, #f8fafc)',
addBtn: false,
menu: false,
refreshBtn: false,
columnBtn: false,
column: [
{ label: '任务', prop: 'name', gridRow: true },
{ label: '负责人', prop: 'owner' },
{ label: '状态', prop: 'status' },
{ label: '说明', prop: 'description', gridRow: true }
]
});
</script>
字段较多时,优先把标题、描述设置为独占一行。背景图通过 gridBackgroundImage 配置;请保证文字和背景有足够对比度。
按状态设置背景
gridBackground(row, index) 返回当前卡片的背景值。本例根据记录状态显示不同背景,颜色与状态文字同时出现,避免只靠颜色传达含义。
<template>
<avue-crud :option="option" :data="data" />
</template>
<script setup>
import { ref } from 'vue';
const data = ref([
{ id: 1, name: '示例检查', owner: '张三', status: 'done' },
{ id: 2, name: '发布检查', owner: '李四', status: 'pending' }
]);
const option = ref({
rowKey: 'id',
grid: true,
gridBtn: false,
gridSpan: 12,
gridXsSpan: 24,
addBtn: false,
menu: false,
refreshBtn: false,
columnBtn: false,
gridBackground: row => row.status === 'done'
? 'linear-gradient(135deg, #ecfdf5, #f8fafc)'
: 'linear-gradient(135deg, #fff7ed, #f8fafc)',
column: [
{ label: '任务', prop: 'name', gridRow: true },
{ label: '负责人', prop: 'owner', gridRow: true },
{
label: '状态',
prop: 'status',
gridRow: true,
type: 'select',
dicData: [
{ label: '已完成', value: 'done' },
{ label: '待处理', value: 'pending' }
]
}
]
});
</script>
自定义内容
使用与字段 prop 同名的插槽展示自定义内容。本例把头像绘制为姓名首字,不需要远程图片;样式限定在当前示例中,窄屏仍按单列显示。
<template>
<div class="grid-slot-example">
<avue-crud :option="option" :data="data">
<template #profile="{ row }">
<div class="grid-profile">
<span class="grid-avatar" aria-hidden="true">{{ row.name.slice(0, 1) }}</span>
<div>
<strong>{{ row.name }}</strong>
<span>{{ row.role }}</span>
</div>
</div>
</template>
</avue-crud>
</div>
</template>
<script setup>
import { ref } from 'vue';
const data = ref([
{ id: 1, name: '张三', role: '前端工程师', department: '研发部', location: '北京' },
{ id: 2, name: '李四', role: '产品经理', department: '产品部', location: '石家庄' }
]);
const option = ref({
rowKey: 'id',
grid: true,
gridBtn: false,
gridSpan: 12,
gridXsSpan: 24,
addBtn: false,
menu: false,
refreshBtn: false,
columnBtn: false,
column: [
{ label: '成员', prop: 'profile', gridRow: true, slot: true },
{ label: '部门', prop: 'department', gridRow: true },
{ label: '所在地', prop: 'location', gridRow: true }
]
});
</script>
<style scoped>
.grid-profile { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
.grid-avatar {
display: inline-flex;
width: 42px;
height: 42px;
flex: 0 0 42px;
align-items: center;
justify-content: center;
border-radius: 12px;
background: #dbeafe;
color: #1d4ed8;
font-size: 20px;
font-weight: 600;
}
.grid-profile strong, .grid-profile div > span { display: block; overflow-wrap: anywhere; }
.grid-profile div > span { margin-top: 4px; color: var(--el-text-color-secondary); font-size: 13px; }
</style>
插槽中的 row 是当前记录,常规文本字段无需额外声明插槽。自定义图片时应提供替代文字和固定显示尺寸,以免图片加载前后造成布局跳动。
