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}";
     }

 }

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神色自若

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值