Vue3 中后台实战:VXE-Table 从基础表格到复杂业务表格全攻略 | Vue生态精选篇

同学们好,我是 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.jsmain.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":左/右固定
  • 要出现横向滚动,表格需设 heightmax-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>
  • rowcol:起始行、列索引(从 0 开始)
  • rowspancolspan:合并行数、列数
  • 数据变化时要重新计算 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-renderoptions 要保证有值,异步加载的选项要等加载完再显示表格

七、远程分页: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 会按 listtotal 取数
  • 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 必须返回包含 listtotal 的结构,或通过 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,接口按 listtotal 返回

先掌握这几块,再按项目需求查文档扩展(树形、导入导出、打印等)。实战中遇到具体报错或交互问题,可以把现象和配置贴出来,再针对性排查。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
SuperEugene4 小时前
Vue3 中后台实战:Element + VXE Table 搜索表格分页完整方案 | Vue生态精选篇
前端·javascript·vue.js
欧哥讼4 小时前
当我问AI如何熟练掌握表单验证时
前端
德鲁叔叔4 小时前
vite前端项目运行时切换代理
前端
亿元程序员5 小时前
老板说最近这款游戏很火让我抄,可是我连玩都玩不明白...
前端
谢小飞5 小时前
如何让AI用一个下午开发上架Chrome插件助我摸鱼
前端·chrome
gyx_这个杀手不太冷静5 小时前
OpenCode 进阶使用指南(第一章:Agent 模式)
前端·javascript·ai编程
树上有只程序猿5 小时前
继续堆无用代码,真的不如早点用Low code
前端·低代码
wuhen_n5 小时前
computed 的缓存哲学:如何避免不必要的重复计算?
前端·javascript·vue.js
闲云一鹤5 小时前
本地部署 B 站 IndexTTS2 模型 - AI 文本生语音神器
前端·人工智能