Net9 Abp Vnext查询、高级搜索、过滤终极解决方案,ORM支持Freesql/SqlSugar/EFCore或原生sql

以员工管理表为例,常用栏位如下图

基本需求:默认搜索框可以模糊查询搜索工号、姓名、手机号、年龄等不需要关联查询基本字段。

特殊需求需要高级搜索:例如按入职区间、部门、公司、年龄段、上级主管等进行模糊搜索,且支持并且或者等关系(and/or),实体表需要关联查询

当用户有其它需求,系统表无法满足时,用户需要自定义字段,Abp使用的是扩展字段(json存储),允许使用扩展字段精准搜索

市场主流的数据库都要支持,例如mysql/sqlserver/oracel/pgsql/sqlite

如下图用户添加了自定义字段Input

实体表

自定义字段精准搜索原理,调用原生sql的json数据查询,目前市场主流和的几大数据库支持(mysql/sqlserver/oracel/pgsql/sqlite)

语法参考下图

多个搜索值用空格分开进行搜索

经常用的搜索条件通过下图保存

特殊情况用右侧的高级搜索

搜索条件非常多时,可以从excel中复制,如下图

高级搜索配置

配置后的搜索界面

高级搜索实体表

新建表TSYS_Filters与TSYS_FiltersDetail记录搜索条件

自定义字段json数据查询

 前端调用通过字段类型filedType=2判断是否有自定义字段搜索

完整的前端代码

前端组件定义

AdvancedSearch.vue代码

<template>
  <t-select-input :value="selectValue" :popup-visible="popupVisible"
    :popup-props="{ overlayInnerStyle: { padding: '6px' } }" :placeholder="$t('pages.common.advancedSearchPlaceholder')"
    allow-input style="width: 220px" autocomplete="" @input-change="onInputChange"
    @popup-visible-change="onPopupVisibleChange" @enter="onEnter">
    <template #panel>
      <ul>
        <li class="t-select-option" v-for="item in options" :key="item" @click="() => onOptionClick(item)">
          {{ item.label }}
        </li>
      </ul>
    </template>
    <template #prefix-icon>
      </SearchIcon> -->
      <t-button shape="square" variant="text" style="margin: 0 -10px" :loading="loading" @click="searchClick">
        <template #icon><search-icon /></template>
      </t-button>
    </template>
    <template #suffixIcon>
      <chevron-down-icon v-if="!popupVisible" @click="iconClick" />
      <chevron-up-icon style="color: #0052d9" v-if="popupVisible" />
      <t-button variant="text" shape="circle" style="margin: 0 -10px 0 0" :title="$t('pages.common.advancedSearch')"
        theme="default" @click="() => onAdvancedQueryClick()">
        <template #icon>
          <ellipsis-icon />
        </template>
      </t-button>
    </template>
  </t-select-input>
  <search-dialog ref="advancedSearchRef" :filters="filters" :fileds="fileds" :enumData="enumData"
    :filterCode="$props.filterCode" :options="options" @dynamicFilter="dynamicFilter" @refreshOptions="refreshOptions"
    @loadingStart="loadingStart" />
</template>
<script setup lang="ts">
import { t } from '@/locales';
import SearchDialog from './SearchDialog.vue';
import { ref, onMounted, watch } from 'vue';
import { SelectInputProps, SelectInputValue, SelectInputFocusContext } from 'tdesign-vue-next';
import { ChevronDownIcon, ChevronUpIcon, SearchIcon, EllipsisIcon } from 'tdesign-icons-vue-next';
import { globalPageSize, globalDateFormat, globalDateTimeFormat, globalTimeFormat, } from '@/config/global';
import { getEnumCode } from '@/api/system/enum';
import { filterFilters, getFilters, filterItemFilters, getFiltersCode } from '@/api/system/filters';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';//参考文档https://day.js.org/docs/zh-CN/durations/durations
import { cloneDeep, forIn } from 'lodash';
const props = defineProps({
  visible: {
    type: Boolean,
    default: false,
  },
  filters: {
    type: Object,
    default: () => { },
  },
  filterCode: {
    type: String,
  },
  fileds: {
    type: Array,
    default: () => [],
  },
});
const loading = ref(false);
const filters = ref([]);
const fileds = ref([]);
const enumData = ref({});
const inputFileds = ref([]);
const advancedSearchRef = ref();
const emits = defineEmits(['dynamicFilter']);
const dynamicFilter = (filtersValue) => {
  let filterInfo = cloneDeep(filtersValue);
  filterInfo.Filters = filterInfo.Filters.map((item) => {
    if (item.filedType === 2) {
      item.Value = JSON.stringify({ Field: item.Field, Operator: item.Operator, Value: item.Value });
      item.Operator = 'Custom';
      item.Field = 'QueryJson 命名空间.DynamicFilterCustomImpl,程序集名称';
    }
    return { Field: item.Field, Operator: item.Operator, Value: item.Value };
  });
  let filtersPage = {
    FilterInfo: filterInfo,
    PageSize: globalPageSize,
  };
  emits('dynamicFilter', filtersPage);
};
// 搜索处理逻辑
const popupVisible = ref(false);
const selectValue = ref();
const options = ref([]);//搜索方案
const onOptionClick = (item) => {
  popupVisible.value = false;
  let filterInfo = JSON.parse(item.value);
  filterInfo.Filters = filterInfo.Filters.map((item) => {
    if (item.filedType === 2) {
      item.Value = JSON.stringify({ Field: item.Field, Operator: item.Operator, Value: item.Value });
      item.Operator = 'Custom';
      item.Field = 'QueryJson 命名空间.DynamicFilterCustomImpl,程序集名称';
    }
    return { Field: item.Field, Operator: item.Operator, Value: item.Value };
  });
  let filtersPage = {
    FilterInfo: filterInfo,
    PageSize: globalPageSize,
  };
  emits('dynamicFilter', filtersPage);
};
const iconClick = () => {
  popupVisible.value = !popupVisible.value;
};
const searchClick = () => {
  onEnter(selectValue.value);
};
const onInputChange = (keyword) => {
  popupVisible.value = false;
  selectValue.value = keyword;
};
const onEnter = (value) => {
  loading.value = true;
  let operator = 'Contains';
  if (value) {
    value = value.replaceAll(' ', ',').replaceAll(' ', ',').replaceAll(',', ',').replaceAll(';', ',').replaceAll(';', ',');
  }
  if (value.indexOf(',') > 0) {
    operator = 'Any';
  }
  let filtersValue = inputFileds.value.map(item => {
    if (item.includes('ExtraProperties.')) {
      return { Field: 'QueryJson 命名空间.DynamicFilterCustomImpl,程序集名称', Operator: 'Custom', Value: JSON.stringify({ Field: item.substr(16), Operator: operator, Value: value }) };
    }
    return { Field: item, Operator: operator, Value: value };
  });
  console.log('filtersValue', filtersValue);
  let filtersPage = {
    FilterInfo: {
      Logic: 'Or',
      Filters: filtersValue,
    },
    PageSize: globalPageSize,
  };
  emits('dynamicFilter', filtersPage);
};
const onAdvancedQueryClick = () => {
  popupVisible.value = false;
  advancedSearchRef.value.show();
};
const onPopupVisibleChange = (val) => {
  if (!val && popupVisible.value) {
    popupVisible.value = val;
  }
};

