以员工管理表为例,常用栏位如下图
基本需求:默认搜索框可以模糊查询搜索工号、姓名、手机号、年龄等不需要关联查询基本字段。
特殊需求需要高级搜索:例如按入职区间、部门、公司、年龄段、上级主管等进行模糊搜索,且支持并且或者等关系(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}";
}
}
如果有问题请与我联系,图中找。