同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。
这篇文章面向:
- 会用 JS,但表格相关概念还有点模糊的前端
- 从零开始学表格组件的同学
- 有几年经验、想系统梳理表格用法和踩坑的前端
重点不是底层原理,而是:日常该用哪些配置、为什么这么配、容易在哪里出错。文中会尽量用完整示例和说明把每一步讲清楚。
一、为什么选 VXE-Table?
1.1 一句话定位
VXE-Table 是面向 Vue 2/3 的企业级表格组件,自带:
- 固定列、合并单元格、行内编辑
- 虚拟滚动、远程分页、树形表格
- 导入导出、打印、列筛选、列拖拽等
1.2 和 Element Plus 的 Table 怎么选?
| 场景 | 更推荐 |
|---|---|
| 简单列表展示 | Element Plus Table |
| 需要大量编辑、合并、虚拟滚动、复杂分页 | VXE-Table |
| 现有项目已大量使用 Element Table | 可继续用,复杂表格再单独上 VXE |
结论:VXE-Table 更偏「数据密集 + 复杂交互」,适合后台管理系统里的业务表格。
二、环境准备:安装与引入
2.1 安装
bash
# 使用 npm
npm install vxe-table --save
# 使用 pnpm(推荐,更快)
pnpm add vxe-table
2.2 全局引入(推荐新手)
在 main.js 或 main.ts 中:
javascript
import { createApp } from 'vue'
import App from './App.vue'
import VxeTable from 'vxe-table'
import 'vxe-table/lib/style.css'
const app = createApp(App)
app.use(VxeTable)
app.mount('#app')
为什么全局引入?
一是不用每个页面重复
import;二是表格经常和工具栏、分页器等一起用,全局注册写起来更干净。
2.3 版本与环境要求
- Vue 3:建议 Vue 3.2+
- Vue 2:使用
vxe-table@3.x - 示例均以 Vue 3 + Composition API 为主,Options API 写法也可照搬。
三、第一步:从普通表格开始
3.1 最基础的表格
html
<template>
<vxe-table border :data="tableData">
<vxe-column type="seq" width="60" title="序号"></vxe-column>
<vxe-column field="name" title="姓名"></vxe-column>
<vxe-column field="sex" title="性别"></vxe-column>
<vxe-column field="age" title="年龄"></vxe-column>
</vxe-table>
</template>
<script setup>
import { ref } from 'vue'
const tableData = ref([
{ id: 10001, name: '张三', sex: '男', age: 28 },
{ id: 10002, name: '李四', sex: '女', age: 22 },
{ id: 10003, name: '王五', sex: '男', age: 35 }
])
</script>
border:显示边框,视觉更清晰type="seq":自动生成序号列field:字段名,和数据的 key 对应title:列头文案
3.2 自定义列渲染
需要复杂展示时,用插槽:
html
<vxe-column field="status" title="状态">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</vxe-column>
#default="{ row }":作用域插槽,row为当前行数据- 复杂列(状态、操作等)多用这种写法
四、固定列:左右冻结
4.1 配置方式
列多、需要横向滚动时,通常把「序号、姓名」固定左侧,「操作」固定右侧:
html
<template>
<vxe-table border :data="tableData" height="400">
<!-- 左侧固定:序号和姓名 -->
<vxe-column type="seq" width="60" fixed="left" title="序号"></vxe-column>
<vxe-column field="name" title="姓名" width="120" fixed="left"></vxe-column>
<!-- 中间可滚动区域 -->
<vxe-column field="dept" title="部门" width="150"></vxe-column>
<vxe-column field="phone" title="电话" width="150"></vxe-column>
<vxe-column field="email" title="邮箱" width="200"></vxe-column>
<vxe-column field="address" title="地址" width="250"></vxe-column>
<!-- 右侧固定:操作列 -->
<vxe-column title="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
</template>
</vxe-column>
</vxe-table>
</template>
<script setup>
import { ref } from 'vue'
const tableData = ref([
{ id: 1, name: '张三', dept: '技术部', phone: '13800138000', email: 'zhangsan@xx.com', address: '北京市朝阳区xxx' },
// ... 更多数据
])
const handleEdit = (row) => {
console.log('编辑', row)
}
</script>
要点:
fixed="left"/fixed="right":左/右固定- 要出现横向滚动,表格需设
height或max-height,且中间列总宽度大于可视区域
4.2 固定列常见坑
坑 1:固定列不生效
- 原因:未设置
height/max-height,没有出现滚动条 - 处理:给表格加
height="400"或max-height="500"
坑 2:虚拟滚动 + 固定列对不齐
- 大表时 VXE 会开虚拟滚动,旧版本可能出现固定列与中间列错位
- 处理:升级到 v4.12+ / v3.14+;或数据量不大时关闭虚拟滚动,避免复杂组合
五、合并单元格:两种方式怎么选
5.1 方式一:spanMethod(适合小数据量)
适合 1000 行以内,逻辑清晰、可读性好:
html
<template>
<vxe-table border :data="tableData" :span-method="spanMethod">
<vxe-column type="seq" width="60" title="序号"></vxe-column>
<vxe-column field="orderNo" title="订单号" width="150"></vxe-column>
<vxe-column field="productName" title="商品名称" width="200"></vxe-column>
<vxe-column field="quantity" title="数量" width="80"></vxe-column>
<vxe-column field="amount" title="金额" width="100"></vxe-column>
</vxe-table>
</template>
<script setup>
import { ref } from 'vue'
const tableData = ref([
{ orderNo: 'ORD001', productName: '商品A', quantity: 2, amount: 100 },
{ orderNo: 'ORD001', productName: '商品B', quantity: 1, amount: 50 },
{ orderNo: 'ORD001', productName: '商品C', quantity: 3, amount: 150 },
{ orderNo: 'ORD002', productName: '商品D', quantity: 1, amount: 80 },
])
// 合并逻辑:相同订单号的行,订单号列纵向合并
function spanMethod({ row, rowIndex, column, columnIndex, $rowIndex, data }) {
const field = column.property
if (field === 'orderNo') {
const cellValue = row[field]
const prevRow = data[$rowIndex - 1]
const nextRow = data[$rowIndex + 1]
// 上一行相同:当前格被合并,隐藏
if (prevRow && prevRow[field] === cellValue) {
return { rowspan: 0, colspan: 0 }
}
// 往下数有几行相同
let rowspan = 1
while (nextRow && data[$rowIndex + rowspan]?.[field] === cellValue) {
rowspan++
}
return { rowspan, colspan: 1 }
}
return { rowspan: 1, colspan: 1 }
}
</script>
rowspan: 0, colspan: 0:该单元格不显示(被上方合并)rowspan: 2:占 2 行- 只在需要合并的列(如
orderNo)做判断,其他列返回{ rowspan: 1, colspan: 1 }
5.2 方式二:mergeCells(适合大数据量)
数据量上千、且开启了虚拟滚动时,用 mergeCells 更稳:
html
<template>
<vxe-table border :data="tableData" :merge-cells="mergeCells">
<!-- 列定义同上 -->
</vxe-table>
</template>
<script setup>
const mergeCells = ref([
{ row: 0, col: 1, rowspan: 3, colspan: 1 }, // 第1列,从第1行起合并3行
{ row: 3, col: 1, rowspan: 1, colspan: 1 }, // 下一个订单
])
</script>
row、col:起始行、列索引(从 0 开始)rowspan、colspan:合并行数、列数- 数据变化时要重新计算
mergeCells并赋值
5.3 合并单元格怎么选?
| 场景 | 推荐 |
|---|---|
| 几百行以内 | spanMethod |
| 几千行 + 虚拟滚动 | mergeCells |
| 合并整行/整列 | 尽量避免,会影响虚拟滚动性能 |
六、行内编辑:cell 与 row 模式
6.1 两种编辑模式
- cell 模式:点哪个格子编辑哪个
- row 模式:点「编辑」后整行一起编辑
6.2 示例:行编辑模式(带保存/取消)
html
<template>
<vxe-table ref="tableRef" border :data="tableData" :edit-config="editConfig">
<vxe-column type="seq" width="60" title="序号"></vxe-column>
<vxe-column field="name" title="姓名" :edit-render="{ name: 'VxeInput' }"></vxe-column>
<vxe-column field="sex" title="性别" :edit-render="{ name: 'VxeSelect', options: sexOptions }"></vxe-column>
<vxe-column field="age" title="年龄" :edit-render="{ name: 'VxeInput', props: { type: 'number' } }"></vxe-column>
<vxe-column title="操作" width="180" fixed="right">
<template #default="{ row }">
<template v-if="!$refs.tableRef?.isEditByRow(row)">
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
</template>
<template v-else>
<el-button size="small" type="success" @click="handleSave(row)">保存</el-button>
<el-button size="small" @click="handleCancel(row)">取消</el-button>
</template>
</template>
</vxe-column>
</vxe-table>
</template>
<script setup>
import { ref, reactive } from 'vue'
const tableRef = ref()
const tableData = ref([
{ id: 1, name: '张三', sex: '1', age: 28 },
{ id: 2, name: '李四', sex: '2', age: 22 },
])
const sexOptions = [
{ label: '男', value: '1' },
{ label: '女', value: '2' }
]
const editConfig = reactive({
trigger: 'manual', // 手动触发,不点不进入编辑
mode: 'row', // 行编辑模式
showStatus: true
})
const handleEdit = (row) => {
tableRef.value.setEditRow(row)
}
const handleSave = async (row) => {
try {
await tableRef.value.validateRow(row)
const $table = tableRef.value
const { row: newRow } = $table.getEditRecord(row)
console.log('保存的数据:', newRow)
$table.clearEdit()
// 这里调接口保存...
} catch (e) {
console.error('校验失败', e)
}
}
const handleCancel = (row) => {
tableRef.value.revertData(row)
tableRef.value.clearEdit()
}
</script>
trigger: 'manual':只有点击「编辑」才进入编辑mode: 'row':整行编辑:edit-render:指定输入组件(VxeInput、VxeSelect 等)setEditRow:激活编辑validateRow:校验当前行revertData:还原数据
6.3 单元格编辑模式(点击即编辑)
javascript
const editConfig = {
trigger: 'click', // 或 'dblclick' 双击
mode: 'cell',
}
适合简单列表、少量字段的快速修改。
6.4 编辑相关踩坑
- 必填校验:用
edit-rules或在validateRow前自己做校验 - 下拉选项:
edit-render里options要保证有值,异步加载的选项要等加载完再显示表格
七、远程分页:proxyConfig + pagerConfig
7.1 为什么用 proxyConfig?
传统做法:手动请求接口 → 拿到数据 → 塞给 :data。
使用 proxyConfig 后:表格根据分页、排序、筛选自动发请求,你只需写一个 query 方法。
7.2 完整示例
html
<template>
<vxe-grid
ref="gridRef"
border
:columns="columns"
:pager-config="pagerConfig"
:proxy-config="proxyConfig"
:toolbar-config="{ slots: { buttons: 'toolbar_buttons' } }"
>
<template #toolbar_buttons>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</template>
</vxe-grid>
</template>
<script setup>
import { ref, reactive } from 'vue'
const gridRef = ref()
const columns = [
{ type: 'seq', width: 60, title: '序号' },
{ field: 'name', title: '姓名', width: 120 },
{ field: 'dept', title: '部门', width: 150 },
{ field: 'createTime', title: '创建时间', width: 180 }
]
const pagerConfig = reactive({
pageSize: 10,
pageSizes: [10, 20, 50, 100],
layouts: ['PrevPage', 'JumpNumber', 'NextPage', 'Sizes', 'Total']
})
const proxyConfig = reactive({
props: {
list: 'data', // 接口返回的列表字段名
total: 'total' // 接口返回的总数字段名
},
ajax: {
query: async ({ page, sort, filters }) => {
const params = {
pageNum: page.currentPage,
pageSize: page.pageSize,
sortField: sort?.field,
sortOrder: sort?.order
}
const res = await fetch('/api/user/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
})
const json = await res.json()
return json
}
}
})
const handleSearch = () => {
gridRef.value?.commitProxy('query')
}
const handleReset = () => {
// 重置查询条件后
gridRef.value?.commitProxy('reload')
}
</script>
props.list/props.total:适配你们接口的返回结构query返回整份响应,VXE 会按list、total取数commitProxy('query'):按当前分页/排序重新请求commitProxy('reload'):回到第 1 页并刷新
7.3 接口返回格式示例
json
{
"code": 0,
"data": [
{ "id": 1, "name": "张三", "dept": "技术部", "createTime": "2024-01-01" }
],
"total": 100
}
若字段不同,只需改 props:
javascript
props: {
list: 'result.list',
total: 'result.total'
}
7.4 远程分页常见坑
query必须返回包含list、total的结构,或通过props映射- 带查询条件时,
query里拿不到外部变量,可把条件存到form/ ref,在query里读取 - 需要强制刷新:
commitProxy('reload')
八、综合示例:固定列 + 合并 + 编辑 + 远程分页
把前面几个能力拼在一起的一个综合示例结构:
html
<template>
<vxe-grid
ref="gridRef"
border
height="500"
:columns="columns"
:pager-config="pagerConfig"
:proxy-config="proxyConfig"
:edit-config="editConfig"
:span-method="spanMethod"
>
</vxe-grid>
</template>
<script setup>
const columns = [
{ type: 'seq', width: 60, fixed: 'left', title: '序号' },
{ field: 'orderNo', title: '订单号', width: 150, fixed: 'left' },
{ field: 'productName', title: '商品', width: 200 },
{ field: 'quantity', title: '数量', width: 100, editRender: { name: 'VxeInput' } },
{ field: 'amount', title: '金额', width: 120 },
{ title: '操作', width: 120, fixed: 'right', slots: { default: 'action' } }
]
const editConfig = { trigger: 'click', mode: 'cell' }
const spanMethod = ({ row, $rowIndex, column, data }) => {
if (column.field === 'orderNo') {
// 同上的 orderNo 合并逻辑
const prevRow = data[$rowIndex - 1]
if (prevRow && prevRow.orderNo === row.orderNo) {
return { rowspan: 0, colspan: 0 }
}
let rowspan = 1
while (data[$rowIndex + rowspan]?.orderNo === row.orderNo) rowspan++
return { rowspan, colspan: 1 }
}
return { rowspan: 1, colspan: 1 }
}
const pagerConfig = { pageSize: 10 }
const proxyConfig = {
props: { list: 'data', total: 'total' },
ajax: {
query: ({ page }) => fetchList(page.currentPage, page.pageSize)
}
}
</script>
注意:同时用 spanMethod 和虚拟滚动时,数据量不宜过大,或考虑改用 mergeCells。
九、踩坑速查
| 现象 | 可能原因 | 处理思路 |
|---|---|---|
| 固定列不生效 | 没有设置 height | 给表格加 height / max-height |
| 合并单元格后很卡 | 数据量大 + spanMethod | 改用 mergeCells 或减少数据 |
| 编辑后校验不触发 | 未调用 validateRow | 保存前调用 validateRow |
| 远程分页无数据 | list/total 字段名不对 | 检查 proxyConfig.props |
| 固定列错位 | 旧版本虚拟滚动问题 | 升级到 v4.12+ 或关闭虚拟滚动 |
十、小结
- 简单列表:普通
vxe-table+:data即可 - 列多:用
fixed="left"/fixed="right"固定关键列 - 需合并:小数据用
spanMethod,大数据用mergeCells - 行内编辑:
editConfig+edit-render,按业务选 cell/row - 远程分页:
proxyConfig+pagerConfig,接口按list、total返回
先掌握这几块,再按项目需求查文档扩展(树形、导入导出、打印等)。实战中遇到具体报错或交互问题,可以把现象和配置贴出来,再针对性排查。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~