简介:这是一份面向 Vue.js 前端开发者的组合查询条件功能源码,演示如何将输入框、下拉选择、日期选择等表单控件封装为可复用组件,并动态拼接查询参数、请求后端数据并刷新列表。资源共 32 个文件,压缩后 139KB,以 14 个 js 文件、4 个 vue 文件为核心,包含 Vue CLI 工程配置、Vuex 状态管理、路由权限控制与 axios 请求封装;另附 json、xml、gitignore 等辅助配置,目录结构清晰,适合初学者对照完整项目理解组件通信、计算属性、侦听器和表单 v-model 的实际用法。已有 1277 人学习下载。通过阅读源码,可以掌握 Vue 项目从脚手架搭建到查询条件组合、数据请求、状态同步的完整链路;同时查询条件已按输入框、下拉、日期等类型拆分,便于直接复用或改造成后台管理系统的筛选模块。整体代码量适中,是学习 Vue 组件化开发与前后端联调思路的实用参考。
1. 组合查询条件源码:一个 Vue 查询组件的完整实现
给列表页做搜索条件的时候,我猜你大概率遇到过这样的需求:字段不固定、条件可增删、查询逻辑还能在 AND/OR 之间切换。如果后端同学再补上一句「条件组合你们前端拼好传给我」,那基本就宣告手写if/else时代结束。这份 vue 组合查询条件源码,就是把这些痛点收敛成一个现成的 Vue 组件方案——它不依赖后端,不绑定 UI 库,纯 Vue 实现动态条件行的增删改查,适合做中后台管理系统、数据报表筛选器和数据看板的高级搜索。打开源码包你会发现,核心其实就是一个组件加一个解析器,既能在 Vue 2 里跑,也能迁移到 Vue 3,属于那种「看懂了一张图,就能自己改出花」的项目。作为插件类资源,它比你自己从零拼条件表单要省下至少一个迭代周期。
2. 整体架构与数据模型:先看条件 DSL 怎么设计
2.1 目录结构:一个组件和一个解析器就能干活
解压源码后,整个项目的结构非常克制,没有把简单的事情复杂化。常见 Vue 工程的src目录在这里被浓缩成一个组件目录、一个工具目录和一个类型声明文件。我拆过不少类似项目,很多源码包喜欢堆十几个辅助文件,但这个项目的组织方式更接近实战团队的写法——把「条件渲染」和「条件解析」拆成两个独立关注点,其余逻辑都塞进各自的index.js。
src/ ├── components/ │ └── query-builder/ │ ├── index.vue // 组件入口,负责整体渲染与状态管理 │ ├── condition-row.vue // 单行条件组件,字段/运算符/值三栏联动 │ └── group.vue // 条件组组件,支持嵌套分组与 AND/OR 切换 ├── utils/ │ ├── parser.js // 将条件树转换成查询参数对象 │ └── validator.js // 校验条件完整性,跳过空行和未选字段 ├── types/ │ └── index.js // 条件节点、运算符枚举、字段配置表结构 ├── App.vue // 演示页面,展示组件双向绑定与查询触发 └── main.js // Vue 入口这个拆分思路值得借鉴:组件只管「用户看到的操作界面」,解析器负责「把界面状态翻译成后端能用的数据结构」。哪怕你之后要把项目改成 React 版本,parser.js和types/index.js几乎是零成本平移的。演示页App.vue里有完整的字段配置示例,把组件接入自己的业务时直接照抄这个配置结构就行。
2.2 条件的数据结构:字段、运算符、值的最小组合
查询条件的本质是「字段 + 运算符 + 值」的三元组。但这个项目的设计没有停留在三元组,而是把多个条件组合成一个树形结构,支持分组嵌套。每一层都带logic字段标记是 AND 还是 OR,这样后端拿到的数据天然就是可递归解析的表达式树。
// types/index.js export const LOGIC_MAP = { AND: 'and', OR: 'or' } export const OPERATOR_MAP = { eq: { label: '等于', symbol: '=' }, ne: { label: '不等于', symbol: '!=' }, gt: { label: '大于', symbol: '>' }, lt: { label: '小于', symbol: '<' }, contains: { label: '包含', symbol: 'like' }, between: { label: '区间', symbol: 'between' }, in: { label: '在列表中', symbol: 'in' } } export function createEmptyCondition() { return { field: '', operator: 'eq', value: '', // 一个条件节点内部也可以继续嵌套,形成树形结构 children: [], logic: LOGIC_MAP.AND, // 标记是否已经被用户修改过,用于校验时跳过低价值的空行 touched: false } }LOGIC_MAP和OPERATOR_MAP是整个组件的「字典表」。前端 UI 下拉框里的展示文案、提交给后端时用到的符号、以及条件校验时的逻辑判断,全都查这两张表。createEmptyCondition()工厂函数看起来简单,但touched这个字段很关键——用户刚点「添加条件」时会立刻生成一个空节点,但还没交互过,这时候如果你直接把它提交给后端,就会产生{ field: '', operator: 'eq', value: '' }这种脏数据。有了touched标记,校验器就能精准跳过「用户加了但没碰过」的空行。
括号嵌套的表示方式一般采用children数组。一行条件既可以承载自己的字段、运算符和值,也可以只有一个逻辑运算符和子条件组,等价于 SQL 里的(A OR B) AND C。这个递归结构是整棵条件树的骨架,后面的渲染、解析、回显全都在这个模型上做文章。顺带提醒一句,field的值不要存中文名,存字段的 key(比如createTime),中文标签在配置表里用label映射。很多初写这个组件的开发者会在field里直接塞中文,结果提交给后端还得再转换一次,属于比较典型的「绕远路」写法。
3. 核心模块拆解:从条件操作到查询参数生成的链路
3.1 条件行交互:增删行与字段联动
condition-row.vue是用户最常打交道的文件。它在界面上渲染成一行「字段下拉框 + 运算符下拉框 + 值输入框 + 删除按钮」,右侧通常还有一个「添加条件」按钮,点击后在当前行后面插入一个新的空条件。这个组件的设计核心是「受控组件」——它不自己管理数据,而是通过v-model接收外部传入的条件对象,再通过$emit通知父组件更新。
<template> <div class="condition-row"> <select :value="condition.field" @change="onFieldChange"> <option value="" disabled>请选择字段</option> <option v-for="field in fieldOptions" :key="field.key" :value="field.key" > {{ field.label }} </option> </select> <select :value="condition.operator" @change="onOperatorChange"> <option v-for="(op, key) in availableOperators" :key="key" :value="key" > {{ op.label }} </option> </select> <!-- 根据字段类型动态切换输入控件 --> <input v-if="inputType === 'text'" v-model="condition.value" type="text" placeholder="请输入值" /> <input v-else-if="inputType === 'number'" v-model="condition.value" type="number" placeholder="请输入数值" /> <input v-else-if="inputType === 'date'" v-model="condition.value" type="date" /> <button @click="$emit('remove', condition)">删除</button> </div> </template>onFieldChange的联动逻辑是这段代码的灵魂。用户切换字段时,组件会清空operator和value,避免上一个字段残留的运算符污染新字段的展示。实际操作中,我一般会在这里做一下字段type的判断,比如日期字段只给gt/lt/between,文本字段只给eq/contains,数字字段才开放全部运算符——参考下面这段伪代码:
onFieldChange(event) { const fieldKey = event.target.value const fieldConfig = this.findFieldConfig(fieldKey) // 重置运算符为默认值,避免新旧字段语义冲突 this.$emit('update:condition', { ...this.condition, field: fieldKey, operator: 'eq', value: '', touched: true }) // 根据字段类型决定可用运算符列表 this.availableOperators = this.filterOperatorsByFieldType(fieldConfig) }filterOperatorsByFieldType推荐的做法是给字段配置加一个operators白名单数组。比如{ key: 'createTime', label: '创建时间', type: 'date', operators: ['gt', 'lt', 'between'] }。这样哪类字段支持哪些运算符,一目了然,运营要加字段也不用翻代码。真实业务中字段动辄几十个,如果每个字段的运算符全靠组件内部逻辑推断,维护成本会直线上升。
父组件index.vue则保存整个条件树数组,处理「添加条件」「删除条件」「切换分组逻辑」等操作。删除操作要注意索引问题:如果条件在循环里用index作为key,删除中间行时可能导致 Vue 复用了错误的 DOM 节点,最稳妥的做法是给每条条件生成一个唯一的id,删除时按id过滤,而不是按数组下标splice。
3.2 参数生成:把条件列表还原成查询对象
条件树的 UI 操作只是第一步,真正决定这个组件能不能被后端同学接纳的是parser.js的输出格式。大部分组合查询组件的翻车现场都在这里:界面上条件排布得很好看,但生成的结果不是嵌套太深就是字段名和值对不上。这个项目的解析器采用递归下降的思路,每一层都负责处理自己的children,最后拼出一个层次清晰的查询对象。
// utils/parser.js export function buildQueryParams(conditionTree) { if (!conditionTree || !conditionTree.children || conditionTree.children.length === 0) { return null } const conditionList = conditionTree.children .map(node => parseNode(node)) .filter(Boolean) if (conditionList.length === 0) { return null } return { logic: conditionTree.logic || 'and', conditions: conditionList } } function parseNode(node) { // 处理嵌套分组节点 if (node.children && node.children.length > 0) { return buildQueryParams(node) } // 跳过用户未配置完整的条件 if (!node.field || (node.operator !== 'in' && node.operator !== 'between' && !node.value)) { return null } // 简单条件节点 return { field: node.field, operator: node.operator, value: normalizeValue(node.operator, node.value) } } function normalizeValue(operator, value) { // between 和 in 是数组形态,其余是普通标量 if (operator === 'between') { return Array.isArray(value) ? value : [value, value] } if (operator === 'in') { return Array.isArray(value) ? value : value.split(',') } return value }这里buildQueryParams承担了两个职责:一是把条件树中配置了children的分组节点递归地再转成子查询对象,二是通过parseNode对叶子节点做数据清洗。空值过滤的逻辑放得很隐蔽但特别重要——用户在前端加了条件但没填值,parseNode会直接返回null,然后被filter(Boolean)吞掉。如果少了这一步,后端会收到大量残缺的查询条件,轻则报错,重则泄露全表数据(某些后端框架会把空字符串当成「不查这个字段」而返回不该返回的数据)。
normalizeValue函数处理的是运算符对值格式的要求差异。between和in这两个运算符天然需要多个值,对前端来说,between的 UI 应该是两个输入框,in则可能是多选框或逗号分隔的文本框。解析器在组装参数时统一归一化成数组,后端不管收到的是一个值还是两个值,都能正确处理。这种边界细节很多是从真实接口血泪里补出来的,第一次写的人大概率会在联调时被后端指出「你传的 value 格式不对」。
3.3 条件回显:拿接口数据反向填充表单
条件回显是另一个容易被低估的模块。编辑已保存的查询方案时,接口通常会把条件数据原样返回(比如{ logic: 'and', conditions: [{ field: 'status', operator: 'eq', value: 1 }] }),前端要做的是把这个平铺或者嵌套的对象反向渲染成之前的条件行。这个源码里的validator.js除了做提交校验,也承担了回显前的数据修正工作。
回显的核心难点在于两类字段的还原:一类是touched标记在回显时不需要了,所有从接口拿到的条件都是「真实条件」,必须标记为已交互;另一类是value的还原——接口返回的between值是数组[start, end],组件里要拆成两个输入框分别填充;in的数组值要塞进多选组件。一个常见的坑是日期类型的字段,接口返回"2024-01-01"字符串,但日期组件要求接收 Date 实例,这时候回显逻辑里需要补一步格式转换:
function normalizeIncomingCondition(rawCondition) { const { field, operator, value } = rawCondition let normalizedValue = value if (operator === 'between' && Array.isArray(value)) { // 区间查询回显到双输入框 normalizedValue = { start: value[0], end: value[1] } } if (operator === 'in' && Array.isArray(value) && fieldConfig.type === 'date') { // 日期列表转成 Date 格式 normalizedValue = value.map(item => new Date(item)) } return { ...rawCondition, value: normalizedValue, touched: true } }这块的逻辑虽然不复杂,但缺少的话,组件就只能「新建查询」而不能「编辑查询」,实用性直接减半。毕竟一个成熟的高级搜索组件,保存和回显几乎是标配能力。所谓完整闭环,不只是 UI 上能点、数据能查,还包括下次进来时状态能百分百复原,少了任何一个环节,项目落地都要返工。
4. 避坑指南:组合查询组件的三个高频问题
4.1 切换运算符后,值控件没有跟着变
现象:字段从「日期」切到「文本」后,输入框还是日期选择器,用户手动输入的文本根本存不进去。
原因:inputType是根据字段类型计算的,但切换字段后组件的availableOperators更新了,condition.operator却还是旧值,value控件的v-if判断依据没变。更隐蔽的情况是,组件复用了旧的 DOM 节点,Vue 的 diff 算法没有重建输入框。
解决:在onFieldChange中强制重置operator为eq并清空value,同时给value输入控件加上:key="condition.field + condition.operator",强制 Vue 在字段或运算符变化时销毁并重建输入控件。这个key绑定是很多 Vue 表单组件的通用解法,花小钱办大事。
4.2 嵌套分组时,父子组件的逻辑混乱
现象:一个分组里拖入另一个分组,外层选 AND,内层选 OR,生成查询参数时内层条件全部「穿透」到了外层,结果与 UI 上看到的条件关系完全不符。
原因:buildQueryParams递归时没有检查子节点的logic字段,导致所有条件被平铺到同一层级。这个问题的本质是递归函数的「递归出口」和「递归分支」没有严格区分。
解决:在parseNode里先判断children && children.length > 0,如果存在子节点就递归调用buildQueryParams,并确保该节点输出结构为{ logic: node.logic, conditions: childrenNested },否则直接当叶子处理。调试时先在控制台打印JSON.stringify(queryParams),两层嵌套一眼就能看出结构是否对。
4.3 提交给后端时,空值和全角符号兜底不完整
现象:用户在条件值里输入了空格或者全角括号,后端like查询返回 500,部分框架还会把 SQL 拼错。
原因:normalizeValue只做了数组归一化,没有清理空白字符,也没有处理运算符本身的字符转义。全角括号在 MySQL 里不属于危险字符,但某些查询引擎要求精确匹配,此时空字符串、纯空格会被当成非法输入。
解决:在normalizeValue增加String.prototype.trim()处理,条件校验再加一道:如果 value 在 trim 后为空且运算符是eq/ne/contains,直接过滤掉该条件。这类兜底看起来是小细节,但真实联调时能省下一整轮的沟通成本——毕竟后端不会告诉你是哪条记录的空格没处理。
5. 二次开发:把这个组件改造成你项目的查询协议
接手这个源码后,第一步不是改组件,而是先理清你的后端需要什么协议。比如 ERP 系统的查询接口只接受扁平参数?status=1&createTime_gt=2024-01-01,那么parser.js的输出就不能用嵌套结构,而应该把条件树拍平:
export function buildFlatParams(tree, prefix = '') { const params = {} tree.children.forEach(node => { if (node.children && node.children.length > 0) { Object.assign(params, buildFlatParams(node, prefix)) } else { // 扁平化 key:字段名_运算符 params[`${node.field}_${node.operator}`] = node.value } }) return params }一旦你掌握了这个组件的改造套路,会发现它的扩展点非常清晰。我实际项目里的做法是这样拆分需求的:
- 新增字段类型:只需要在字段配置表里加一条记录,比如
{ key: 'age', label: '年龄', type: 'number', operators: ['gt', 'lt', 'between'] },组件渲染会自动识别。 - 替换 UI 框架:把
condition-row.vue里的原生select替换成 Element 的el-select会节省大量样式维护成本。但务必确认v-model还是绑定在condition.field上,$emit事件名不要动,这样其他模块不受影响。 - 对接后端动态字段:如果字段本身是后台配置下发的,那
fieldOptions就不能写死在App.vue里,而应该换成接口请求后把数据注入组件 props,同时保留一份静态默认配置作为兜底。
我的建议是改动时先用App.vue做 playground,跑通一条完整的「添加条件 → 填写值 → 生成参数 → 回显」链路,再去接真实后端。改完以后记得回归测试两个边界场景:第一,只有一层条件时生成结果是否正确;第二,条件全是空值时,提交按钮是否被禁用或生成null。有一次我就是只顾着加新运算符,忘了校验in和between的数组格式,结果 main 分支炸了一下午。从那以后我每次改完parser.js,都强制自己先在浏览器控制台跑一遍JSON.stringify(buildQueryParams(tree)),确认输出协议没有问题再去碰 UI 层。
后续你再接入权限系统或者多租户隔离时,组件的条件树本身就是很好的结构逻辑,往里面塞一个userId字段或者租户 ID 就能实现数据隔离。灵活性的天花板比想象中高。希望帮到你。
本文还有配套的精品资源,点击获取