Vue3 + Element Plus 表格查询规范:条件管理、分页联动 + 避坑,标准化写法|表单与表格规范篇

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

📑 文章目录


同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

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

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。


一、开篇:表格查询的三个核心问题

在实际业务里,表格查询常见有三个问题:

  1. 查询条件:条件怎么存、怎么和接口对应、怎么校验
  2. 重置:点重置后列表会不会乱、分页要不要清、防抖会不会残留
  3. 分页联动:切换页/改每页条数时,要不要带条件、要不要清空页码

下面围绕这三个点,用 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-pagev-model:page-size 会直接改 paginationhandlePageChange / 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 时都带上当前条件请求,改 pageSizepage = 1
防抖 输入即查时用 useDebounceFn,按钮点击不必防抖
空值 不传的字段用 undefined,避免传空字符串

这套写法能覆盖大部分表格查询场景,结构清晰、易维护,也方便团队统一规范。

[⬆ 返回目录](#⬆ 返回目录)

🔍 系列模块导航

📝 编码语法规范

一、《Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇》
二、《Vue3 + Element Plus 表单校验实战:规则复用、自定义校验、提示语统一,告别混乱避坑|表单与表格规范篇》

三、《Vue3 + Element Plus 表格查询规范:条件管理、分页联动 + 避坑,标准化写法|表单与表格规范篇》
四、《Vue3 + Element Plus 表格实战:批量操作、行内编辑、跨页选中逻辑统一|表单与表格规范篇》
五、《VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。

更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
小邓睡不饱耶2 小时前
东方财富网股票数据爬取实战:从接口分析到数据存储
开发语言·爬虫·python·网络爬虫
dapeng28702 小时前
C++与Docker集成开发
开发语言·c++·算法
2501_945423542 小时前
C++中的策略模式实战
开发语言·c++·算法
2301_792308252 小时前
C++与自动驾驶系统
开发语言·c++·算法
问道飞鱼2 小时前
【前端知识】React生态你了解多少?
前端·react.js·前端框架·生态
Pu_Nine_92 小时前
前端SSE(Server-Sent Events)实现详解:从原理到前端AI对话应用
前端·langchain·sse·ai对话
hongtianzai2 小时前
Laravel8.x核心特性全解析
java·c语言·开发语言·golang·php
2401_874732532 小时前
模板编译期排序算法
开发语言·c++·算法
weixin_421922692 小时前
C++与Node.js集成
开发语言·c++·算法