const refreshOptions = (data) => {
  options.value = data;
};
const loadingStart = () => {
  loading.value = true;
};
const loadingStop = () => {
  loading.value = false;
};
onMounted(() => {
  if (props.filterCode != undefined) {
    getFiltersCode({ codes: [props.filterCode] }).then((res) => {
      fileds.value = res[props.filterCode].Details;
      fileds.value.forEach((element) => {
        element.DisplayName = t(element.DisplayName);
        if (element.Operator == 'Eq') element.Operator = 'Equal';
        if (element.Operator == 'Equals') element.Operator = 'Equal';
        if (element.DefaultValue == 'date') element.DefaultValue = dayjs().format(globalDateFormat);
        if (element.DefaultValue == 'datetime') element.DefaultValue = dayjs().format(globalDateTimeFormat);
        if (element.DefaultValue == 'time') element.DefaultValue = dayjs().format(globalTimeFormat);
      });
      filters.value = { Logic: 'Or', Filters: fileds.value.map((item) => { return { Field: item.FiledName, Operator: item.Operator, Value: item.DefaultValue, filedType: item.FilterFiledType, dataType: item.DataType, valueType: item.ValueType, EnumCode: item.EnumCode } }) };
      if (res[props.filterCode].DefaultFileds != null) {
        inputFileds.value = res[props.filterCode].DefaultFileds.split(',');
      }
      let codes = fileds.value.filter((item) => { return item.EnumCode != null && item.EnumCode != '' }).map(item => item.EnumCode);
      getEnumCode({ codes: codes }).then((resEnumData) => {
        for (var key in resEnumData) {
          resEnumData[key] = resEnumData[key].map(item => {
            item.label = t(item.label);
            return item;
          });
        }
        enumData.value = resEnumData;
      });
      //带出用户保存的搜索方案
      const filter = { FilterInfo: { Logic: 'And', Filters: [{ Field: 'FilterType', Operator: 'equals', Value: 2 }, { Field: 'Code', Operator: 'equals', Value: props.filterCode }] }, Sorting: 'Sort,CreationTime' };
      filterFilters(filter).then((res) => {
        options.value = res.Items.map((item: any) => {
          return { Id: item.Id, label: item.Name, value: item.UserFilter };
        });
      });
    });
  } else {
    fileds.value = props.fileds;
  }
});
defineExpose({
  loadingStop,
});
</script>

SearchDialog.vue

<template>
  <t-dialog v-model:visible="dialogVisible" width="920px" destroy-on-close :header="t('pages.common.advancedSearch')"
    :ok-text="t('pages.common.submit')" :cancel-text="t('pages.common.cancel')" @confirm="submit" @cancel="close">
    <t-space :size="8">
      <div style="align-items: center;">{{ t('pages.common.searchName') }}</div>
      <t-select v-model="selectedValue" :options="options" @change="handleChange" />
      <t-button @click="saveShowHandle">
        {{ $t('pages.common.saveSearch') }}
      </t-button>
      <t-button theme="default" @click="delHandle">
        {{ $t('pages.common.delSearch') }}
      </t-button>
      <t-button theme="default" @click="renameHandle">
        {{ $t('pages.common.renameSearch') }}
      </t-button>
      <t-button theme="default" @click="pasteHandle" :title="t('pages.common.excelCopyPaste')">
        {{ $t('pages.common.paste') }}
      </t-button>
      <t-button theme="default" @click="clearHandle">
        {{ $t('pages.common.clearSearch') }}
      </t-button>
    </t-space>
    <div style="max-height: 500px; overflow: auto">
      <Tree :filterCode="props.filterCode" :filter="filters" :fileds="props.fileds" :enumData="props.enumData" />
    </div>
  </t-dialog>
  <t-dialog v-model:visible="visibleBody" attach="body" :header="t('pages.common.name')" destroy-on-close
    :on-confirm="saveHandle">
    <template #body>
      <t-input v-model="saveName" />
    </template>
  </t-dialog>
