Vue3 项目中集成 jQuery QueryBuilder

背景与需求分析
  • 介绍 jQuery QueryBuilder 的功能和适用场景(动态查询条件生成、复杂筛选等)。
    因为项目需求,是需要在vue3页面中创建一个类似 jQuery QueryBuilder的组件。首先是在网上一顿摸索,发现这个组件是可以满足要求的,如下图。然后就开始借助AI的力量帮我去办,结果踩了好多坑。接下来一一道来。
    在这里插入图片描述
环境准备与基础配置
坑一:安装依赖的名字不一样,不能按照ai给的来弄!!一定要去看官网!大小写注意区分,因为你会发现都能下载,但是不正确。
  • 创建或确认现有 Vue3 项目环境(Vue CLI 或 Vite)。

  • 安装 jQuery 和 jQuery QueryBuilder 依赖:

  • 在这里插入图片描述
    在这里插入图片描述

    npm install jquery 
    npm install jQuery-QueryBuilder
    
  • 配置 Vue3 的 main.jsmain.ts 全局引入 jQuery: 还要引入样式bootstrap,并手动挂载(不知道为什么我没有直接成功,必须手动一下)

    import { createApp } from 'vue';
    import App from './App.vue';
    import $ from 'jquery';
    window.jQuery = $
    window.$ = $
    // 验证挂载结果(确保所有模块共用这个实例)
    console.log('main.js 全局 jQuery 实例:', window.$)
    console.log('jQuery 版本:', $.fn,window.$.fn.jquery)
    
    // 2. 再引入 Bootstrap CSS + JS(必须在 jQuery 之后)
    import 'bootstrap/dist/css/bootstrap.min.css'
    import 'bootstrap/dist/js/bootstrap.min.js'
      // 验证 Bootstrap 是否加载成功
      console.log('Bootstrap 是否加载:', typeof window.bootstrap !== 'undefined')  
      if (!window.bootstrap) {
       // 手动挂载 bootstrap 全局变量(适配魔改版插件)
    	window.bootstrap = $.fn.bootstrap || {}
    	console.log('手动挂载 bootstrap 全局变量完成')
      }
    
    坑二:这里也不算是坑吧,就是其实在搜索中会有很多别人已经弄好的npm包,可供vue直接使用该组件,什么vue-query-builder,query-builder-vue等等,也都能下载,但是我看了一眼,就是将近六七年前的,而且大多都是vue2的框架,其实没法使用的(反正我没用成功),然后还有一些收费的说集成好的vue3 querbuilder组件,大家可以顺便自行选择,我用的还是jquery的方法。
组件封装与集成
  • 创建 Vue3 组件封装 JQueryQueryBuilder(如 JQueryQueryBuilder.vue),也是和其对应的父组件有传参,这个父组件上面还有个父组件,嵌套了两次。
  • 模板中定义容器元素并初始化 QueryBuilder:
      <template>
        <!-- QueryBuilder 容器 -->
        <div ref="builderRef" class="query-builder-container"></div>
      </template>
    
  • 然后script中,这里我采用的异步加载包,好像也是报错解决过程中AI推荐的,能用就没改了,整个做完后感觉直接导入应该也没问题。
<template>
  <!-- QueryBuilder 容器 -->
  <div ref="builderRef" class="query-builder-container"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, defineProps, defineEmits, toRaw } from 'vue'
import 'jquery-extendext'

const $ = window.$
const jQuery = window.jQuery

// ========== 第二步:动态导入 QueryBuilder(确保 jQuery 挂载后再加载) ==========
let queryBuilderLoaded = false
// 动态导入 + 延迟执行,避免加载顺序问题
const loadQueryBuilder = async () => {
  if (queryBuilderLoaded) return
    window.$ = window.jQuery = $;
    console.log('插件导入前全局 jQuery:',$.fn, typeof $.extend) // 必须是 function
  try {
    // 1. 先导入样式(官方包路径,替换你之前的非官方分支)
    await import('jQuery-QueryBuilder/dist/css/query-builder.default.css')
    // 2. 再导入 standalone 版 JS(内置 jquery-extendext,无需单独引入)
    await import('jQuery-QueryBuilder/dist/js/query-builder.js')
    queryBuilderLoaded = true
    console.log('QueryBuilder 加载成功')
  } catch (e) {
    console.error('QueryBuilder 加载失败:', e)
  }
}

