自定义一个玩出花来的查询构建器React Query Builder

867 阅读2分钟

首先我们来看看官方给出demo:一键直达 在这里插入图片描述 通过自定义Antd的Cascader组件,最后的效果如下: 在这里插入图片描述

一、还原demo

首先,我们需要把整个demo的架子还原,然后再此基础上才好做自定义拓展。

1)引入所需库

import QueryBuilder from 'react-querybuilder';
import {
  AntDActionElement,
  AntDDragHandle,
  AntDNotToggle,
  AntDValueEditor,
  AntDValueSelector,
} from '@react-querybuilder/antd';
import './index.css';

2)自定义控件对象:controlElements

const controlElements = {
  addGroupAction: AntDActionElement,
  addRuleAction: AntDActionElement,
  cloneGroupAction: AntDActionElement,
  cloneRuleAction: AntDActionElement,
  combinatorSelector: AntDValueSelector,
  fieldSelector: AntDValueSelector,
  notToggle: AntDNotToggle,
  operatorSelector: AntDValueSelector,
  removeGroupAction: AntDActionElement,
  removeRuleAction: AntDActionElement,
  valueEditor: AntDValueEditor,
  dragHandle: AntDDragHandle,
};

3)定义运算符数组:operators

const operators = [
  { name: '=', label: '=' },
  { name: '!=', label: '!=' },
  { name: '<', label: '<' },
  { name: '>', label: '>' },
  { name: '<=', label: '<=' },
  { name: '>=', label: '>=' },
  { name: 'contains', label: 'contains' },
  { name: 'beginsWith', label: 'begins with' },
  { name: 'endsWith', label: 'ends with' },
  { name: 'doesNotContain', label: 'does not contain' },
  { name: 'doesNotBeginWith', label: 'does not begin with' },
  { name: 'doesNotEndWith', label: 'does not end with' },
  { name: 'null', label: 'is null' },
  { name: 'notNull', label: 'is not null' },
  { name: 'in', label: 'in' },
  { name: 'notIn', label: 'not in' },
  { name: 'between', label: 'between' },
  { name: 'notBetween', label: 'not between' },
];

中文版的可以这么写:

const operators = [
  { label: '等于', name: '=' },
  { label: '不等于', name: '!=' },
  { label: '小于', name: '<' },
  { label: '大于', name: '>' },
  { label: '小于等于', name: '<=' },
  { label: '大于等于', name: '>=' },
  { label: '包含', name: 'contains' },
  { label: '不包含', name: 'does not contain' },
  { label: '为空', name: 'is null' },
  { label: '不为空', name: 'is not null' },
  { label: '在范围中', name: 'between' },
  { label: '不在范围中', name: 'not between' },
];

4)定义用于 RuleGroups 的组合子数组:combinators

const combinators = [
  { name: 'and', label: '且' },
  { name: 'or', label: '或' },
];

5)定义可翻译的文本: translations

{
  fields: {
    title: "Fields",
  },
  operators: {
    title: "Operators",
  },
  value: {
    title: "Value",
  },
  removeRule: {
    label: "x",
    title: "Remove rule",
  },
  removeGroup: {
    label: "x",
    title: "Remove group",
  },
  addRule: {
    label: "+Rule",
    title: "Add rule",
  },
  addGroup: {
    label: "+Group",
    title: "Add group",
  },
  combinators: {
    title: "Combinators",
  },
  notToggle: {
    label: "Not",
    title: "Invert this group",
  },
  cloneRule: {
    label: '⧉',
    title: 'Clone rule'
  },
  cloneRuleGroup: {
    label: '⧉',
    title: 'Clone group'
  },
  dragHandle: {
    label: '⁞⁞',
    title: 'Drag handle'
  }
}

6)定义特定的 CSS 类分配给组件呈现的各种控件:controlClassnames

const controlClassnames = {
  removeGroup: 'bg_grey',
  removeRule: 'bg_grey',
  combinators: 'bg_grey',
};

对应的css样式:

.bg_grey {
  background-color: #ff7e4e;
  border-color: #ff7e4e;
}
.bg_grey:hover {
  background-color: #ff9b75;
  border-color: #ff9b75;
}

7)最后

export default (props: any) => {
  const { query, setQuery, variableFields, disabled } = props;
  return (
    <QueryBuilder
      disabled={disabled}
      query={query}
      fields={variableFields}
      operators={operators}
      combinators={combinators}
      translations={translations}
      controlElements={controlElements}
      controlClassnames={controlClassnames}
      onQueryChange={(q) => {
        setQuery(q)
      }}
    />
  );
};

到这里,我们就配置好了基础版的demo。

二、把fields的select下拉改造成Cascader级联选择

1)关闭fields的默认来源

修改controlElements配置:

// fieldSelector: AntDValueSelector,

修改QueryBuilder配置:

 // fields={variableFields}

2)引入自定义组件CustomValueEditor

修改controlElements配置:

fieldSelector: CustomValueEditor,

修改QueryBuilder配置:不需要额外定义fields

controlElements={controlElements}

3)自定义组件CustomValueEditor

import React, { useEffect, useState } from 'react';
import { ValueEditor } from 'react-querybuilder';

import { Cascader, message } from 'antd';
import client from '../../../utils/request';

const CustomValueEditor: React.FC = (props: any) => {
  const [options, setOptions] = useState([]);
  const [cascaderDisabled, setCascaderDisabled] = useState(Boolean);

  const list = [] as any;
  const DFS = (op: any, field: any) => {
    for (let i = 0; i < op.length; i++) {
      const localName = op[i].name;
      const localChildren = op[i].children;
      list.push(localName);

      if (localName === field) {
        return true;
      }
      if (localChildren) {
        const res = DFS(localChildren, field);
        if (res) {
          return true;
        } else {
          list.pop(localName);
        }
      }
    }
  };
  
  
  function displayRender(label: any) {
    return label[label.length - 1];
  }

  useEffect(() => {
    (async (params = {}) => {
      let data = await client<any>(`接口请求地址`, {
        params,
      });
      setOptions(data.data);
    })();
  }, []);

  if (props.value != '~' && options.length>0) {
    DFS(options, props.value);
  }

  if (props && options.length>0) {
    return (
      <div>
        <Cascader
          expandTrigger="hover"
          options={options}
          displayRender={displayRender}
          onChange={(value: any) => {
            const data = value[value.length - 1]
            props.handleOnChange(data);
          }}
          defaultValue={list}
          value={list}
        />
      </div>
    );
  }
  DFS(options, props.value);
  return <ValueEditor {...props} />;
};
export default CustomValueEditor;

options的JSON格式如下:

{
    value: 'zhejiang',
    label: 'Zhejiang',
    children: [
      {
        value: 'hangzhou',
        label: 'Hangzhou',
        children: [
          {
            value: 'xihu',
            label: 'West Lake',
          },
        ],
      },
    ],
  },
  {
    value: 'jiangsu',
    label: 'Jiangsu',
    children: [
      {
        value: 'nanjing',
        label: 'Nanjing',
        children: [
          {
            value: 'zhonghuamen',
            label: 'Zhong Hua Men',
          },
        ],
      },
    ],
  },

⚠️props.handleOnChange()会将数据更新给QueryBuilder

onChange={(value: any) => {
            const data = value[value.length - 1]
            props.handleOnChange(data);
          }}

到这里整个改造完成啦!