</template>
<script setup lang="ts">
import { t } from '@/locales';
import Tree from './childFilter.vue';
import { ref, getCurrentInstance, onActivated, onMounted, watch } from 'vue';
import { DialogPlugin, DialogProps, MessagePlugin, Input } from 'tdesign-vue-next';
import { getEnumCode } from '@/api/system/enum';
import { filterFilters, getFilters, createFilters, modifyFilters, deleteFilters, deleteFiltersBatch, getFiltersCode, getDetail, modifyFiltersSort, modifyDetailFiltersSort, forbidFiltersBatch, enableFiltersBatch } from '@/api/system/filters';
const { ctx } = getCurrentInstance();
const getAttach: DialogProps['attach'] = () => ctx.$root.$el;
const props = defineProps({
  visible: {
    type: Boolean,
    default: false,
  },
  filters: {
    type: Object,
    default: () => { },
  },
  filterCode: {
    type: String,
  },
  fileds: {
    type: Array,
    default: () => [],
  },
  enumData: {
    type: Array,
    default: () => [],
  },
  options: {
    type: Array,
    default: () => [],
  },
});
const visibleBody = ref(false);
const filters = ref([]);
const options = ref([]);
const selectedValue = ref();
const selectedOption = ref({});
const saveName = ref('');
const dialogVisible = ref(false);
const delId = ref(0);
const reName = ref(false);
const emits = defineEmits(['dynamicFilter', 'refreshOptions', 'loadingStart']);
const submit = () => {
  console.log('props.filters', props.filters);
  dialogVisible.value = false;
  emits('loadingStart');
  let dynamicFilter = { Logic: props.filters.Logic, Filters: props.filters.Filters.filter((item) => item.Value !== '') };  
  emits('dynamicFilter', dynamicFilter);
};
const show = async () => {
  await 1;
  dialogVisible.value = true;
  options.value = props.options;
};
const close = () => {
  dialogVisible.value = false;
};
const handleChange = (value, context) => {
  delId.value = context.option.Id;
  selectedOption.value = context.option;
  filters.value = JSON.parse(value);
};
const saveHandle = () => {
  const api = selectedValue.value ? modifyFilters : createFilters;
  let Name = '';
  if (selectedValue.value) {
    Name = selectedOption.value.label;
  } else {
    Name = saveName.value.replaceAll(' ', '').replaceAll(' ', '');
  }
  // 重命名
  if (reName.value) {
    Name = saveName.value.replaceAll(' ', '').replaceAll(' ', '');
    if (Name !== '') {
      reName.value = false;
    }
  }
  if (Name === '') {
    MessagePlugin.error(t('pages.common.enterSearchName'));
  } else {
    var postData = { Id: delId.value, FilterType: 2, FilterScope: 3, Code: props.filterCode, Name: Name, UserFilter: JSON.stringify(filters.value), Status: 3 };
    api(postData).then((res) => {
      if (selectedValue.value) {
        const targetData = options.value.find(item => item.Id === delId.value);
        targetData.value = postData.UserFilter;
        targetData.label = Name;
        emits('refreshOptions', options.value);
      } else {
        options.value.push({ Id: res.Id, label: Name, value: postData.UserFilter });
        visibleBody.value = false;
      }

      MessagePlugin.success(t('pages.common.success'));
    }).finally(() => {
      visibleBody.value = false;
    });
  }
};
const saveShowHandle = () => {
  if (selectedValue.value) {
    saveHandle();
  } else {
    visibleBody.value = true;
  }
};
const delHandle = () => {
  if (delId.value > 0) {
    const DialogInstance = DialogPlugin.confirm({
      header: t('pages.common.deleteWarning'),
      body: t('pages.common.deleteConfirm'),
      confirmBtn: {
        content: t('pages.common.ok'),
        theme: 'primary',
        loading: false,
      },
      onConfirm: () => {
        deleteFilters({ Id: delId.value })
          .then((res) => {
            const data = options.value.filter((item) => item.Id !== delId.value);
            options.value = data;
            selectedValue.value = null;
            emits('refreshOptions', data);
            MessagePlugin.success(t('pages.common.success'));
            DialogInstance.destroy();
          })
          .finally(() => {
            // finally中取消loading状态
            DialogInstance.update({
              confirmBtn: { loading: false },
            });
          });
      },
      onCancel: () => {
        DialogInstance.destroy();
      },
    });
  }
};
const renameHandle = () => {
  if (selectedValue.value) {
    visibleBody.value = true;
    reName.value = true;
  }
};
const ops = [
  { label: t('pages.common.Equal'), value: 'Equal' },
  { label: t('pages.common.NotEqual'), value: 'NotEqual' },
  { label: t('pages.common.Contains'), value: 'Contains' },
  { label: t('pages.common.NotContains'), value: 'NotContains' },
  { label: t('pages.common.StartsWith'), value: 'StartsWith' },
  { label: t('pages.common.NotStartsWith'), value: 'NotStartsWith' },
  { label: t('pages.common.EndsWith'), value: 'EndsWith' },
  { label: t('pages.common.NotEndsWith'), value: 'NotEndsWith' },
  { label: t('pages.common.GreaterThan'), value: 'GreaterThan' },
  { label: t('pages.common.GreaterThanOrEqual'), value: 'GreaterThanOrEqual' },
  { label: t('pages.common.LessThan'), value: 'LessThan' },
  { label: t('pages.common.LessThanOrEqual'), value: 'LessThanOrEqual' },
  { label: t('pages.common.DateRange'), value: 'DateRange' },
];
const pasteHandle = () => {
  navigator.clipboard.readText().then((text) => {
    let rows = text.split('\r\n');
    let keyVal = [];
    for (const key in rows) {
      if (rows[key]) {
        let cols = rows[key].split('\t');
        let col = props.fileds.find((item) => item.DisplayName === cols[0]);
        if (col != null) {
          let op = ops.find((item) => item.label === cols[1]);
          keyVal.push({ Field: col.FiledName, Operator: op?.value, Value: cols[2] });
        }
      }
    }
    filters.value = { Logic: 'Or', Filters: keyVal };
  });
};
const clearHandle = () => {
  selectedValue.value = null;
  delId.value = 0;
  filters.value = { Logic: 'Or', Filters: [{ Field: '', Operator: '', Value: null }, { Field: '', Operator: '', Value: null }] };
};
onMounted(() => {
  options.value = props.options;
});
watch(
  () => props.filters,
  (newVal, oldVal) => {
    filters.value = newVal;
  },
  { immediate: true, deep: true }
);
defineExpose({
  show,
});
</script>