// ========== 组件属性/事件定义 ==========
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({
      condition: '',
      rules: []
    })
  },
  fields: {
    type: Array,
    required: true
  },
  operators: {
    type: Array,
    default: () => []
  },
  showGroupBtn: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['update:modelValue', 'change', 'rules-change'])

// ========== 实例管理 ==========
const builderRef = ref(null)
let builderInstance = null
// 标记实例是否已初始化完成
const isInstanceReady = ref(false)

// ========== 初始化逻辑(先加载插件,再初始化) ==========
onMounted(async () => {
  if (!builderRef.value) return

  // 步骤1:先加载 QueryBuilder 插件
  await loadQueryBuilder()

  // 步骤2:验证插件是否加载成功
  if (typeof $.fn.queryBuilder === 'undefined') {
    console.error('QueryBuilder 插件未正确加载!')
    return
  }

  // 步骤3:初始化插件
  try {
    // 0. 优先加载语言包(确保在渲染前语言包已生效)
    try {
        // 加载语言包内容(注意路径正确)
        const langModule = await import('jQuery-QueryBuilder/dist/i18n/query-builder.zh-CN.js?raw');
        // 手动执行语言包代码,指定上下文为 window
        const langCode = langModule.default || langModule;
        new Function('$', langCode)(window.$);
        console.log('语言包手动执行成功');
    } catch (e) {
        console.warn('语言包加载失败,使用手动注入的中文配置',e);
    }

    // 1. 初始化插件(不预设 rules 和 condition,防止初始化状态不一致)
    builderInstance = $(builderRef.value).queryBuilder({
      plugins: ['bt-tooltip-errors'],
      lang_code: 'zh-CN',
      filters: props.fields || [{ id: 'default', label: '默认字段', type: 'string' }],
      allow_groups: 1, // 允许根级别有组
      allow_empty: true,
      display_errors: false,
      display_as: 'root',
      // 工具栏配置
      toolbar: {
        display: ['add-group', 'add-rule', 'clear', 'toggle-group']
      },
      buttons: {
        add_group: props.showGroupBtn,
        add_rule: true,
        remove_rule: true,
        remove_group: true
      }
    }).data('queryBuilder')

    // 2. 显式设置初始规则(核心修复:确保根条件被正确应用)
    if (props.modelValue) {
      builderInstance.setRules(props.modelValue)
    }

    // 标记实例已就绪
    isInstanceReady.value = true
    console.log('QueryBuilder 实例初始化成功')

    // 监听规则变化
    $(builderRef.value).on('rulesChanged.queryBuilder', (e, rule) => {
      nextTick(() => {
        // 双重校验实例是否存在
        if (!builderInstance) return
        const newRules = builderInstance.getRules({ get_value: true })
        emit('update:modelValue', newRules)
        emit('change', newRules)
        emit('rules-change', newRules)
      })
    })

  } catch (e) {
    console.error('QueryBuilder 初始化失败:', e)
    isInstanceReady.value = false
  }
})

watch(
  () => props.modelValue,
  async (newVal) => {
    // 修复1:先校验实例是否存在且就绪
    if (!isInstanceReady.value || !builderInstance || !newVal) return

    try {
      const rawNewVal = toRaw(newVal)
      // 修复2:调用 getRules 前再次校验实例
      if (!builderInstance.getRules) return
      
      const currentRules = builderInstance.getRules({ get_value: true })

      // 优化比较逻辑
      const stringify = (obj) => JSON.stringify(obj, (k, v) => v === undefined ? null : v)
      if (stringify(rawNewVal) === stringify(currentRules)) return

      await nextTick()
      builderInstance.reset()
      builderInstance.setRules(rawNewVal)
      console.log('规则同步到 QueryBuilder 成功')
    } catch (e) {
      console.error('更新规则失败:', e)
      // 容错:避免崩溃
      if (builderInstance) {
        builderInstance.reset()
      }
    }
  },
  { deep: true, immediate: true }
)

// 销毁实例
onUnmounted(() => {
  // 销毁前校验实例
  if (builderInstance) {
    try {
      builderInstance.destroy()
      $(builderRef.value).off('rulesChanged.queryBuilder')
    } catch (e) {
      console.error('销毁实例失败:', e)
    } finally {
      builderInstance = null
      queryBuilderLoaded = false
      isInstanceReady.value = false
    }
  }
})

