10. vxe-grid 表格开发全解
一句话定位
全站列表页只用 vxe-grid,参数统一由 useVxeTable() 产出,分页/排序偏好由 useGridPageParam() / gridPageEvents() 管理,查询刷新由 useGridQuery() 触发。
代码位置:src/hooks/useVxeTableOptionsHook.ts
结构图
页面 index.vue
├─ useGridPageParam(id, init) ← 先调用!恢复本地 pageSize / defaultSort
│ 返回 { key: 'localPageParam:sysConfig', param: { pageSize, defaultSort } }
├─ useVxeTable<T>(opt, extras) ← 产出 options(reactive)
│ opt: { id, name, columns, data?, sortConfig?, showFooter?, footerData?, footerMethod?, remoteCustom? }
│ extras: 任意 VxeGridProps(proxyConfig / pagerConfig / toolbarConfig ...),lodash merge 覆盖
├─ <vxe-grid ref="xGrid" v-bind="options" v-on="gridEvents" />
├─ useGridQuery(xGrid, options) ← 返回 (reset = false) => commitProxy('reload' | 'query')
└─ gridEvents = { ...gridPageEvents(key, param), cellDblclick(){}, ... }useVxeTable 参数表
第一个参数 opt: iVxeOption
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 表格唯一标识;为空时取 name。必须固定,否则列宽/列序本地缓存失效 |
name | string | 表格名称,用于导出文件名 参数配置导出_20251018120000 |
columns | CustomColumnOptions<any>[] | 列配置,见下文 |
data | VxeTablePropTypes.Data | 给了 data 就不生成 proxyConfig(本地数据模式) |
sortConfig | VxeTablePropTypes.SortConfig | 不传则默认 { remote: true } |
showFooter / footerData / footerMethod | - | 表尾合计 |
remoteCustom | boolean | true 时列定制走服务端接口 apiSysColumnCustom* |
第二个参数 extras: VxeGridProps<T>
可覆盖/追加任何官方属性,用 merge(options, extras) 深合并。常用:
{
proxyConfig: { autoLoad: true, ajax: { query: ({ page, sort }) => handleQueryApi(page, sort) } },
sortConfig: { defaultSort: localPageParam.defaultSort },
pagerConfig: { pageSize: localPageParam.pageSize },
toolbarConfig: { export: false },
}默认配置清单(useVxeTable 内置)
| 配置 | 默认值 | 备注 |
|---|---|---|
stripe | false | 全局 setupVXETable 里是 true,这里逐表覆盖为 false |
height | 'auto' | 配合 .full-table 撑满卡片 |
autoResize | true | 父容器变化时重算 |
size | mini / small / medium | 由 themeConfig.globalComponentSize 映射 |
align | 'center' | 单元格居中 |
toolbarConfig | refresh / export / print / zoom / custom 全开,refreshOptions.code = 'query' | 插槽 buttons→toolbar_buttons、tools→toolbar_tools |
checkboxConfig | { range: true } | 复选框范围选择 |
rowClassName | row.activated === false → row-deactivate-hightlight | 停用行高亮 |
exportConfig | { remote: false, original: false, filename: '${name}导出_${时间戳}', modes: ['current','selected','all'] } | 本地导出 |
pagerConfig | { enabled: true, size, pageSize: 50 } | 默认每页 50 |
printConfig | { sheetName: '' } | - |
customConfig.storage | { visible: true, resizable: true, sort: true, fixed: true } | 本地持久化列状态 |
proxyConfig(无 data 时) | { seq: tablePageSequence, enabled: true, autoLoad: false, sort: true, response: { list: 'data.result', result: 'data.result.items', total: 'data.result.total', message: 'data.message' } } | 服务端分页响应映射 |
全局默认(
hooks/setupVXETableHook.ts)里还有:边框、圆角、showOverflow: true、列最小宽 70、虚拟滚动virtualXConfig.gt = 30/virtualYConfig.gt = 50、pagerpageSizes: [10,20,50,100,200,500,1000]。
列配置
type CustomColumnOptions<D> = VxeGridPropTypes.Columns<D>[number] & {
dictExportConfig?: { code: string; multiple?: boolean }; // 字典导出配置
children?: CustomColumnOptions<D>[]; // 嵌套列(多级表头)
};真实列定义(src/views/system/config/index.vue):
columns: [
{ field: 'seq', type: 'seq', title: '序号', width: 60, fixed: 'left' },
{ field: 'name', title: '配置名称', minWidth: 200, headerAlign: 'center', align: 'left', showOverflow: 'tooltip', sortable: true },
{ field: 'sysFlag', title: '内置参数', width: 80, showOverflow: 'tooltip', sortable: true, slots: { default: 'row_sysFlag' } },
{ field: 'record', title: '修改记录', width: 100, showOverflow: 'tooltip', slots: { default: 'row_record' } },
{ field: 'buttons', title: '操作', fixed: 'right', width: 'auto', showOverflow: true, slots: { default: 'row_buttons' } },
]行插槽命名约定:row_字段名
slots: { default: 'row_xxx' } → 模板里写 <template #row_xxx="{ row }">。项目内已成惯例的有:
| 插槽 | 用途 | 常见实现 |
|---|---|---|
row_buttons | 行操作按钮组 | el-tooltip + el-button(text),按钮挂 v-auth="'xxx/update'" |
row_record | 修改记录 | <ModifyRecord :data="row" />(hover 弹 el-descriptions) |
row_状态字段 | 状态标签 | el-tag 按值切 type |
| 空数据 | #empty | <el-empty :image-size="200" /> |
<template #row_buttons="{ row }">
<el-tooltip content="编辑" placement="top">
<el-button icon="ele-Edit" text type="primary" v-auth="'sysConfig/update'" @click="handleEdit(row)" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button icon="ele-Delete" text type="danger" v-auth="'sysConfig/delete'" :disabled="row.sysFlag === 1" @click="handleDelete(row)" />
</el-tooltip>
</template>dictExportConfig
声明该列导出时按字典转文案:
{ field: 'status', title: '状态', width: 100, dictExportConfig: { code: 'sys_status' } },
{ field: 'tags', title: '标签', width: 160, dictExportConfig: { code: 'sys_tag', multiple: true } },⚠️ 当前 useVxeTable 中自动注入 exportMethod 的 processColumns 逻辑处于注释状态(useVxeTableOptionsHook.ts:93-121)。也就是说现在 dictExportConfig 只是「声明式占位」,若要生效需自己在列上写 exportMethod:
{
field: 'status', title: '状态',
exportMethod: ({ row }) => useUserInfo().getDictItemByValue('sys_status', String(row.status))?.label ?? row.status,
}工具栏插槽
<vxe-grid ref="xGrid" v-bind="options" v-on="gridEvents">
<template #toolbar_buttons>
<el-button type="primary" icon="ele-Plus" @click="handleAdd" v-auth="'sysConfig/add'"> 新增 </el-button>
</template>
<template #toolbar_tools></template>
<template #empty><el-empty :image-size="200" /></template>
</vxe-grid>工具栏内置按钮:refresh(code = query,会触发 proxyConfig.ajax.query)、export、print、zoom、custom。
分页 / 排序 / 偏好持久化
// ① 必须在 state 与 options 之前调用:sortConfig / pagerConfig 在 setup 阶段就要拿到恢复后的值
const { key: localPageParamKey, param: localPageParam } = useGridPageParam('sysConfig', {
pageSize: 50 as number,
defaultSort: { field: 'orderNo', order: 'asc', descStr: 'desc' },
});
const state = reactive({ queryParams: { name: undefined }, localPageParam, /* ... */ });
const options = useVxeTable<SysConfig>({ id: 'sysConfig', name: '参数配置', columns: [...] }, {
proxyConfig: { autoLoad: true, ajax: { query: ({ page, sort }) => handleQueryApi(page, sort) } },
sortConfig: { defaultSort: localPageParam.defaultSort },
pagerConfig: { pageSize: localPageParam.pageSize },
});
// ② 查询(reset=true 回第一页;false 为当前页刷新)
const handleQuery = useGridQuery(xGrid, options);
// ③ 事件:分页/排序变化写回 Local
const gridEvents: VxeGridListeners<SysConfig> = {
...gridPageEvents(localPageParamKey, localPageParam),
async cellDblclick({ row }) { if (auth('sysConfig/update')) await handleEdit(row); },
};三个函数的语义:
| 函数 | 作用 | 存储键 |
|---|---|---|
useGridPageParam(id, init) | 读 Local.get('localPageParam:' + id),与缺省值合并 | localPageParam:${id} |
gridPageEvents(key, param) | 返回 { pageChange, sortChange },把 pageSize / 排序写回 Local | 同上 |
useGridQuery(xGrid, options) | 返回 async (reset = false):reset ? commitProxy('reload') : commitProxy('query'),期间切 options.loading | - |
导出 / 打印 / 列定制
| 能力 | 默认 | 关闭/改写方式 |
|---|---|---|
| 导出 | 本地(remote: false),类型 xlsx/csv/html/xml/txt,模式 current/selected/all | extras.exportConfig = { modes: ['current'] };服务端导出设 remote: true 并在 proxyConfig.ajax 提供 queryAll |
| 打印 | printConfig: { sheetName: '' },模式 current/selected/all | extras.printConfig = { sheetName: '参数配置' } |
| 列定制(本地) | customConfig.storage 全开,存 localStorage,依赖固定 id | extras.customConfig = { storage: false } |
| 列定制(服务端) | 需显式开启 | opt.remoteCustom = true |
开启服务端列定制
const options = useVxeTable<SysUser>(
{ id: 'sysUser', name: '用户管理', columns: [...], remoteCustom: true },
{ proxyConfig: { autoLoad: true, ajax: { query: ({ page, sort }) => handleQueryApi(page, sort) } } }
);remoteCustom: true 后 hook 会重写两个钩子,改用 SysColumnCustomApi:
customConfig.restoreStore = async ({ id, storeData }) => {
const { data } = await getAPI(SysColumnCustomApi).apiSysColumnCustomDetailGet(id);
storeData.fixedData = data.result?.fixedData ?? storeData.fixedData;
storeData.resizableData = data.result?.resizableData ?? storeData.resizableData;
storeData.sortData = data.result?.sortData ?? storeData.sortData;
storeData.visibleData = data.result?.visibleData ?? storeData.visibleData;
return storeData;
};
customConfig.updateStore = async ({ id, type, storeData }) => {
if (type === 'reset') await getAPI(SysColumnCustomApi).apiSysColumnCustomResetPost({ gridId: id });
else await getAPI(SysColumnCustomApi).apiSysColumnCustomStorePost({ gridId: id, ...storeData });
};⚠️ 服务端列定制的 gridId 就是 opt.id,换 id = 换一套用户配置,改名要谨慎。
性能:虚拟滚动
setupVXETableHook.ts 已全局开启:
virtualXConfig: { enabled: true, gt: 30 }, // 列数 > 30 自动横向虚拟滚动
virtualYConfig: { enabled: true, gt: 50, scrollToTopOnChange: true }, // 行数 > 50 自动纵向虚拟滚动单表调优:scrollY: { enabled: true, gt: 100 } 提高阈值、scrollX: { enabled: false } 关闭横向虚拟(列少且用合并表头时)。其它要点:大表格不要给每个单元格塞复杂组件(用 showOverflow: 'tooltip' + 纯文本);行内插槽里的组件(如 ModifyRecord)保持轻量;数据量极大时 showOverflow: true 比 'tooltip' 更快。
完整表格配置示例(可直接复制改)
<template>
<div class="sys-demo-container">
<el-card shadow="hover" :body-style="{ padding: '5px 5px 0 5px', display: 'flex', width: '100%', height: '100%', alignItems: 'start' }">
<el-form :model="state.queryParams" ref="queryForm" :show-message="false" :inlineMessage="true" label-width="auto" style="flex: 1 1 0%">
<el-row :gutter="10">
<el-col class="mb5" :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="名称" prop="name">
<el-input v-model="state.queryParams.name" placeholder="名称" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-divider style="height: calc(100% - 5px); margin: 0 10px" direction="vertical" />
<el-row>
<el-col>
<el-button-group>
<el-button type="primary" icon="ele-Search" @click="handleQuery(true)" v-auth="'sysDemo/page'" :loading="options.loading"> 查询 </el-button>
<el-button icon="ele-Refresh" @click="resetQuery" :loading="options.loading"> 重置 </el-button>
</el-button-group>
</el-col>
</el-row>
</el-card>
<el-card class="full-table" shadow="hover" style="margin-top: 5px">
<vxe-grid ref="xGrid" class="xGrid-style" v-bind="options" v-on="gridEvents">
<template #toolbar_buttons>
<el-button type="primary" icon="ele-Plus" @click="handleAdd" v-auth="'sysDemo/add'"> 新增 </el-button>
</template>
<template #toolbar_tools></template>
<template #empty><el-empty :image-size="200" /></template>
<template #row_status="{ row }">
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
<template #row_record="{ row }"><ModifyRecord :data="row" /></template>
<template #row_buttons="{ row }">
<el-tooltip content="编辑" placement="top">
<el-button icon="ele-Edit" text type="primary" v-auth="'sysDemo/update'" @click="handleEdit(row)"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button icon="ele-Delete" text type="danger" v-auth="'sysDemo/delete'" @click="handleDelete(row)"></el-button>
</el-tooltip>
</template>
</vxe-grid>
</el-card>
<EditDemo ref="editDemoRef" :title="state.title" @updateData="updateData" />
</div>
</template>
<script lang="ts" setup name="sysDemo">
import { reactive, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { VxeGridInstance, VxeGridListeners, VxeGridPropTypes } from 'vxe-table';
import { useVxeTable, useGridPageParam, gridPageEvents, useGridQuery } from '/@/hooks/useVxeTableOptionsHook';
import { auth } from '/@/utils/authFunction';
import ModifyRecord from '/@/components/table/modifyRecord.vue';
import EditDemo from '/@/views/system/demo/component/editDemo.vue';
import { getAPI } from '/@/utils/axios-utils';
import { SysDemoApi } from '/@/api-services/system/api';
import { SysDemo, PageDemoInput } from '/@/api-services/system/models';
const xGrid = ref<VxeGridInstance>();
const editDemoRef = ref<InstanceType<typeof EditDemo>>();
// ① 先恢复本地偏好
const { key: localPageParamKey, param: localPageParam } = useGridPageParam('sysDemo', {
pageSize: 50 as number,
defaultSort: { field: 'orderNo', order: 'asc', descStr: 'desc' },
});
const state = reactive({
queryParams: { name: undefined },
localPageParam,
title: '',
});
// ② 表格参数
const options = useVxeTable<SysDemo>(
{
id: 'sysDemo',
name: '示例管理',
columns: [
{ field: 'seq', type: 'seq', title: '序号', width: 60, fixed: 'left' },
{ field: 'name', title: '名称', minWidth: 200, headerAlign: 'center', align: 'left', showOverflow: 'tooltip', sortable: true },
{ field: 'status', title: '状态', width: 80, showOverflow: 'tooltip', sortable: true, slots: { default: 'row_status' } },
{ field: 'orderNo', title: '排序', width: 80, showOverflow: 'tooltip', sortable: true },
{ field: 'record', title: '修改记录', width: 100, showOverflow: 'tooltip', slots: { default: 'row_record' } },
{ field: 'buttons', title: '操作', fixed: 'right', width: 'auto', showOverflow: true, slots: { default: 'row_buttons' } },
],
},
{
proxyConfig: { autoLoad: true, ajax: { query: ({ page, sort }) => handleQueryApi(page, sort) } },
sortConfig: { defaultSort: localPageParam.defaultSort },
pagerConfig: { pageSize: localPageParam.pageSize },
toolbarConfig: { export: true },
}
);
// ③ 查询 API:返回值需满足 proxyConfig.response 映射(data.result.items / data.result.total)
const handleQueryApi = async (page: VxeGridPropTypes.ProxyAjaxQueryPageParams, sort: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams) => {
const params = Object.assign(state.queryParams, {
page: page.currentPage,
pageSize: page.pageSize,
field: sort.field,
order: sort.order,
descStr: 'desc',
}) as PageDemoInput;
return getAPI(SysDemoApi).apiSysDemoPagePost(params);
};
const handleQuery = useGridQuery(xGrid, options);
const resetQuery = async () => {
state.queryParams.name = undefined;
await xGrid.value?.commitProxy('reload');
};
const handleAdd = () => { state.title = '添加示例'; editDemoRef.value?.openDialog({ orderNo: 100 }); };
const handleEdit = (row: any) => { state.title = '编辑示例'; editDemoRef.value?.openDialog(row); };
const handleDelete = (row: any) => {
ElMessageBox.confirm(`确定删除:${row.name}?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
.then(async () => {
await getAPI(SysDemoApi).apiSysDemoDeletePost({ id: row.id });
handleQuery();
ElMessage.success('删除成功');
})
.catch(() => {});
};
const gridEvents: VxeGridListeners<SysDemo> = {
...gridPageEvents(localPageParamKey, localPageParam),
async cellDblclick({ row }) { if (auth('sysDemo/update')) await handleEdit(row); },
};
const updateData = async () => { await handleQuery(); };
</script>步骤清单:给新页面加一张表
useGridPageParam('业务id', { pageSize: 50, defaultSort: {...} })—— 放在state之前。- 写
columns,行插槽统一row_字段名。 useVxeTable({ id, name, columns }, { proxyConfig, sortConfig, pagerConfig })。v-bind="options" v-on="gridEvents"+#toolbar_buttons/#toolbar_tools/#empty三个插槽。useGridQuery(xGrid, options)得到handleQuery,点查询传true,增删改后不传。gridEvents展开gridPageEvents(key, param),再叠加自己的事件。- 需要跨设备同步列配置时加
remoteCustom: true。
⚠️ 注意事项 / 常见坑
id必须固定且全局唯一:本地列缓存、服务端列定制都以它为主键。useGridPageParam必须先于options调用,否则sortConfig.defaultSort拿到的是默认值。opt.data与proxyConfig互斥:传了data就是本地数据,不会再生成proxyConfig。- 默认失败不中断:接口报错只是弹提示,
handleQueryApi的返回值仍会被 proxy 消费;需要中断请在ajax.query里抛错。 - 表格高度:第二个
el-card必须带class="full-table",否则height: 'auto'撑不满、滚动条跑到页面上。 width: 'auto'只适合操作列;改列名 / 增删列后用户本地缓存的visibleData可能失配,必要时改id或引导点工具栏「重置」。
小结
- 表格参数全部走
useVxeTable,默认配置已覆盖 90% 场景,只写差异即可。 - 分页排序偏好自动化:
useGridPageParam+gridPageEvents+useGridQuery三件套。 - 行插槽遵守
row_字段名约定,操作列放row_buttons、修改记录放row_record。 - 列定制默认存本地,跨设备用
remoteCustom: true接SysColumnCustomApi。