childFilter.vue

<template>
  <div class="db-advanced-panel">
    <table border="0" style="width: 100%;">
      <tr>
        <td style="position: relative;">
          <table cellpadding="0" cellspacing="0" border="0" class="sub-table" style="width: 100%;">
            <tr class="item" v-for="(item, index) in filter.Filters" :key="index">
              <td style="vertical-align: middle; position: relative;">
                <div class="line"></div>
                <div class="line-v"></div>
                <div style="padding: 0px 0px 0px 31px;">
                  <div class="search-row">
                    <t-space :size="8" v-if="!item.Filters">
                      <t-select v-model="item.Field" :options="props.fileds"
                        :keys="{ label: 'DisplayName', value: 'FiledName' }" filterable :loading="loading"
                        @enter="handleEnter" @change="(value, context) => handleChange(item, value, context)" />
                      <t-select style="width: 100px;" v-model="item.Operator" :options="item.operators"
                        @change="(value) => handleChangeOperator(item, value)" />
                      <t-input v-if="getType(item, index) === 'string'"
                        v-model="item.Value"></t-input>
                        <div v-if="getType(item, index) === 'switch'">                        
                        <t-switch v-model="item.Value" />
                      </div>
                      <t-select v-if="getType(item, index) === 'select'" v-model="item.Value"
                        :options="item.ValueOptions" @change="(value) => handleChangeOperator(item, value)" />
                        <user-input v-if="getType(item, index) === 'user'" v-model="item.Value" />                        
                        <org-input v-if="getType(item, index) === 'org'" v-model="item.Value"/>
                        <dept-input v-if="getType(item, index) === 'dept'" v-model="item.Value" />
                        <unit-select filterable clearable v-if="getType(item, index) === 'unit'" v-model="item.Value" />
                        <location-select v-if="getType(item, index) === 'location'" v-model="item.Value" />
                        <processes-select v-if="getType(item, index) === 'process'" v-model="item.Value"/>
                        <equipment-select v-if="getType(item, index) === 'equipment'" v-model="item.Value" />
                        <warehouse-select v-if="getType(item, index) === 'warehouse'" v-model="item.Value" />
                        <supplier-select-input v-if="getType(item, index) === 'supplier'" v-model="item.Value" />
                        <technology-select v-if="getType(item, index) === 'technology'" v-model="item.Value" />
                        <bom-select v-if="getType(item, index) === 'bom'" v-model="item.Value" />
                        <product-line-select v-if="getType(item, index) === 'line'" v-model="item.Value" />
                        <material-select-input v-if="getType(item, index) === 'material'" v-model="item.Value" />
                        <work-order-select-input v-if="getType(item, index) === 'order'" v-model="item.Value" />
                        <property-select v-if="getType(item, index) === 'property'" v-model="item.Value" />
                        <property-data-type-value v-if="getType(item, index) === 'propertydata'" v-model="item.Value" />
                        <product-rule-select v-if="getType(item, index) === 'rule'" v-model="item.Value" />
                        <decay-select v-if="getType(item, index) === 'decay'" v-model="item.Value" />
                      <t-date-picker v-if="getType(item, index) === 'date'" v-model="item.Value"
                        allow-input clearable :format="globalDateFormat" />
                      <t-date-picker v-if="getType(item, index) === 'datetime'"
                        v-model="item.Value" enable-time-picker allow-input clearable :format="globalDateTimeFormat" />
                      <t-date-range-picker allow-input clearable
                        v-if="getType(item, index) === 'daterange'" v-model="item.Value" :presets="presets" />
                      <t-time-picker v-if="getType(item, index) === 'time'"
                        :format="globalTimeFormat" v-model="item.Value" />
                      <t-button shape="square" variant="text" :title="$t('pages.common.delFilter')"
                        @click="deleteHandle(filter, index, parentData)">
                        <template #icon>
                          <DeleteIcon size="1.5em" />
                        </template>
                      </t-button>
                      <t-button shape="square" variant="text" :title="$t('pages.common.addFilter')"
                        @click="addHandle(filter, index)">
                        <template #icon>
                          <AddIcon size="1.5em" />
                        </template>
                      </t-button>
                      <t-button shape="square" variant="text" :title="$t('pages.common.addFilterGroup')"
                        @click="addGroupHandle(filter, index)">
                        <template #icon>
                          <FolderAdd1Icon size="1.5em" />
                        </template>
                      </t-button>
                    </t-space>
                    <Tree v-if="item.Filters" :fileds="props.fileds" :filter="item" :parentData="filter" />
                  </div>
                </div>
              </td>
            </tr>
          </table>
          <div class="btn-relation" @click="logicHandle(filter)">{{ filter.Logic == 'And' ? t('pages.common.and') :
            t('pages.common.or') }}
          </div>
        </td>
      </tr>
    </table>
  </div>