// 暴露方法(增加实例校验)
defineExpose({
  getRules: () => {
    if (isInstanceReady.value && builderInstance) {
      return builderInstance.getRules({ get_value: true }) || {}
    }
    return { condition: 'AND', rules: [] }
  },
  reset: () => {
    if (isInstanceReady.value && builderInstance) {
      builderInstance.reset()
    }
  },
  setRules: (rules) => {
    if (isInstanceReady.value && builderInstance && rules) {
      builderInstance.setRules(rules)
    }
  }
})
</script>

<style scoped>
.query-builder-container {
  --qb-padding: 12px;
  --qb-border: 1px solid #e5e7eb;
  --qb-bg: #f9fafb;

  background-color: var(--qb-bg);
  border: var(--qb-border);
  border-radius: 6px;
  padding: var(--qb-padding);
  margin-top: 8px;
}

.query-builder .rule-container,
.query-builder .group-container {
  margin-bottom: 8px;
  padding: 8px;
  border-radius: 4px;
  background-color: #fff;
  border: 1px solid #e5e7eb;
}

.query-builder .btn {
  padding: 4px 8px;
  font-size: 14px;
}
</style>

这里面也存在一些问题,算是小坑:

坑三:这里算是官网上的一些问题,图一是我下的有关jquery的包,图二是官网相关用法的示例。看着官网好像standalone更全面些的意思,所以我刚开始只导入query-builder.standalone.js,会一直出现找不到$.extend相关的报错,ai的解答也相对清晰“核心矛盾是:控制台能打印出 .extend是正常函数,但加载jQuery−QueryBuilder的standalone.js时仍报Cannotreadpropertiesofundefined(reading′extend′)——这是因为插件内部执行时访问的jQuery/.extend 是正常函数,但加载 jQuery-QueryBuilder 的 standalone.js 时仍报 Cannot read properties of undefined (reading 'extend')—— 这是因为 插件内部执行时访问的 jQuery/.extend是正常函数,但加载jQueryQueryBuilderstandalone.js时仍报Cannotreadpropertiesofundefined(readingextend)——这是因为插件内部执行时访问的jQuery/ 和你当前文件的 $ 不是同一个对象(Vite 模块化隔离导致的 “多实例” 问题)。”反正后续一直也没有解决,突然想着换成query-builder.js,结果迎刃而解了,就成功了(混混沌沌ing)。

在这里插入图片描述
在这里插入图片描述

