Vue3 中后台实战:Element + VXE Table 搜索表格分页完整方案 | Vue生态精选篇

0 阅读8分钟

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

用 Element + VXE 组合,讲清楚:该怎么做、为什么要这么做、容易踩哪些坑。

一、开篇:为什么用 Element + VXE?

1.1 组合思路

组件库职责特点
Element UI搜索表单、按钮、布局、对话框成熟稳定,表单、布局生态完善
VXE Table表格展示、分页虚拟滚动、导出、可编辑等能力更强

Element 自带的 el-table 适合简单场景;当有大量数据、虚拟滚动、导出、可编辑等需求时,用 VXE Table 更合适。

1.2 本文能帮你解决什么

  • 不清楚「搜索 → 请求 → 表格 → 分页」的数据流
  • 分页、搜索、表格三块状态纠缠在一起
  • 首次加载和「点击搜索」「翻页」到底该怎么发请求
  • 导出、批量操作和表格如何配合

二、基础扫盲:黄金三角怎么运转

2.1 三个模块的职责

┌─────────────────────────────────────────┐
│  搜索区:收集筛选条件,触发「查询」      
├─────────────────────────────────────────┤
│  表格区:展示当前页数据,支持行操作     
├─────────────────────────────────────────┤
│  分页区:切换页码/每页条数,触发「查询」
└─────────────────────────────────────────┘
  • 搜索区:用户填写条件 → 点击查询 → 回到第 1 页重新拉数据
  • 表格区:展示当前页数据,行内操作(编辑、删除等)
  • 分页区:改页码或每页条数 → 用当前搜索条件重新拉数据

2.2 数据流要清晰(关键!)

很多混乱来源于:不知道「什么时候」该请求接口。

用户操作              触发动作              接口参数
──────────────────────────────────────────────────────────────────────
1. 进入页面       →   fetchData()      →  page=1, size=10, 空条件
2. 点击「搜索」   →   handleSearch()   →  page=1, size=10, 当前条件
3. 切换页码       →   handlePageChange →  page=新页码, size, 当前条件
4. 切换每页条数   →   handleSizeChange →  page=1, size=新值, 当前条件

原则:搜索、分页都用同一套查询逻辑,只是参数不同。

2.3 状态放在哪

状态建议说明
搜索条件组件内 ref / reactive只影响本页,不必全局
表格数据组件内 ref接口返回的当前页列表
分页信息组件内 refpagepageSizetotal
加载中组件内 refloading 控制表格和按钮

三、实战规范:代码怎么组织

3.1 目录结构建议

views/
  user/
    index.vue          # 主页面
    components/
      SearchForm.vue   # 搜索表单(可选拆分)
      UserTable.vue    # 表格组件(可选拆分)
api/
  user.js              # 接口封装

单页场景可以把搜索、表格、分页都放在一个 index.vue,逻辑清楚即可。

3.2 搜索区规范

原则:表单双向绑定,查询时传当前条件 + 分页信息。

<template>
  <el-form :model="searchForm" inline class="search-form">
    <el-form-item label="用户名">
      <el-input v-model="searchForm.username" placeholder="请输入" clearable />
    </el-form-item>
    <el-form-item label="状态">
      <el-select v-model="searchForm.status" placeholder="请选择" clearable>
        <el-option label="启用" value="1" />
        <el-option label="禁用" value="0" />
      </el-select>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" :loading="loading" @click="handleSearch">搜索</el-button>
      <el-button @click="handleReset">重置</el-button>
    </el-form-item>
  </el-form>
</template>

要点:

  • clearable:方便清空
  • 搜索按钮加 loading:防止重复点击
  • 重置:清空表单,并重新拉取第 1 页数据

3.3 表格区规范(VXE Table)

原则:表格只负责展示和行内操作,不承担请求逻辑。