</template>
<script lang="ts" setup>
import { globalDateFormat, globalDateTimeFormat, globalTimeFormat } from '@/config/global';
import { onMounted, readonly, reactive, ref, toRefs, watch } from 'vue';
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
import { AddIcon, FolderAdd1Icon, DeleteIcon } from 'tdesign-icons-vue-next';
import { alertError } from '@/utils/common'
import { t } from '@/locales';
import dayjs from 'dayjs';
import userInput from '@/components/UserSelect/src/UserInput.vue';
import orgInput from '@/components/OrgSelect/src/OrgInput.vue';
import deptInput from '@/components/DeptSelect/src/DeptInput.vue';
import unitSelect from '@/components/Unit/src/UnitSelect.vue';
import WarehouseSelect from '@/components/warehouse/src/warehouse-select.vue';
import WorkOrderSelectInput from '@/components/WorkOrder/src/work-order-select-input.vue';
import MaterialSelectInput from '@/components/Material/src/material-select-input.vue';
import ProductLineSelect from '@/components/ProductLine/src/ProductLineSelect.vue';
import LocationSelect from '@/components/location/src/location-select.vue';
import BomSelect from '@/components/BomSelect/src/BomSelect.vue';
import ProcessesSelect from '@/components/Processes/src/processes-select.vue';
import EquipmentSelect from '@/components/EquipmentSelect/src/EquipmentSelect.vue';
import TechnologySelect from '@/components/Technology/src/technology-select.vue';
import SupplierSelectInput from '@/components/Supplier/src/supplier-select-input.vue';
import PropertySelect from '@/components/Property/src/property-select.vue';
import PropertyDataTypeValue from '@/components/Property/src/property-data-type-value.vue';
import ProductRuleSelect from '@/components/ProductRule/src/ProductRuleSelect.vue';
import DecaySelect from '@/components/Decay/src/DecaySelect.vue';
const props = defineProps({
  filter: {
    type: Object,
    default: () => {},
  },
  filterCode: {
    type: String,
  },
  fileds: {
    type: Array,
    default: () => [],
  },
  parentData: {
    type: Object,
    default: () => {},
  },
  enumData: {
    type: Object,
    default: () => {},
  },
});
const presets = ref({
  最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
  最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
  今天: [dayjs().toDate(), dayjs().toDate()],
});
const ops: object = {
  string: [
    { label: t('pages.common.Equal'), value: 'Equal' },
    { label: t('pages.common.NotEqual'), value: 'NotEqual' },
    { label: t('pages.common.Contains'), value: 'Contains' },
    { label: t('pages.common.NotContains'), value: 'NotContains' },
    { label: t('pages.common.StartsWith'), value: 'StartsWith' },
    { label: t('pages.common.NotStartsWith'), value: 'NotStartsWith' },
    { label: t('pages.common.EndsWith'), value: 'EndsWith' },
    { label: t('pages.common.NotEndsWith'), value: 'NotEndsWith' },
    { label: t('pages.common.Any'), value: 'Any' },
    { label: t('pages.common.NotAny'), value: 'NotAny' },
  ],
  int: [
    { label: t('pages.common.Equal'), value: 'Equal' },
    { label: t('pages.common.NotEqual'), value: 'NotEqual' },
    { label: t('pages.common.GreaterThan'), value: 'GreaterThan' },
    { label: t('pages.common.GreaterThanOrEqual'), value: 'GreaterThanOrEqual' },
    { label: t('pages.common.LessThan'), value: 'LessThan' },
    { label: t('pages.common.LessThanOrEqual'), value: 'LessThanOrEqual' },
  ],
  date: [
  { label: t('pages.common.Equal'), value: 'Equal' },
    { label: t('pages.common.NotEqual'), value: 'NotEqual' },
    { label: t('pages.common.GreaterThan'), value: 'GreaterThan' },
    { label: t('pages.common.GreaterThanOrEqual'), value: 'GreaterThanOrEqual' },
    { label: t('pages.common.LessThan'), value: 'LessThan' },
    { label: t('pages.common.LessThanOrEqual'), value: 'LessThanOrEqual' },
    { label: t('pages.common.DateRange'), value: 'DateRange' },
  ],
  time: [
    { label: t('pages.common.Equal'), value: 'Equal' },
    { label: t('pages.common.NotEqual'), value: 'NotEqual' },
    { label: t('pages.common.GreaterThan'), value: 'GreaterThan' },
    { label: t('pages.common.GreaterThanOrEqual'), value: 'GreaterThanOrEqual' },
    { label: t('pages.common.LessThan'), value: 'LessThan' },
    { label: t('pages.common.LessThanOrEqual'), value: 'LessThanOrEqual' },
  ],
  Boolean: [
    { label: t('pages.common.Equal'), value: 'Equal' },
    { label: t('pages.common.NotEqual'), value: 'NotEqual' },
  ],
  any: [
    { label: t('pages.common.Equal'), value: 'Equal' },
    { label: t('pages.common.NotEqual'), value: 'NotEqual' },
    { label: t('pages.common.Any'), value: 'Any' },
    { label: t('pages.common.NotAny'), value: 'NotAny' },
  ],
};
var delIndex: number = 0;
const traverseFilter = (filters: any) => {
  for (let item of filters) {
    //console.log(JSON.stringify(item));
    if (item.Filters) {
      delIndex = delIndex + 1;
      traverseFilter(item);
    } else {
      if (item.del) {
        return delIndex;
      }
    }
  }
};
const logicHandle = (data) => {
  data.Logic = data.Logic === 'And' ? 'Or' : 'And';
};
const deleteHandle = (data, index, parent) => {
  if (parent == null && data.Filters.length === 1) {
    alertError(t('pages.common.queryDeleteError'));
  } else {
    data.Filters.splice(index, 1);//删除,只能用splice删除,不能用delete
    //delete data.Filters[index]; //不能用这种删除方法  
    if (parent != null && props.filter.Filters.length === 0) {
      for (let i = 0; i < parent.Filters.length; i++) {
        if (parent?.Filters[i].Filters) {
          parent.Filters.splice(i, 1);
        }
      }
    }
  }
};
const addHandle = (data, index) => {
  console.log('data', data);
  data.Filters.splice(index + 1, 0, {
    Field: '',
    Operator: '',
    Value: null,
  });
};
const addGroupHandle = (data, index) => {
  const group = {
    Logic: 'Or',
    Filters: [{
      Field: '',
      Operator: '',
      Value: null,
    },
    {
      Field: '',
      Operator: '',
      Value: null,
    },]
  };
  data.Filters.splice(index + 1, 0, group);
};
const getType = (data, index) => {
  if (data.EnumCode != null) {
    data.ValueOptions = props.enumData[data.EnumCode];
  }
  if (data.valueType) {
    if (data.valueType === 'daterange') {
      data.operators = ops.date;
    } else {
      if (data.dataType === null || data.dataType === undefined) {
        console.log('data', props.fileds);
        let datatypes = props.fileds.filter((filed) => { return filed.FiledName === data.Field });
        if (datatypes.length > 0) {
          data.dataType = datatypes[0].DataType;
        } else {
          data.dataType = 'string';
        }
      }
      data.operators = ops[data.dataType];
    }
    return data.valueType;
  }
  data.operators = ops.string;
  return 'string';
};
const handleEnter = ({ value, e, inputValue }) => {
  //console.log('handleEnter: ', value, e, inputValue);
};
const handleChange = (item, value, context) => {
  let datatypes = props.fileds.filter((filed) => { return filed.FiledName === item.Field });
  if (datatypes.length > 0) {
    item.valueType = datatypes[0].DataType;
  }
  else {
    item.valueType = 'string';
  }
  item.operators = ops[item.valueType];
};
const handleChangeOperator = (item, value) => {
  if (item.valueType === 'date' || item.valueType === 'daterange') {
    if (value === 'DateRange') {
      item.valueType = 'daterange';
      item.Value = [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()];
    } else {
      item.valueType = 'date';
      item.Value = dayjs().toDate();
    }
  }
};
</script>