父组件使用
  • 通过 Vue3 的 refreactive 管理 QueryBuilder 的规则数据。
  • 监听 QueryBuilder 事件(如 rulesChanged)并同步到 Vue 状态。将 规则配置上面的数据双向绑定到规则脚本。 上面还有一层父组件调用,在此就不展示了。目前是可以正常展示组件,然后选择可以双向和规则脚本对应,至于其中还有一些按钮还没有过多添加逻辑。
      <template>
        <Teleport to="body">
          <div v-if="visible" class="modal-backdrop" @click.self="handleClose">
            <div class="modal-box">
              <!-- 弹窗头部 -->
              <div class="modal-header">
                <h3 class="modal-title">规则编辑器</h3>
                <button class="modal-close" @click="handleClose">×</button>
              </div>
      
              <!-- 弹窗主体 -->
              <div class="modal-body">
                <!-- 右侧 QueryBuilder 区 -->
                <div class="builder-column">
                  <!-- 标签页 -->
                  <div class="builder-tabs">
                    <button
                      class="tab-btn"
                      :class="{ active: activeTab === 'config' }"
                      @click="handleTabChange('config')"
                    >
                      规则配置
                    </button>
                    <button
                      class="tab-btn"
                      :class="{ active: activeTab === 'script' }"
                      @click="handleTabChange('script')"
                    >
                      规则脚本
                    </button>
                  </div>
      
                  <!-- 规则配置面板 -->
                  <div v-if="activeTab === 'config'">
                    <JQueryQueryBuilder
                      ref="queryBuilderRef"
                      :modelValue="form.queryRules"
                      :fields="queryFields"
                      :operators="queryOperators"
                      @rules-change="handleRulesChange"
                    />
                  </div>
      
                  <!-- 规则脚本面板(优化同步) -->
                  <div v-else class="script-panel">
                    <!-- 错误提示 -->
                    <div v-if="scriptError" class="script-error">{{ scriptError }}</div>
                    <textarea
                      v-model="ruleScript"
                      placeholder="规则脚本(JSON 格式,修改后会同步到配置面板)"
                      class="form-control"
                      rows="20"
                      @input="handleScriptChange"
                      @blur="validateScript"
                    ></textarea>
                    <button class="format-btn" @click="formatScript">格式化JSON</button>
                  </div>
                </div>
              </div>
      
              <!-- 弹窗底部 -->
              <div class="modal-footer">
                <button class="btn-reset" @click="handleReset">重置</button>
                <button class="btn-submit" @click="handleSubmit">创建规则</button>
                <button class="btn-browse" @click="handleBrowse">数据浏览</button>
              </div>
            </div>
          </div>
        </Teleport>
      </template>
      
      <script setup>
      import { ref, reactive, watch, defineProps, defineEmits, nextTick } from 'vue'
      import JQueryQueryBuilder from './JQueryQueryBuilder.vue'
      
      // 组件属性
      const props = defineProps({
        visible: {
          type: Boolean,
          default: false
        },
        initialData: {
          type: Object,
          default: () => ({})
        }
      })
      
      // 组件事件
      const emit = defineEmits(['update:visible', 'submit'])
      
      // 表单数据
      const form = reactive({
        queryRules: {
          "condition": "AND",
          "rules": [
            {
              "condition": "AND",
              "rules": [
                {
                  "id": "ITEM_CODES",
                  "field": "ITEM_CODES",
                  "type": "string",
                  "input": "text",
                  "operator": "in",
                  "value": "项目代码1,项目代码2"
                },
                {
                  "id": "PAY_PER_RETIO",
                  "field": "PAY_PER_RETIO",
                  "type": "double",
                  "input": "number",
                  "operator": "not_equal",
                  "value": 1
                }
              ]
            },
            {
              "id": "ITEM_CODES",
              "field": "ITEM_CODES",
              "type": "string",
              "input": "text",
              "operator": "in",
              "value": "项目代码3,项目代码4"
            },
            {
              "id": "JOINTYPE",
              "field": "JOINTYPE",
              "type": "string",
              "input": "radio",
              "operator": "equal",
              "value": "%Y%m%d"
            },
            {
              "id": "MONEY_INDEX",
              "field": "MONEY_INDEX",
              "type": "integer",
              "input": "radio",
              "operator": "equal",
              "value": 0
            },
            {
              "id": "JUDGE_INDEX",
              "field": "JUDGE_INDEX",
              "type": "integer",
              "input": "radio",
              "operator": "equal",
              "value": 2
            }
          ]
        }
      })
      
      // 标签页状态
      const activeTab = ref('config')
      // 规则脚本
      const ruleScript = ref('')
      // 脚本错误提示
      const scriptError = ref('')
      // QueryBuilder 引用
      const queryBuilderRef = ref(null)
      
      // 字段配置
      const queryFields = ref([
        { id: 'ITEM_CODES', label: '医保目录编码', type: 'string' },
        { id: 'PAY_PER_RETIO', label: '报销比例', type: 'double' },
        { id: 'JOINTYPE', label: '聚合依据', type: 'string' },
        { id: 'MONEY_INDEX', label: '违规资金序号', type: 'integer' },
        { id: 'JUDGE_INDEX', label: '筛选条件', type: 'integer' }
      ])
      
      // 操作符配置
      const queryOperators = ref([
        { 
          id: 'in',          
          label: '在...之内', 
          type: 'string',     
          nb_inputs: 1,       
          apply_to: ['string']
        },
        { 
          id: 'not_in', 
          label: '不在...之内', 
          type: 'string',     
          nb_inputs: 1, 
          apply_to: ['string']
        },
        { 
          id: 'equal', 
          label: '等于', 
          type: 'integer',    
          nb_inputs: 1, 
          apply_to: ['integer', 'float', 'double']
        },
        { 
          id: 'not_equal', 
          label: '不等于', 
          type: 'integer',    
          nb_inputs: 1, 
          apply_to: ['integer', 'float', 'double']
        }
      ])
      
      // 规则配置 → 脚本同步
      const handleRulesChange = (newRules) => {
        try {
          // 深拷贝避免响应式问题
          form.queryRules = JSON.parse(JSON.stringify(newRules))
          // 同步到脚本面板(格式化)
          ruleScript.value = JSON.stringify(newRules, null, 2)
          // 清除错误提示
          scriptError.value = ''
        } catch (e) {
          console.error('规则同步到脚本失败:', e)
        }
      }
      
      // 脚本 → 规则配置同步
      const handleScriptChange = () => {
        // 实时验证但不强制同步(避免输入过程中频繁报错)
        validateScript(false)
      }
      
      // 验证脚本并同步
      const validateScript = (forceSync = true) => {
        try {
          // 空值处理
          if (!ruleScript.value.trim()) {
            scriptError.value = ''
            if (forceSync) {
              form.queryRules = { condition: 'AND', rules: [] }
            }
            return
          }
      
          // 解析 JSON
          const parsedRules = JSON.parse(ruleScript.value)
          
          // 验证基本结构
          if (!parsedRules.hasOwnProperty('condition') || !Array.isArray(parsedRules.rules)) {
            throw new Error('JSON 结构错误:必须包含 condition 和 rules 字段')
          }
      
          // 清除错误提示
          scriptError.value = ''
          
          // 强制同步时更新规则
          if (forceSync) {
            form.queryRules = parsedRules
            // 切换到配置面板时强制刷新
            if (activeTab.value === 'config') {
              nextTick(() => {
                queryBuilderRef.value?.setRules(parsedRules)
              })
            }
          }
        } catch (e) {
          scriptError.value = `JSON 格式错误:${e.message}`
          console.warn('脚本验证失败:', e)
        }
      }
      
      // 格式化 JSON 脚本
      const formatScript = () => {
        try {
          const parsed = JSON.parse(ruleScript.value)
          ruleScript.value = JSON.stringify(parsed, null, 2)
          scriptError.value = ''
        } catch (e) {
          alert('JSON 格式错误,无法格式化!')
        }
      }
      
      // 标签页切换
      const handleTabChange = (tab) => {
        activeTab.value = tab
        // 切换到脚本面板时同步最新规则
        if (tab === 'script') {
          ruleScript.value = JSON.stringify(form.queryRules, null, 2)
          scriptError.value = ''
        }
        // 切换到配置面板时验证脚本并同步
        else {
          validateScript(true)
        }
      }
      
      // 监听弹窗显示初始化
      watch(
        () => props.visible,
        (val) => {
          if (val && props.initialData) {
            Object.assign(form, props.initialData)
            // 初始化规则脚本
            ruleScript.value = JSON.stringify(form.queryRules, null, 2)
          }
        },
        { immediate: true }
      )
      
      // 原有弹窗操作逻辑
      const handleClose = () => {
        emit('update:visible', false)
      }
      
      const handleReset = () => {
        const defaultRules = { condition: 'AND', rules: [] }
        Object.assign(form, {
          ruleCode: '',
          regionCode: '',
          itemName: '',
          itemDesc: '',
          policy: '',
          remark: '',
          queryRules: defaultRules
        })
        ruleScript.value = JSON.stringify(defaultRules, null, 2)
        scriptError.value = ''
      }
      
      const handleSubmit = () => {
        // 提交前验证脚本
        validateScript(true)
        if (scriptError.value) {
          alert('规则脚本格式错误,请修正后提交!')
          return
        }
        
        const submitData = JSON.parse(JSON.stringify(form))
        emit('submit', submitData)
        handleClose()
      }
      
      const handleBrowse = () => {
        console.log('当前规则数据:', form.queryRules)
      }
      </script>
    
    
总结:就是其中会遇见大大小小的问题,比如用querybuilder,但是我发现我顶层 condition 修改无效果,但嵌套组的 condition 修改正常,后来发现是传参过程中忽略等问题。还有中文包导入的时候,直接导入也出现了报错,后来发现大部分原因或许是因为实例组件还未加载好就进行下步操作(很多我的报错都有类似未加载好的情况)

心得:
1.稍稍理解了npm下完包要去看包里面对应的导入,以前对这些根本不关心,觉得下了包就好了。
2.一定要看官网!看官网!一味的让AI给你导包很有报错可能,他真的会给你瞎扯,但是那些包还真的是可以npm下载起来。
3.对于vue入口文件,全局注入,局部等概念稍微有点感觉,还有vite.config.js的配置,每一项是什么意思,后面还是需要再去学习看看。
4.不能老往上学,想着借AI去完成所有的东西,也得往下学,知道底下到底怎么运行的。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