同学们好,我是 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 | 接口返回的当前页列表 |
| 分页信息 | 组件内 ref | page、pageSize、total |
| 加载中 | 组件内 ref | loading 控制表格和按钮 |
三、实战规范:代码怎么组织
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 页,容易看到空列表。
解决:handleSearch 和 handleReset 里都要先 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-change 和 current-change 同时触发,发两次请求。
解决:确认分页组件事件绑定正确,必要时用防抖。Element Plus 的 v-model 绑定一般不会重复触发,注意不要重复监听。
5.5 接口字段与前端期望不一致
现象:data.list、data.total 拿不到。
解决:和接口约定好字段名,或在接口层做一层映射:
const { data } = await getUserList(params)
tableData.value = data.records || data.list || []
pagination.total = data.total ?? 0
六、小结
- 搜索、分页共用同一个
fetchData,参数来自searchForm+pagination。 - 搜索、重置、切换每页条数时,都把
page设为 1。 - 表格只负责展示和行内操作,勾选、加载由组件状态控制。
- 状态集中在组件内,结构清晰,便于维护和扩展。
按这个思路写「搜索 + 表格 + 分页」页面,代码会更好读、更好改。如果你用 Vue 2,把 script setup 换成 Options API,逻辑不变;用 Vue 3 + Element Plus + VXE Table 时,可以直接参考本文的结构和示例。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~