页面调用示例,filterCode值为高级搜索配置界面的的编码

        

后端搜索代码,请参考freesql动态搜索

文档地址

动态操作 | FreeSql 官方文档

当然也支持其它orm和原生sql,例如

SqlSugar使用IConditionalModel,efcore使用Dynamic LINQ

解析为原生sql代码

 public class QueryHelper
 {

     public static (string, Dictionary<string, object>, string) GenerateWhereString(DynamicFilterInfo filterInfo)
     {
         string order = "Id desc";
         var lstValues = new Dictionary<string, object>();
         int index = 0;
         var whereClause = GenerateWhereClause(filterInfo, ref index);

         filterInfo.Travel((filterInfo) => lstValues.Add(lstValues.Count.ToString(), GetValue(filterInfo)));
         return (whereClause, lstValues, order);
     }
     public static string GenerateOrder(QueryCondition queryCondition)
     {
         string order = "Id desc";
         if (queryCondition.sort.Count > 0)
         {
             order = string.Empty;
             foreach (var item in queryCondition.sort)
             {
                 order += $"{item.field} {item.order},";
             }
             order = order.Trim(',');
         }
         return order;
     }
     static bool isArray =false;
     static DateTime[] dates;
     static bool isDate1 = false;
     protected static string GenerateWhereClause(DynamicFilterInfo filterInfo, ref int index)
     {
         var lstConditions = new List<string>();

         if (filterInfo.Value!=null && filterInfo.Value.ToString().Trim()!=string.Empty)
         {
             if (filterInfo.Value.GetType() == typeof(JArray))
             {
                 isArray=true;
                 dates = Newtonsoft.Json.JsonConvert.DeserializeObject<DateTime[]>(filterInfo.Value.ToString());
                 isDate1=true;
                 lstConditions.Add(ConvertToCondition(filterInfo, index++));
             }
             isArray = false;
             lstConditions.Add(ConvertToCondition(filterInfo, index++));            
         }
         if (filterInfo.Filters != null && filterInfo.Filters.Count > 0)
         {
             foreach (var subGroup in filterInfo.Filters)
             {
                 if (subGroup.Value != null && subGroup.Value.ToString().Trim() != string.Empty)
                 {
                     lstConditions.Add(GenerateWhereClause(subGroup, ref index));
                 }
             }
         }

         return $"({lstConditions.JoinAsString(filterInfo.Logic.ToString().ToLower() == "and" ? " and " : " or ")})";
     }

     protected static string ConvertToCondition(DynamicFilterInfo condition, int index)
     {
         string left = GetLeft(condition, index);
         string right = GetRight(condition, index);
         switch (condition.Operator.ToString().ToLower())
         {
             case "equals":
             case "equal":
             case "eq":
             case "=":
             case "==":
                 return $"{left} = {right}";
             case "<>":
             case "!=":
             case "notequal":
                 return $"{left} <> {right}";
             case "greater":
             case "greaterthan":
             case ">":
                 return $"{left} > {right}";
             case "greaterorequal":
             case "greaterthanorequal":
             case ">=":
                 return $"{left} >= {right}";
             case "less":
             case "lessthan":
             case "<":
                 return $"{left} < {right}";
             case "lessorequal":
             case "lessthanorequal":
             case "<=":
                 return $"{left} <= {right}";
             case "startwith":
                 return $"{left} like {right}";
             case "notstartwith":
                 return $"{left} not like {right}";
             case "endwith":
                 return $"{left} like {right}";
             case "notendwith":
                 return $"{left} not like {right}";
             case "contain":
             case "contains":
             case "like":
                 return $"{left} like {right}";
             case "notcontains":
                 return $"{left} not like {right}";
             case "range":
                 return $"{left} in({right})";
             case "daterange":
                 if (isArray)
                 {
                     return $"{left} >={right}";
                 }
                 else
                 {
                     return $"{left} <={right}";
                 }
             case "in":
             case "any":
                 return $"{left} in {right}";
             case "notany":
             case "notcontain":
                 return $"{left} not in {right}";
             default:
                 throw new InvalidOperationException($"Unknown dynamic query operator: {condition.Operator}");
         }
     }
     protected static object GetValue(DynamicFilterInfo condition)
     {

         switch (condition.Operator.ToString().ToLower())
         {

             case "startwith":
             case "notstartwith":
                 return $"{condition.Value}%";
             case "endwith":
             case "notendwith":
                 return $"%{condition.Value}";
             case "contain":
             case "contains":
             case "like":              
             case "notcontains":
                 return $"%{condition.Value}%";
             case "daterange":
                 if (isDate1)
                 {
                     isDate1 = false;
                     return dates[0].ToShortDateString();
                 }
                 else
                 {
                     return dates[1].ToShortDateString();
                 }
             case "in":
             case "any":
             case "notany":
             case "notcontain":
                 return condition.Value.ToString().Split(','); 
             default:
                 return condition.Value;
         }
     }
     protected static string GetLeft(DynamicFilterInfo condition, int index)
     {
         return condition.Field;
     }

     protected static string GetRight(DynamicFilterInfo condition, int index)
     {
         return $"@{index}";
     }

 }