<vxe-table
  border
  :data="tableData"
  :loading="loading"
  :checkbox-config="{ checkRowKeys: selectedIds }"
  @checkbox-change="handleSelectionChange"
  @checkbox-all="handleSelectionChange"
>
  <vxe-column type="checkbox" width="50" />
  <vxe-column field="username" title="用户名" />
  <vxe-column field="status" title="状态">
    <template #default="{ row }">
      <el-tag :type="row.status === '1' ? 'success' : 'info'">
        {{ row.status === '1' ? '启用' : '禁用' }}
      </el-tag>
    </template>
  </vxe-column>
  <vxe-column field="createTime" title="创建时间" width="180" />
  <vxe-column title="操作" width="180" fixed="right">
    <template #default="{ row }">
      <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
      <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
    </template>
  </vxe-column>
</vxe-table>

要点:

  • border 提升可读性
  • loading 由统一的 fetchData 控制
  • 勾选事件只更新 selectedIds,不在这里发请求

3.4 分页规范

原则:页码、每页条数变化时,都调用同一套查询逻辑。

<el-pagination
  v-model:current-page="pagination.page"
  v-model:page-size="pagination.pageSize"
  :total="pagination.total"
  :page-sizes="[10, 20, 50, 100]"
  layout="total, sizes, prev, pager, next, jumper"
  @size-change="handleSizeChange"
  @current-change="handlePageChange"
/>

注意:size-change 时要把 page 重置为 1,否则可能出现“当前页超出范围”的问题。

四、完整示例(可直接跑)

4.1 依赖安装

npm install element-plus vxe-table xe-utils

4.2 按需引入(main.js 或 main.ts)

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import VxeTable from 'vxe-table'
import 'vxe-table/lib/style.css'
import App from './App.vue'

const app = createApp(App)
app.use(ElementPlus)
app.use(VxeTable)
app.mount('#app')

4.3 接口封装(api/user.js)

import request from '@/utils/request'  // 假设你有 axios 封装

export function getUserList(params) {
  return request({
    url: '/api/user/list',
    method: 'get',
    params
  })
}

export function deleteUser(id) {
  return request({
    url: `/api/user/${id}`,
    method: 'delete'
  })
}

4.4 完整页面(views/user/index.vue)

