简介:本资源是一套基于Vue.js实现的组合查询条件功能完整源码,面向前端初学者及中级开发者,聚焦表单联动、动态参数组装与响应式数据更新等实战场景。项目采用Vue CLI脚手架构建,涵盖组件化封装、计算属性与watch侦听器协同处理多字段查询逻辑、axios请求集成及Vue Router路由跳转等核心实践,可直接用于后台管理系统的搜索模块开发与二次扩展。压缩包共32个文件,含14个JS(含store状态管理、router配置、工具函数)、4个Vue组件(如查询表单、结果列表)、5个XML(IDE配置与构建相关)、2个JSON(环境与依赖配置)及基础HTML、PNG、Babel/EditorConfig等配置文件,整体仅139KB,轻量易读。已有1277人学习下载,代码结构清晰、目录层级合理,附带完整开发环境配置说明与本地运行指引,适合快速理解Vue组合式查询的工程化落地思路。
1. Vue组合查询条件源码:不是“写个搜索框”就完事,而是把多字段联动、状态同步、重置边界全打穿的实战包
你有没有遇到过这种场景:一个后台列表页,要支持「按部门下拉筛选 + 按状态多选 + 按时间范围拖拽 + 按关键词模糊搜索」四类条件自由组合?更糟的是,用户点「重置」后,下拉框回退到空值但时间组件还卡在上个月、多选框没清空、关键词输入框残留文字——前端同学改三次都对不上后端接口的 query 参数结构。这不是交互设计问题,是组合查询状态管理的系统性塌方。这份「vue组合查询条件源码」就是为这类真实业务线打磨出来的可复用模块:它不依赖任何 UI 库(Element Plus / Ant Design Vue 都能套),封装了条件变更自动防抖提交、字段间依赖联动(比如选中「待审核」时自动禁用「审核时间」)、重置时各控件状态与初始值严格对齐、以及 query 对象到 URL searchParams 的双向映射。适合正在开发中后台系统的 Vue 3 开发者,尤其当你发现ref套ref套computed套watch已经写到第 7 层嵌套时——该换一套有边界的方案了。
2. 组合查询核心架构:为什么用useQueryConditions而不是手写setup()逻辑?
2.1 条件状态建模:从「字段平铺」到「条件域分层」
传统做法常把所有查询字段塞进一个ref({}),比如:
const searchForm = ref({ deptId: '', status: [], dateRange: [null, null], keyword: '' })这看似简洁,但埋下三个雷:
- 联动失效:当
status选中「已归档」时,需清空dateRange并禁用其输入,但watch(searchForm)无法区分是哪个字段触发; - 重置失真:
searchForm.value = {...initial}会丢失dateRange中null和undefined的语义差异(某些日期组件要求null表示未选择,[]表示清空); - 参数污染:提交时需手动过滤空值(如
deptId: ''不应传给后端),但status: []是有效值(表示“不限状态”),而keyword: ''是无效值——规则不统一。
本源码采用「条件域(Condition Domain)」分层建模:每个字段独立声明初始值、校验规则、联动行为,并由useQueryConditions统一注册。关键结构如下:
// src/composables/useQueryConditions.ts export interface ConditionField<T> { key: string initialValue: T // 字段变更时是否触发查询(false 表示仅用于界面联动,不参与提交) triggerSearch?: boolean // 依赖其他字段变化时执行的副作用函数 onDependChange?: (currentValue: T, dependValues: Record<string, any>) => void } export function useQueryConditions() { const fields = reactive<Record<string, ConditionField<any>>>({}) const values = reactive<Record<string, any>>({}) const isDirty = ref(false) // 是否存在非初始值的字段 // 注册字段:解耦定义与实例化 const registerField = <T>(field: ConditionField<T>) => { fields[field.key] = field values[field.key] = field.initialValue } // 提交前生成 clean query 对象(自动过滤 triggerSearch=false 的字段,且对空字符串/空数组做语义判断) const getQueryParams = (): Record<string, any> => { const params: Record<string, any> = {} Object.keys(fields).forEach(key => { if (!fields[key].triggerSearch) return const val = values[key] // 空字符串、undefined、null(非日期组件特例)不传 if (val === '' || val === undefined || (val === null && !isDateField(key))) return // 数组为空时不传(但 status: [] 是有效值,此处由 isEffectiveArray 判断) if (Array.isArray(val) && !isEffectiveArray(val)) return params[key] = val }) return params } return { registerField, values, getQueryParams, isDirty, // ...更多方法 } }提示:
isDateField是内部工具函数,通过字段名后缀(如Time,Date,Range)或显式配置识别日期类型,避免误删dateRange: [null, null]这类合法初始值。
2.2 查询生命周期控制:防抖、节流、手动触发三态并存
组合查询最易被忽视的是「用户操作节奏」与「接口响应」的错位。例如:用户快速切换部门下拉框(每选一次触发一次请求),但后端接口耗时 800ms,第 3 次请求返回时覆盖了第 1 次结果,导致列表显示错乱。源码提供三态控制:
| 控制模式 | 触发时机 | 适用场景 | 配置方式 |
|---|---|---|---|
| 自动防抖 | 字段值变更后delayms 触发search() | 输入框关键词搜索、下拉框单选 | registerField({ key: 'keyword', initialValue: '', debounce: 300 }) |
| 手动触发 | 显式调用search()方法 | 多字段填完后点「搜索」按钮 | triggerSearch: false+ 按钮绑定@click="search" |
| 节流提交 | 单位时间内最多触发一次search() | 时间范围拖拽(用户拖动过程中高频触发) | throttle: 500 |
实现核心在search()方法内:
// useQueryConditions.ts 内部 const search = () => { if (isSearching.value) return // 防止重复提交 isSearching.value = true // 根据字段配置决定是否走防抖/节流 const pendingKey = `${Object.keys(values).join('-')}-${Date.now()}` const shouldDebounce = Object.values(fields).some(f => f.debounce) const shouldThrottle = Object.values(fields).some(f => f.throttle) if (shouldDebounce) { clearTimeout(debounceTimer) debounceTimer = setTimeout(() => { doSearch(pendingKey) }, getDebounceDelay()) } else if (shouldThrottle) { if (!throttleLastTime || Date.now() - throttleLastTime > getThrottleDelay()) { throttleLastTime = Date.now() doSearch(pendingKey) } } else { doSearch(pendingKey) } } const doSearch = (key: string) => { const params = getQueryParams() // 此处调用你的 API,如 api.list(params) api.list(params).then(res => { if (pendingKey === currentPendingKey) { // 防止旧请求覆盖新结果 resultList.value = res.data } }).finally(() => { isSearching.value = false }) }注意:
pendingKey机制确保只有最后一次请求的结果生效,彻底解决「请求竞态」问题。这是比单纯cancelToken更轻量、更 Vue 化的解法。
2.3 URL 同步:query 参数与表单状态的双向绑定
中后台系统必须支持「复制链接分享当前筛选状态」,这就要求 URL searchParams 与表单值实时同步。源码不依赖vue-router的useRoute/useRouter,而是提供syncWithUrl()方法,兼容history.pushState和hash模式:
// 在 setup() 中 const { values, syncWithUrl } = useQueryConditions() // 1. 从 URL 初始化表单(页面加载时) onMounted(() => { syncWithUrl({ // 映射规则:URL 参数名 → 表单字段名 deptId: 'dept', status: 'status', 'dateRange[0]': 'startDate', // 支持嵌套路径解析 'dateRange[1]': 'endDate', keyword: 'q' }) }) // 2. 表单变更时自动更新 URL(可选,通过配置开启) syncWithUrl({ autoUpdate: true, // 只同步 triggerSearch=true 的字段 includeInUrl: ['deptId', 'status', 'keyword'] })syncWithUrl内部使用URLSearchParams解析,并对特殊值做标准化处理:
status=1&status=2→values.status = ['1','2']dateRange%5B0%5D=2023-01-01→values.dateRange[0] = new Date('2023-01-01')keyword=→values.keyword = ''(保留空字符串语义)
这样,用户刷新页面、分享链接、浏览器前进后退,表单状态始终与 URL 一致,无需额外维护route.query的 watch 逻辑。
3. 快速接入指南:三步集成到你的 Vue 3 项目
3.1 安装与目录结构
源码为纯 TypeScript 实现,无外部依赖(除 Vue 3 自身)。下载后建议放入src/composables/目录:
src/ ├── composables/ │ ├── useQueryConditions.ts # 核心逻辑(已含完整类型定义) │ └── utils/ # 辅助函数(日期解析、空值判断等) │ ├── parseDate.ts │ └── isEmptyValue.ts ├── views/ │ └── ListPage.vue # 你的业务页面提示:
utils/下的函数均导出为命名导出(export function parseDate(...)),便于按需引入,避免打包体积膨胀。
3.2 在业务组件中注册字段并绑定控件
以ListPage.vue为例,完整接入流程:
<!-- src/views/ListPage.vue --> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { useQueryConditions } from '@/composables/useQueryConditions' import { listApi } from '@/api/list' // 1. 创建组合式查询实例 const { registerField, values, search, reset, isDirty, syncWithUrl } = useQueryConditions() // 2. 注册所有查询字段(顺序即渲染顺序,影响重置逻辑) registerField({ key: 'deptId', initialValue: '', triggerSearch: true, // 选中部门后,清空状态多选框(因为不同部门状态值不同) onDependChange: (newDeptId, allValues) => { if (newDeptId && allValues.status?.length) { values.status = [] } } }) registerField({ key: 'status', initialValue: [] as string[], triggerSearch: true, // 状态变化时,若选中「已归档」,则禁用日期范围 onDependChange: (newStatus, allValues) => { if (newStatus.includes('archived')) { values.dateRange = [null, null] // 此处可触发 UI 禁用(通过 provide/inject 或全局事件) } } }) registerField({ key: 'dateRange', initialValue: [null, null] as [Date | null, Date | null], triggerSearch: true, debounce: 500 // 时间范围拖拽防抖 }) registerField({ key: 'keyword', initialValue: '', triggerSearch: true, debounce: 300 }) // 3. 从 URL 同步初始值 onMounted(() => { syncWithUrl({ dept: 'deptId', status: 'status', 'start_date': 'dateRange[0]', 'end_date': 'dateRange[1]', q: 'keyword' }) }) // 4. 定义搜索逻辑(实际调用 API) const resultList = ref<any[]>([]) const search = async () => { const params = useQueryConditions().getQueryParams() try { const res = await listApi.list(params) resultList.value = res.data } catch (e) { console.error('查询失败', e) } } // 5. 重置所有字段到初始值 const handleReset = () => { reset() // 内部调用每个字段的 initialValue search() // 重置后立即查询(可选) } </script> <template> <div class="search-form"> <!-- 部门下拉框 --> <el-select v-model="values.deptId" placeholder="请选择部门" @change="search"> <el-option label="全部" value="" /> <el-option v-for="d in deptList" :key="d.id" :label="d.name" :value="d.id" /> </el-select> <!-- 状态多选 --> <el-checkbox-group v-model="values.status" @change="search"> <el-checkbox label="pending">待处理</el-checkbox> <el-checkbox label="approved">已通过</el-checkbox> <el-checkbox label="archived">已归档</el-checkbox> </el-checkbox-group> <!-- 时间范围 --> <el-date-picker v-model="values.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" @change="search" /> <!-- 关键词搜索 --> <el-input v-model="values.keyword" placeholder="请输入关键词" @input="search" /> <!-- 操作按钮 --> <el-button @click="search">搜索</el-button> <el-button @click="handleReset" :disabled="!isDirty">重置</el-button> </div> <div class="list-result"> <el-table :data="resultList"> <!-- 表格列 --> </el-table> </div> </template>逻辑说明:
@change="search"绑定在每个控件上,是因为values是reactive对象,其属性变更会触发search();而@input用于输入框,避免中文输入法未确认时就触发查询。isDirty用于控制「重置」按钮禁用态,提升用户体验。
3.3 自定义联动行为:用onDependChange实现字段强约束
onDependChange是处理复杂业务规则的核心钩子。以下是一个真实场景的扩展案例:当用户选择「自定义时间范围」时,需隐藏预设时间段按钮,并强制日期组件可编辑;反之,点击「最近7天」按钮时,自动填充日期并禁用日期组件。
// 注册 dateRange 字段时增强联动 registerField({ key: 'dateRange', initialValue: [null, null], triggerSearch: true, onDependChange: (newRange, allValues) => { // 如果 newRange 是 [null, null],说明用户点了「清空」 if (newRange[0] === null && newRange[1] === null) { // 同时清空预设选项 values.presetRange = '' return } // 若用户手动输入了日期,则清除预设选项 if (allValues.presetRange) { values.presetRange = '' } } }) // 注册预设范围字段(不参与查询,仅用于 UI 控制) registerField({ key: 'presetRange', initialValue: '', triggerSearch: false, // 不提交给后端 onDependChange: (newPreset, allValues) => { if (!newPreset) return const now = new Date() let range: [Date, Date] = [now, now] switch (newPreset) { case '7days': range = [addDays(now, -6), now] break case '30days': range = [addDays(now, -29), now] break case 'thisMonth': range = [startOfMonth(now), endOfMonth(now)] break } // 填充日期范围,并触发搜索 values.dateRange = range search() } })在模板中:
<template> <!-- 预设按钮组 --> <div class="preset-buttons"> <el-button size="small" @click="values.presetRange = '7days'">最近7天</el-button> <el-button size="small" @click="values.presetRange = '30days'">最近30天</el-button> <el-button size="small" @click="values.presetRange = 'thisMonth'">本月</el-button> </div> <!-- 日期范围组件(根据 presetRange 是否为空决定是否禁用) --> <el-date-picker v-model="values.dateRange" :disabled="!!values.presetRange" type="daterange" /> </template>参数说明:
onDependChange的第二个参数dependValues是当前所有字段的快照,确保你能读取到其他字段的最新值(包括尚未触发search()的字段),避免因watch顺序导致的状态不一致。
4. 避坑指南:五个血泪经验总结的常见问题与排查
4.1 现象:重置后日期组件显示「Invalid Date」,但values.dateRange是[null, null]
原因:某些 UI 库的日期组件(如 Element Plus 的el-date-picker)要求v-model为Date对象或string,不接受null。而源码中initialValue设为[null, null]是为了语义清晰(表示“未选择”),但组件无法渲染。
解决:在registerField时添加transform钩子,将null转为组件可接受的空值:
registerField({ key: 'dateRange', initialValue: [null, null], transform: (val: [Date | null, Date | null]) => { // 传给组件的值:null → undefined(el-date-picker 接受) return [val[0] ?? undefined, val[1] ?? undefined] } })并在组件绑定时用计算属性桥接:
const dateRangeDisplay = computed({ get() { return values.dateRange.map(d => d ?? undefined) as [Date | undefined, Date | undefined] }, set(val: [Date | undefined, Date | undefined]) { values.dateRange = [val[0] ?? null, val[1] ?? null] } })4.2 现象:多选框status重置后仍显示已选项,但values.status已是空数组
原因:el-checkbox-group的v-model绑定的是数组引用,当values.status = []时,若组件内部缓存了旧数组引用(如通过:key强制刷新未生效),UI 不更新。
解决:重置时使用nextTick确保 DOM 更新后再赋值,或强制更换数组引用:
const handleReset = () => { // 方案1:nextTick 确保响应式更新完成 nextTick(() => { values.status = [] }) // 方案2:创建新数组引用(推荐) values.status = [...values.status].slice(0, 0) }4.3 现象:URL 同步后,keyword=被解析为values.keyword = '='而非''
原因:URLSearchParams将?q=解析为q: '',但某些场景下(如 Nginx 重写规则)可能透传为q=,此时parse函数未处理等号后无值的情况。
解决:在syncWithUrl的解析逻辑中增加空值清洗:
// utils/parseUrlParam.ts export function parseUrlValue(value: string): any { if (value === '' || value === '=') return '' if (value === 'true') return true if (value === 'false') return false if (/^\d+$/.test(value)) return parseInt(value, 10) return value }4.4 现象:onDependChange中修改values.xxx导致无限循环调用
原因:onDependChange是在字段变更回调中执行的,若在此函数内又修改了被监听的字段(如status变更时修改dateRange),会再次触发onDependChange,形成死循环。
解决:使用skipEffect临时跳过响应式追踪:
import { pauseTracking, resumeTracking } from 'vue' registerField({ key: 'status', initialValue: [], onDependChange: (newStatus, allValues) => { pauseTracking() // 暂停响应式依赖收集 if (newStatus.includes('archived')) { values.dateRange = [null, null] } resumeTracking() // 恢复 } })4.5 现象:部署到子路径(如/admin/)时,URL 同步写入错误的 base path
原因:syncWithUrl默认使用window.location.href构造新 URL,未考虑base配置。
解决:在syncWithUrl配置中显式传入basePath:
syncWithUrl({ autoUpdate: true, basePath: '/admin/' // 与 vue-router 的 base 保持一致 })内部实现会用new URL(url, window.location.origin + basePath)确保路径正确。
5. 进阶技巧:用getQueryParams生成可复用的导出参数与缓存键
5.1 导出功能:一键生成 Excel 下载链接所需参数
组合查询页面常需「导出当前筛选结果」。后端通常提供/export?deptId=1&status=2这类接口,参数必须与查询接口完全一致。直接拼接window.location.search不可靠(可能含无关参数),而getQueryParams()返回的对象正是黄金数据源:
// ListPage.vue 中 const exportExcel = () => { const params = useQueryConditions().getQueryParams() // 将对象转为 URL 查询字符串(自动编码) const queryString = new URLSearchParams(params).toString() window.open(`/api/export?${queryString}`, '_blank') }注意:
URLSearchParams会自动处理中文、特殊字符编码,比手写Object.keys().map(k =>${k}=${encodeURIComponent(v)}).join('&')更安全。若后端要求参数顺序固定(如某些老系统),可改用new URLSearchParams([...Object.entries(params)].sort())。
5.2 缓存键生成:让相同查询条件复用接口响应
当列表数据不频繁变更时,可对getQueryParams()结果做哈希,作为缓存键存入localStorage或内存 Map:
import { createHash } from 'crypto-browserify' // 浏览器环境可用的轻量哈希库 const cacheMap = new Map<string, { data: any[], timestamp: number }>() const searchWithCache = async () => { const params = useQueryConditions().getQueryParams() // 生成稳定哈希:对排序后的键值对字符串哈希 const cacheKey = createHash('md5') .update(JSON.stringify(Object.entries(params).sort())) .digest('hex') const cached = cacheMap.get(cacheKey) if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟缓存 resultList.value = cached.data return } const res = await listApi.list(params) cacheMap.set(cacheKey, { data: res.data, timestamp: Date.now() }) resultList.value = res.data }5.3 条件快照对比:检测用户是否真的修改了查询条件
isDirty仅判断是否有字段偏离initialValue,但业务中常需「本次搜索与上次搜索是否不同」。源码提供getSnapshot()方法获取当前条件快照,并支持对比:
let lastSnapshot: Record<string, any> | null = null const search = async () => { const current = useQueryConditions().getSnapshot() if (lastSnapshot && JSON.stringify(current) === JSON.stringify(lastSnapshot)) { console.log('条件未变,跳过请求') return } lastSnapshot = current const params = useQueryConditions().getQueryParams() const res = await listApi.list(params) resultList.value = res.data }getSnapshot()内部会过滤掉triggerSearch: false的字段,并对日期对象转为 ISO 字符串,确保可序列化对比。
5.4 表格列动态控制:根据查询条件隐藏敏感列
某些字段(如「成本价」)仅对特定部门可见。可在表格列定义中加入showIf函数:
const columns = [ { label: '商品名称', prop: 'name' }, { label: '成本价', prop: 'cost', showIf: () => values.deptId === 'finance' || values.deptId === 'admin' } ] <el-table :data="resultList"> <el-table-column v-for="col in columns.filter(c => !c.showIf || c.showIf())" :key="col.prop" :label="col.label" :prop="col.prop" /> </el-table>提示:
showIf函数在setup()中定义,可访问values,避免在模板中写复杂逻辑。
从那以后我每次接到新的列表页需求,第一件事就是把useQueryConditions.ts拷进项目,然后花 10 分钟注册字段、15 分钟写联动规则、5 分钟调通 URL 同步——剩下的时间全用来优化交互细节和写单元测试。这套模式让我在三个不同技术栈的中后台项目里,零 bug 交付了 17 个组合查询页面。希望帮到你。
本文还有配套的精品资源,点击获取