如果有问题请与我联系,图中找。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/929297.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在办公室环境中用HMD替代传统显示器的优势

VR头戴式显示器&#xff08;HMD&#xff09;是进入虚拟现实环境的一把钥匙&#xff0c;拥有HMD的您将能够在虚拟现实世界中尽情探索未知领域&#xff0c;正如如今的互联网一样&#xff0c;虚拟现实环境能够为您提供现实中无法实现的或不可能实现的事。随着技术的不断进步&#…

PPT怎样做的更加精美

目录 PPT怎样做的更加精美 3D的GIF图片 3维空间图​编辑 结果有明显的对比 阅读高质量文献,采用他们的图 PPT怎样做的更加精美 3D的GIF图片 3维空间图 结果有明显的对比

Altium Designer学习笔记 26-27 PCB布局优化_规则创建

基于Altium Designer 23学习版&#xff0c;四层板智能小车PCB 更多AD学习笔记&#xff1a;Altium Designer学习笔记 1-5 工程创建_元件库创建Altium Designer学习笔记 6-10 异性元件库创建_原理图绘制Altium Designer学习笔记 11-15 原理图的封装 编译 检查 _PCB封装库的创建Al…

在Unity编辑模式下运行Mono中的方法

[ExecuteAlways] 最简单的方法当然是直接给Mono加上[ExecuteAlways]修饰&#xff0c;这样Mono中的Awake&#xff0c;Update等等都可以在编辑模式下按照原本的时机运行。 [ExecuteAlways] public class TestScript : MonoBehaviour {void TestMethod(){Debug.Log("TestMe…

PDF与PDF/A的区别及如何使用Python实现它们之间的相互转换

目录 概述 PDF/A 是什么&#xff1f;与 PDF 有何不同&#xff1f; 用于实现 PDF 与 PDF/A 相互转换的 Python 库 Python 实现 PDF 转 PDF/A 将 PDF 转换为 PDF/A-1a 将 PDF 转换为 PDF/A-1b 将 PDF 转换为 PDF/A-2a 将 PDF 转换为 PDF/A-2b 将 PDF 转换为 PDF/A-3a 将…

【超图】iClient3D for Cesium 以动静结合方式加载WMTS服务

作者&#xff1a;taco 一、问题来源 在最近支持的项目中&#xff0c;我们面临一个挑战&#xff1a;客户需要在前端动态加载高达3亿级别的白模底面数据。这样做的主要原因是客户的数据库会频繁更新&#xff0c;因此我们需要采用动态加载的方式来确保用户界面能够实时反映最新的…