<template>
  <div class="page-container">
    <!-- 1. 搜索区 -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="用户名">
          <el-input v-model="searchForm.username" placeholder="请输入" clearable style="width: 180px" />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 120px">
            <el-option label="全部" value="" />
            <el-option label="启用" value="1" />
            <el-option label="禁用" value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" :loading="loading" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 2. 表格区 -->
    <el-card class="table-card">
      <vxe-toolbar>
        <template #buttons>
          <el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">批量删除</el-button>
        </template>
      </vxe-toolbar>
      <vxe-table
        border
        :data="tableData"
        :loading="loading"
        :checkbox-config="{ checkRowKeys: selectedIds }"
        @checkbox-change="handleSelectionChange"
        @checkbox-all="handleSelectionChange"
      >
        <vxe-column type="checkbox" width="50" />
        <vxe-column field="id" title="ID" width="80" />
        <vxe-column field="username" title="用户名" />
        <vxe-column field="status" title="状态">
          <template #default="{ row }">
            <el-tag :type="row.status === '1' ? 'success' : 'info'">
              {{ row.status === '1' ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </vxe-column>
        <vxe-column field="createTime" title="创建时间" width="180" />
        <vxe-column title="操作" width="180" fixed="right">
          <template #default="{ row }">
            <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
          </template>
        </vxe-column>
      </vxe-table>

      <!-- 3. 分页区 -->
      <div class="pagination-wrap">
        <el-pagination
          v-model:current-page="pagination.page"
          v-model:page-size="pagination.pageSize"
          :total="pagination.total"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handlePageChange"
        />
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUserList, deleteUser } from '@/api/user'

// 搜索表单
const searchForm = reactive({
  username: '',
  status: ''
})

// 表格数据 & 加载状态
const tableData = ref([])
const loading = ref(false)

// 分页信息
const pagination = reactive({
  page: 1,
  pageSize: 10,
  total: 0
})

// 已勾选行
const selectedIds = ref([])

// 核心:统一的查询方法
const fetchData = async () => {
  loading.value = true
  try {
    const { data } = await getUserList({
      ...searchForm,
      page: pagination.page,
      pageSize: pagination.pageSize
    })
    tableData.value = data.list || []
    pagination.total = data.total || 0
  } catch (err) {
    ElMessage.error('获取数据失败')
  } finally {
    loading.value = false
  }
}

// 搜索:回到第 1 页再查
const handleSearch = () => {
  pagination.page = 1
  fetchData()
}

// 重置:清空条件,回到第 1 页再查
const handleReset = () => {
  searchForm.username = ''
  searchForm.status = ''
  pagination.page = 1
  fetchData()
}

// 切换每页条数:回到第 1 页
const handleSizeChange = () => {
  pagination.page = 1
  fetchData()
}

// 切换页码
const handlePageChange = () => {
  fetchData()
}

// 勾选变化
const handleSelectionChange = ({ records }) => {
  selectedIds.value = records.map(r => r.id)
}

// 编辑 / 删除(示例)
const handleEdit = (row) => {
  console.log('编辑', row)
}
const handleDelete = async (row) => {
  await ElMessageBox.confirm('确定删除?')
  await deleteUser(row.id)
  ElMessage.success('删除成功')
  fetchData()  // 删除后刷新当前页
}
const handleBatchDelete = () => {
  console.log('批量删除', selectedIds.value)
}

onMounted(() => {
  fetchData()
})
</script>

<style scoped>
.page-container { padding: 20px; }
.search-card { margin-bottom: 16px; }
.table-card { margin-bottom: 16px; }
.pagination-wrap { margin-top: 16px; text-align: right; }
</style>

说明:

  • fetchData 是所有查询的入口,搜索、重置、分页都调用它
  • 搜索、重置、size-change 时都把 page 设为 1
  • 删除后调用 fetchData() 刷新当前页

五、常见踩坑点

5.1 搜索后分页没回到第 1 页

现象:搜索后 total 变了,但还停留在第 5 页,容易看到空列表。

解决:handleSearchhandleReset 里都要先 pagination.page = 1,再 fetchData()

5.2 切换每页条数后页码没重置

现象:原来是第 5 页 10 条,改成 50 条/页后,第 5 页可能超出范围。

解决:handleSizeChange 里先 pagination.page = 1,再 fetchData()

5.3 VXE 勾选和 Element 混用导致样式问题

现象:表格勾选样式异常。
解决:使用 VXE 的 checkbox-config@checkbox-change,不要混用 Element 的 el-table 多选。

5.4 重复请求

现象:size-changecurrent-change 同时触发,发两次请求。
解决:确认分页组件事件绑定正确,必要时用防抖。Element Plus 的 v-model 绑定一般不会重复触发,注意不要重复监听。

5.5 接口字段与前端期望不一致

现象:data.listdata.total 拿不到。
解决:和接口约定好字段名,或在接口层做一层映射:

const { data } = await getUserList(params)
tableData.value = data.records || data.list || []
pagination.total = data.total ?? 0

六、小结

  1. 搜索、分页共用同一个 fetchData,参数来自 searchForm + pagination
  2. 搜索、重置、切换每页条数时,都把 page 设为 1。
  3. 表格只负责展示和行内操作,勾选、加载由组件状态控制。
  4. 状态集中在组件内,结构清晰,便于维护和扩展。

按这个思路写「搜索 + 表格 + 分页」页面,代码会更好读、更好改。如果你用 Vue 2,把 script setup 换成 Options API,逻辑不变;用 Vue 3 + Element Plus + VXE Table 时,可以直接参考本文的结构和示例。


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

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

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

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

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