【Vue3+Element Plus】中后台表格查询场景:从条件管理、重置逻辑到分页联动,掌握标准化查询交互写法,避开响应式丢失、分页错乱、空值传参等高频坑!

📑 文章目录
- 一、开篇:表格查询的三个核心问题
- 二、环境与依赖
- 三、查询条件:用一个对象统一管理
- [3.1 为什么要用一个对象?](#3.1 为什么要用一个对象?)
- [3.2 推荐:统一查询对象](#3.2 推荐:统一查询对象)
- 四、重置逻辑:只清条件,分页回第一页
- [4.1 常见误区](#4.1 常见误区)
- [4.2 正确做法:逐字段重置 + 分页归位](#4.2 正确做法:逐字段重置 + 分页归位)
- 五、分页联动:切换页、改每页条数时带条件
- [5.1 要联动什么?](#5.1 要联动什么?)
- [5.2 Element Plus 分页写法](#5.2 Element Plus 分页写法)
- 六、完整示例:一个可复用的表格查询页面
- 七、常见踩坑与对应处理
- [7.1 防抖:避免频繁请求](#7.1 防抖:避免频繁请求)
- [7.2 空字符串和 undefined](#7.2 空字符串和 undefined)
- [7.3 日期范围格式](#7.3 日期范围格式)
- [7.4 重置时表单校验](#7.4 重置时表单校验)
- 八、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、开篇:表格查询的三个核心问题
在实际业务里,表格查询常见有三个问题:
- 查询条件:条件怎么存、怎么和接口对应、怎么校验
- 重置:点重置后列表会不会乱、分页要不要清、防抖会不会残留
- 分页联动:切换页/改每页条数时,要不要带条件、要不要清空页码
下面围绕这三个点,用 Vue3 + Element Plus 给出一套可直接落地的写法。
[⬆ 返回目录](#⬆ 返回目录)
二、环境与依赖
示例基于:
- Vue 3.x
- Element Plus
- Vue Router(如用到)
- 支持
async/await的现代浏览器
bash
# 如需从零创建
npm create vue@latest
# 安装 Element Plus
npm install element-plus
[⬆ 返回目录](#⬆ 返回目录)
三、查询条件:用一个对象统一管理
3.1 为什么要用一个对象?
错误写法示例:
js
// ❌ 不推荐:分散在多个 ref 里
const keyword = ref('')
const status = ref('')
const dateRange = ref([])
const page = ref(1)
const pageSize = ref(10)
// 调用接口时要手动拼
const fetchList = () => {
api.getList({
keyword: keyword.value,
status: status.value,
startDate: dateRange.value?.[0],
endDate: dateRange.value?.[1],
page: page.value,
pageSize: pageSize.value
})
}
问题在于:
- 字段一多,传参容易漏
- 重置时要一个个清,逻辑分散
- 后续加字段要改多处
更推荐:用一个对象统一管理查询条件。
[⬆ 返回目录](#⬆ 返回目录)
3.2 推荐:统一查询对象
html
<script setup>
import { ref, reactive, computed } from 'vue'
// 1. 查询条件统一放在一个对象里
const queryForm = reactive({
keyword: '',
status: '',
dateRange: [] // 日期范围,Element Plus DatePicker 常用 []
})
// 2. 分页单独拆出来,因为重置时只清条件,不清分页配置
const pagination = reactive({
page: 1,
pageSize: 10
})
// 3. 根据 queryForm + pagination 生成接口参数
const queryParams = computed(() => ({
keyword: queryForm.keyword?.trim() || undefined,
status: queryForm.status || undefined,
startDate: queryForm.dateRange?.[0] || undefined,
endDate: queryForm.dateRange?.[1] || undefined,
page: pagination.page,
pageSize: pagination.pageSize
}))
</script>
要点:
queryForm:所有和筛选相关的字段pagination:分页配置,和业务条件分开queryParams:把表单+分页转成接口需要的参数,便于统一传给接口
[⬆ 返回目录](#⬆ 返回目录)
四、重置逻辑:只清条件,分页回第一页
4.1 常见误区
js
// ❌ 错误1:重置时不清分页,导致"查不到数据"
const handleReset = () => {
queryForm.keyword = ''
queryForm.status = ''
queryForm.dateRange = []
// 忘记把 page 设为 1,用户可能还在第 5 页,结果列表为空
}
// ❌ 错误2:用 Object.assign 导致响应式丢失
const handleReset = () => {
Object.assign(queryForm, { keyword: '', status: '', dateRange: [] })
// 如果 queryForm 是 ref,这样可能无法触发视图更新
}
// ❌ 错误3:整个替换 queryForm,会打断表单的 v-model 绑定
const handleReset = () => {
queryForm = { keyword: '', status: '', dateRange: [] }
// 若 queryForm 是 reactive,直接赋值会丢失响应式
}
[⬆ 返回目录](#⬆ 返回目录)
4.2 正确做法:逐字段重置 + 分页归位
html
<script setup>
// 定义初始值,便于重置时复用
const getInitialQueryForm = () => ({
keyword: '',
status: '',
dateRange: []
})
const queryForm = reactive(getInitialQueryForm())
const pagination = reactive({
page: 1,
pageSize: 10
})
const handleReset = () => {
// 1. 逐个字段还原,保持响应式
const initial = getInitialQueryForm()
Object.keys(initial).forEach(key => {
queryForm[key] = initial[key]
})
// 2. 分页回到第一页
pagination.page = 1
// 3. 立即拉取列表(重置后的第一页)
fetchList()
}
</script>
这样做的效果:
- 条件被完整重置
- 分页回到第一页,避免"空列表"
- 仍然保持
reactive的响应式 - 使用
getInitialQueryForm()方便以后增减字段
[⬆ 返回目录](#⬆ 返回目录)
五、分页联动:切换页、改每页条数时带条件
5.1 要联动什么?
- 切换页码、改变
pageSize时,都要带上当前查询条件重新请求 - 改变
pageSize时,一般把page置为 1,避免越界
[⬆ 返回目录](#⬆ 返回目录)
5.2 Element Plus 分页写法
html
<template>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="tableData.total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</template>
<script setup>
const handlePageChange = (page) => {
pagination.page = page
fetchList()
}
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.page = 1 // 每页条数变了,回到第一页
fetchList()
}
</script>
注意:v-model:current-page 和 v-model:page-size 会直接改 pagination,handlePageChange / handleSizeChange 里只需要再调用 fetchList() 即可,此时 queryParams 里已经包含最新条件和分页。
[⬆ 返回目录](#⬆ 返回目录)
六、完整示例:一个可复用的表格查询页面
下面是一个可直接复用的单文件示例,覆盖查询、重置、分页联动和防抖。
html
<template>
<div class="table-query-demo">
<!-- 1. 查询表单 -->
<el-form :model="queryForm" inline class="query-form">
<el-form-item label="关键词" prop="keyword">
<el-input
v-model="queryForm.keyword"
placeholder="请输入关键词"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryForm.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 label="日期范围" prop="dateRange">
<el-date-picker
v-model="queryForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 2. 表格 -->
<el-table v-loading="loading" :data="tableData.list" border>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
{{ row.status === 1 ? '启用' : '禁用' }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
</el-table>
<!-- 3. 分页 -->
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="tableData.total"
layout="total, sizes, prev, pager, next, jumper"
class="pagination"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
// ========== 1. 查询条件 ==========
const getInitialQueryForm = () => ({
keyword: '',
status: '',
dateRange: []
})
const queryForm = reactive(getInitialQueryForm())
// ========== 2. 分页 ==========
const pagination = reactive({
page: 1,
pageSize: 10
})
// ========== 3. 接口参数(computed 自动同步) ==========
const queryParams = computed(() => ({
keyword: queryForm.keyword?.trim() || undefined,
status: queryForm.status || undefined,
startDate: queryForm.dateRange?.[0] || undefined,
endDate: queryForm.dateRange?.[1] || undefined,
page: pagination.page,
pageSize: pagination.pageSize
}))
// ========== 4. 列表数据 ==========
const loading = ref(false)
const tableData = reactive({
list: [],
total: 0
})
// ========== 5. 请求列表(模拟接口) ==========
const fetchList = async () => {
loading.value = true
try {
// 实际项目替换为真实 API
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
list: [
{ id: 1, name: '测试数据', status: 1, createTime: '2024-01-01' }
],
total: 100
}
}, 300)
})
})
tableData.list = res.data.list
tableData.total = res.data.total
} finally {
loading.value = false
}
}
// ========== 6. 查询 ==========
const handleSearch = () => {
pagination.page = 1
fetchList()
}
// ========== 7. 重置 ==========
const handleReset = () => {
const initial = getInitialQueryForm()
Object.keys(initial).forEach((key) => {
queryForm[key] = initial[key]
})
pagination.page = 1
fetchList()
}
// ========== 8. 分页切换 ==========
const handlePageChange = () => {
fetchList()
}
const handleSizeChange = () => {
pagination.page = 1
fetchList()
}
onMounted(() => {
fetchList()
})
</script>
<style scoped>
.table-query-demo {
padding: 20px;
}
.query-form {
margin-bottom: 16px;
}
.pagination {
margin-top: 16px;
justify-content: flex-end;
}
</style>
你可以直接复制到项目里,把 fetchList 里的模拟请求换成真实接口即可。
[⬆ 返回目录](#⬆ 返回目录)
七、常见踩坑与对应处理
7.1 防抖:避免频繁请求
js
import { useDebounceFn } from '@vueuse/core'
// 对 fetchList 做防抖,300ms
const fetchListDebounced = useDebounceFn(fetchList, 300)
// 关键词输入框变化时用防抖查询
const handleKeywordChange = () => {
pagination.page = 1
fetchListDebounced()
}
注意:查询 / 重置按钮通常不需要防抖,只有"输入框变化即查询"的场景才用。
[⬆ 返回目录](#⬆ 返回目录)
7.2 空字符串和 undefined
后端往往希望"没填"时不传该字段,而不是传空字符串:
js
// ✅ 用 undefined 表示"不传"
keyword: queryForm.keyword?.trim() || undefined
// ❌ 传空字符串可能影响后端逻辑
keyword: queryForm.keyword?.trim() ?? ''
[⬆ 返回目录](#⬆ 返回目录)
7.3 日期范围格式
js
// Element Plus DatePicker value-format="YYYY-MM-DD"
// queryForm.dateRange 为 ['2024-01-01', '2024-01-31']
startDate: queryForm.dateRange?.[0]
endDate: queryForm.dateRange?.[1]
需和后端约定的格式一致(如 YYYY-MM-DD 或时间戳)。
[⬆ 返回目录](#⬆ 返回目录)
7.4 重置时表单校验
若使用了 el-form 的校验规则:
js
const formRef = ref()
const handleReset = () => {
formRef.value?.resetFields()
pagination.page = 1
fetchList()
}
resetFields() 会按 prop 把对应字段还原为初始值,适合和 rules 一起使用。
[⬆ 返回目录](#⬆ 返回目录)
八、小结
| 场景 | 做法 |
|---|---|
| 查询条件 | 用 reactive 对象统一管理,computed 转成接口参数 |
| 重置 | 逐字段还原 + pagination.page = 1 + 重新请求 |
| 分页 | 切换页、改 pageSize 时都带上当前条件请求,改 pageSize 时 page = 1 |
| 防抖 | 输入即查时用 useDebounceFn,按钮点击不必防抖 |
| 空值 | 不传的字段用 undefined,避免传空字符串 |
这套写法能覆盖大部分表格查询场景,结构清晰、易维护,也方便团队统一规范。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 编码语法规范
一、《Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇》
二、《Vue3 + Element Plus 表单校验实战:规则复用、自定义校验、提示语统一,告别混乱避坑|表单与表格规范篇》
三、《Vue3 + Element Plus 表格查询规范:条件管理、分页联动 + 避坑,标准化写法|表单与表格规范篇》
四、《Vue3 + Element Plus 表格实战:批量操作、行内编辑、跨页选中逻辑统一|表单与表格规范篇》
五、《VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
「前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~