Y20030026 VUE+Springboot+MYSQL+LW+实体店推广平台的设计与实现 源代码 配置 文档 PPT

实体店推广平台的设计与实现 1.摘要2.开发目的和意义3.系统功能设计4.系统界面截图5.源码获取 1.摘要 随着互联网的普及和电子商务的快速发展&#xff0c;消费者的购物习惯发生了显著变化。越来越多的消费者倾向于在线购物&#xff0c;享受便捷、丰富的选择和个性化的购物体验…

基数排序(代码+注释)

#include <stdio.h> #include <stdlib.h>// 获取数组中的最大值 int GetMax(int* a, int n) {int max a[0];for (int i 1; i < n; i) {if (a[i] > max) {max a[i];}}return max; }// 对数组按照某个位数进行计数排序 void CountingSortForRadix(int* a, i…

esp8266 编译、烧录环境搭建

一、准备 xtensa-lx106-elf-gcc8-4-0-esp-2020r3-win32下载&#xff1a;点击跳转 MSYS2 压缩包文件&#xff1a; 固件烧录工具&#xff1a;点击跳转 esp8266源码地址&#xff1a;点击跳转 二、搭建编译环境 1、在D盘创建一个ESP8266目录&#xff0c;解压MSYS2.zip到里面&a…

【Delphi】modbus-TCP 协议库

在日常开发中&#xff0c;也会遇到使用modbus的部件&#xff0c;比如温度控制器、读卡器等等&#xff0c;那么使用Delphi开发&#xff0c;也就必须遵守modbus-TCP协议&#xff0c;如果自己使用TCP控件写也没有问题&#xff0c;不过如果有开源的三方库&#xff0c;别人已经调试过…

AgGrid 组件封装设计笔记:自定义 icon 以及每个 icon 的点击事件处理

文章目录 问题目前解决效果 v1思路 目前解决效果 v0思路 代码V1 问题 自己封装的 AgGrid 如何自定义传递 icon &#xff0c;以及点击事件的处理&#xff1f; 目前解决效果 v1 思路 目前解决效果 v0 思路 一张图片说明一下 代码 V1 父组件使用 <template><MyPageL…

MySQL——MySQL 日志

文章目录 MySQL 文件介绍二进制日志&#xff08;bin log&#xff09;概念binlog 日志的三种格式两阶段提交binlog 落盘更新语句执行流程 慢查询日志&#xff08;slow query log&#xff09;重做日志&#xff08;redo log&#xff09;redo log 日志的理解WAL 技术redo log 的工作…

解决git did not exit cleanly (exit code 128)问题

解决 git did not exit cleanly &#xff08;exit code 128&#xff09;问题 1、错误描述2、解决方法2.1 方法一2.2 方法二 1、错误描述 使用TortoiseGit进行操作时&#xff0c;总是提示下述错误。 2、解决方法 2.1 方法一 打开 TortoiseGit -> Settings 点击 Network&…

家校通小程序实战教程05教师登录

目录 1 搭建角色选择页面2 设置登录方法3 创建加入班级页面4 创建教师主页页面5 完善登录方法总结 我们上一篇开发了教师管理的后台功能&#xff0c;后台一般是给管理员提供的。教师一般是使用小程序开展各类业务&#xff0c;本篇介绍一下教师如何通过小程序登录。 1 搭建角色选…

yolov5 解决:export GIT_PYTHON_REFRESH=quiet

当我们在第一次运行YOLOv5中的train.py程序时&#xff1a;可能会出现以下报错&#xff1a; This initial warning can be silenced or aggravated in the future by setting the $GIT_PYTHON_REFRESH environment variable. Use one of the following values: - quiet|q|silen…

neo4j如何存储关于liquidity structure的层次和关联结构

在 Neo4j 中存储关于流动性结构&#xff08;liquidity structure&#xff09;的层次和关联结构非常适合&#xff0c;因为 Neo4j 是一个基于图的数据库&#xff0c;能够自然地建模和存储复杂的关系和层次结构。下面是如何在 Neo4j 中设计和实现这样的数据模型的详细步骤和示例。…

SpringBoot高级-底层原理

目录 1 SpringBoot自动化配置原理 01-SpringBoot2高级-starter依赖管理机制 02-SpringBoot2高级-自动化配置初体验 03-SpringBoot2高级-底层原理-Configuration配置注解 04-SpringBoot2高级-底层原理-Import注解使用1 05-SpringBoot2高级-底层原理-Import注解使用2 06-S…

C++模拟堆

模板题目 图片来源Acwing 堆的基础知识 代码实现 #include<iostream> #include<algorithm>using namespace std;const int N 1e5 10; int a[N]; int n, m;void down(int u) {int t u;if (2 * u < n && a[2 * u] < a[u]){t 2 * u;}if (2 * u …

RNACOS:用Rust实现的Nacos服务

RNACOS是一个使用Rust语言开发的Nacos服务实现&#xff0c;它继承了Nacos的所有核心功能&#xff0c;并在此基础上进行了优化和改进。作为一个轻量级、快速、稳定且高性能的服务&#xff0c;RNACOS不仅包含了注册中心、配置中心和Web管理控制台的功能&#xff0c;还支持单机和集…

Flink常见面试题

1、Flink 的四大特征&#xff08;基石&#xff09; 2、Flink 中都有哪些 Source&#xff0c;哪些 Sink&#xff0c;哪些算子&#xff08;方法&#xff09; 预定义Source 基于本地集合的source&#xff08;Collection-based-source&#xff09; 基于文件的source&#xff08;…