feat: 高级搜索-样式(除视图)

This commit is contained in:
teukkk 2024-08-28 19:21:40 +08:00 committed by Craftsman
parent 86581e4580
commit e755c9de4d
9 changed files with 427 additions and 540 deletions

View File

@ -248,6 +248,8 @@
.btn-outline-sec-hover();
.btn-outline-sec-active();
.btn-outline-sec-disabled();
border-color: var(--color-text-n8) !important;
}
.arco-btn-outline--danger {
.btn-outline-danger-default();

View File

@ -1,415 +0,0 @@
<template>
<a-form ref="formRef" :model="formModel" layout="vertical">
<div class="w-full overflow-y-auto bg-[var(--color-text-n9)] px-[12px] py-[12px]">
<header class="flex flex-row items-center justify-between">
<div class="text-[var(--color-text-2)]">{{ t('advanceFilter.setFilterCondition') }}</div>
<div class="flex flex-row items-center text-[var(--color-text-2)]">
<div class="text-[var(--color-text-2)]">{{ t('advanceFilter.accordBelow') }}</div>
<div class="ml-[16px]">
<a-select v-model:model-value="accordBelow" size="small">
<a-option value="AND">{{ t('advanceFilter.all') }}</a-option>
<a-option value="OR">{{ t('advanceFilter.any') }}</a-option>
</a-select>
</div>
<div class="ml-[8px] text-[var(--color-text-2)]">{{ t('advanceFilter.condition') }}</div>
</div>
</header>
<a-scrollbar :style="{ 'max-height': '300px', 'overflow': 'auto' }">
<article class="mt-[12px] flex max-h-[300px] flex-col gap-[8px]">
<section
v-for="(item, idx) in formModel.list"
:key="item.dataIndex || `filter_item_${idx}`"
class="flex flex-row items-center gap-[8px]"
>
<div class="flex-1 grow">
<a-form-item
:field="`list[${idx}].dataIndex`"
hide-asterisk
class="hidden-item"
:rules="[{ required: true, message: t('advanceFilter.plaseSelectFilterDataIndex') }]"
>
<a-select v-model="item.dataIndex" allow-search @change="(v) => dataIndexChange(v, idx)">
<div v-for="(option, i) in currentOptionArr" :key="option.dataIndex">
<a-option :value="option.dataIndex" :disabled="option.disabled">
{{ t(option.title as string) }}
</a-option>
<a-divider
v-if="
props?.customList &&
(props.customList || []).length &&
(props.configList || []).length - 1 === i
"
class="!my-1"
/>
</div>
</a-select>
</a-form-item>
</div>
<div class="grow-0">
<a-form-item
:field="`list[${idx}].operator`"
hide-asterisk
class="hidden-item"
:rules="[{ required: true, message: t('advanceFilter.plaseSelectOperator') }]"
>
<a-select
v-model="item.operator"
class="w-[120px]"
:disabled="!item.dataIndex"
@change="(v) => operationChange(v, item.dataIndex as string, idx)"
>
<a-option
v-for="option in getOperationOption(item.type as FilterType, item.dataIndex as string)"
:key="option.value"
:value="option.value"
>
{{ t(option.label as string) }}
</a-option>
</a-select>
</a-form-item>
</div>
<div class="flex-1 grow">
<a-form-item
:field="`list[${idx}].value`"
:rules="[{ required: true, message: t('advanceFilter.plaseInputFilterContent') }]"
hide-asterisk
class="hidden-item"
>
<a-input
v-if="item.type === FilterType.INPUT && !isMutipleOperator(item.operator as string)"
v-model:model-value="item.value"
class="w-full"
allow-clear
:disabled="!item.dataIndex"
:max-length="255"
/>
<MsTagsInput
v-else-if="isMutipleOperator(item.operator as string)"
v-model:model-value="item.value"
:disabled="!item.dataIndex"
allow-clear
unique-value
retain-input-value
/>
<a-input-number
v-else-if="item.type === FilterType.NUMBER"
v-model:model-value="item.value"
class="w-full"
allow-clear
:disabled="!item.dataIndex"
:max-length="255"
/>
<MsSelect
v-else-if="item.type === FilterType.SELECT"
v-model:model-value="item.value"
class="w-full"
allow-clear
allow-search
:placeholder="t('common.pleaseSelect')"
:disabled="!item.dataIndex"
:options="item.selectProps?.options || []"
v-bind="item.selectProps"
/>
<a-tree-select
v-else-if="item.type === FilterType.TREE_SELECT"
v-model:model-value="item.value"
:data="item.treeSelectData"
:disabled="!item.dataIndex"
v-bind="(item.treeSelectProps as any)"
/>
<a-date-picker
v-else-if="item.type === FilterType.DATE_PICKER && item.operator !== 'between'"
v-model:model-value="item.value"
class="w-full"
show-time
format="YYYY-MM-DD hh:mm"
:disabled="!item.dataIndex"
/>
<a-range-picker
v-else-if="item.type === FilterType.DATE_PICKER && item.operator === 'between'"
v-model:model-value="item.value"
class="w-full"
show-time
format="YYYY-MM-DD HH:mm"
:separator="t('common.to')"
:disabled="!item.dataIndex"
/>
<MsCascader
v-else-if="item.type === FilterType.CASCADER"
v-model:model-value="item.value"
:options="item.cascaderOptions || []"
:disabled="!item.dataIndex"
v-bind="item.cascaderProps"
/>
<a-textarea
v-else-if="item.type === FilterType.TEXTAREA"
v-model:model-value="item.value"
class="w-full"
allow-clear
:disabled="!item.dataIndex"
:auto-size="{
minRows: 1,
maxRows: 1,
}"
:max-length="1000"
/>
<a-radio-group v-else-if="item.type === FilterType.RADIO" v-model:model-value="item.value">
<a-radio
v-for="it of item.radioProps?.options || []"
:key="it[item.radioProps?.valueKey || 'value']"
:value="it[item.radioProps?.valueKey || 'value']"
>{{ it[item.radioProps?.labelKey || 'label'] }}</a-radio
>
</a-radio-group>
<a-checkbox-group v-else-if="item.type === FilterType.CHECKBOX" v-model:model-value="item.value">
<a-checkbox
v-for="it of item.checkProps?.options || []"
:key="it[item.checkProps?.valueKey || 'value']"
:value="it[item.checkProps?.valueKey || 'value']"
>{{ it[item.checkProps?.labelKey || 'label'] }}</a-checkbox
>
</a-checkbox-group>
</a-form-item>
</div>
<div
v-if="formModel.list.length > 1"
class="delete-btn"
:class="{ 'delete-btn:disabled': idx === 0 }"
@click="handleDeleteItem(idx)"
>
<icon-minus-circle />
</div>
</section>
</article>
</a-scrollbar>
<footer
class="mt-[12px] flex flex-row items-center justify-between"
:class="{ '!justify-end': !showAddCondition }"
>
<div
v-if="showAddCondition"
class="flex cursor-pointer items-center gap-[4px] text-[rgb(var(--primary-7))]"
@click="handleAddItem"
>
<icon-plus />
<span>{{ t('advanceFilter.addCondition') }}</span>
</div>
<div>
<a-button class="mr-[8px]" @click="handleReset">{{ t('advanceFilter.reset') }}</a-button>
<a-button type="primary" @click="handleFilter">{{ t('advanceFilter.filter') }}</a-button>
</div>
</footer>
</div>
</a-form>
</template>
<script lang="ts" setup>
import { FormInstance } from '@arco-design/web-vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsCascader from '@/components/business/ms-cascader/index.vue';
import MsSelect from '@/components/business/ms-select';
import { useI18n } from '@/hooks/useI18n';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import { isMutipleOperator, OPERATOR_MAP } from './index';
import { AccordBelowType, BackEndEnum, CombineItem, FilterFormItem, FilterResult, FilterType } from './type';
const { t } = useI18n();
const accordBelow = ref<AccordBelowType>('AND');
const formRef = ref<FormInstance | null>(null);
const formModel = reactive<{ list: FilterFormItem[] }>({
list: [],
});
const props = defineProps<{
configList: FilterFormItem[]; //
customList?: FilterFormItem[]; //
visible: boolean;
count: number;
rowCount: number;
}>();
const emit = defineEmits<{
(e: 'onSearch', value: FilterResult): void;
(e: 'dataIndexChange', value: string): void;
(e: 'update:count', value: number): void; // FilterIcon
(e: 'update:rowCount', value: number): void; // MsBaseTable
(e: 'reset'): void;
}>();
const isMultipleSelect = (dataIndex: string) => {
const tmpObj = [...props.configList, ...(props.customList || [])].find((item) => item.dataIndex === dataIndex);
if (tmpObj) {
return tmpObj.selectProps?.multiple || tmpObj.type === FilterType.TAGS_INPUT;
}
return false;
};
const getOperationOption = (type: FilterType, dataIndex: string) => {
let result: { label: string; value: string }[] = [];
switch (type) {
case FilterType.NUMBER:
result = OPERATOR_MAP.number;
break;
case FilterType.DATE_PICKER:
result = OPERATOR_MAP.date;
break;
case FilterType.SELECT:
result = isMultipleSelect(dataIndex) ? OPERATOR_MAP.array : OPERATOR_MAP.string;
break;
default:
result = OPERATOR_MAP.string;
}
return result;
};
//
const getCurrentOptionArr = () => {
const arr1 = [...props.configList, ...(props?.customList || [])];
const arr2 = formModel.list.map((item) => item.dataIndex);
const intersection = arr1.map((item1) => ({
...item1,
disabled: arr2.includes(item1.dataIndex),
}));
return intersection;
};
const currentOptionArr = computed(() => getCurrentOptionArr());
//
const showAddCondition = computed(() => {
return currentOptionArr.value.some((item) => !item.disabled);
});
const getInitItem = () => {
return {
dataIndex: '',
type: FilterType.INPUT,
operator: '',
value: '',
backendType: BackEndEnum.STRING,
};
};
/**
* @description 添加条件
*/
const handleAddItem = async () => {
formRef.value?.validate((errors) => {
if (!errors) {
formModel.list.push(getInitItem());
}
});
};
/**
* @description 删除条件
*/
const handleDeleteItem = (index: number) => {
// if (index === 0) {
// return;
// }
if (formModel.list.length === 1) {
return;
}
formModel.list.splice(index, 1);
};
/**
* @description 重置
*/
const handleReset = () => {
formRef.value?.resetFields();
formModel.list = [getInitItem()];
emit('reset');
};
/**
* @description 筛选
*/
const handleFilter = () => {
formRef.value?.validate((errors) => {
if (!errors) {
const tmpObj: FilterResult = { accordBelow: 'AND', combine: {} };
const combine: CombineItem = {};
formModel.list.forEach((item) => {
combine[item.dataIndex as string] = {
operator: item.operator,
value: item.value,
backendType: Array.isArray(item.value) ? BackEndEnum.ARRAY : item.backendType,
};
});
tmpObj.accordBelow = accordBelow.value;
tmpObj.combine = combine;
emit('onSearch', tmpObj);
}
});
};
const getAttributeByDataIndex = (dataIndex: string) => {
return [...props.configList, ...(props.customList || [])].find((item) => item.dataIndex === dataIndex);
};
/**
* @description 筛选项变化
*/
const dataIndexChange = (dataIndex: SelectValue, idx: number) => {
if (!dataIndex) {
return;
}
const tmpObj = getAttributeByDataIndex(dataIndex as string);
if (!tmpObj) {
return;
}
formModel.list[idx] = { ...tmpObj };
formModel.list[idx].value = isMultipleSelect(dataIndex as string) ? [] : '';
emit('dataIndexChange', dataIndex as string);
};
const operationChange = (v: SelectValue, dataIndex: string, idx: number) => {
if (isMutipleOperator(v as string)) {
formModel.list[idx].value = [];
} else {
formModel.list[idx].value = isMultipleSelect(dataIndex) ? [] : '';
}
};
onBeforeMount(() => {
formModel.list = [getInitItem()];
});
watch(
() => props.visible,
(val) => {
if (!val) {
emit('update:count', formModel.list.filter((item) => !!item.dataIndex).length);
}
}
);
</script>
<style lang="less" scoped>
.delete-btn {
padding: 8px;
width: 32px;
height: 32px;
border-radius: 4px;
color: var(--color-text-4);
cursor: pointer;
}
.delete-btn:hover {
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-9));
}
.delete-btn:disabled {
@apply cursor-not-allowed hover:bg-transparent hover:text-[var(--color-text-4)];
}
:deep(.arco-form-item-layout-vertical > .arco-form-item-label-col) {
margin-bottom: 0;
}
:deep(.arco-form-item.arco-form-item-error, .arco-form-item.arco-form-item-has-help) {
position: relative;
top: 10px;
}
:deep(.arco-scrollbar-track-direction-vertical .arco-scrollbar-thumb-bar) {
margin-left: 8px;
}
</style>

View File

@ -0,0 +1,315 @@
<template>
<MsDrawer v-model:visible="visible" :width="800">
<template #title>
<a-input
v-show="isShowNameInput"
ref="nameInputRef"
v-model:model-value="formModel.name"
class="flex-1"
:max-length="255"
show-word-limit
@blur="isShowNameInput = false"
/>
<div v-show="!isShowNameInput" class="flex flex-1 items-center gap-[8px] overflow-hidden">
<a-tooltip :content="formModel.name">
<div class="one-line-text"> {{ formModel.name }}</div>
</a-tooltip>
<MsIcon
type="icon-icon_edit_outlined"
class="min-w-[16px] cursor-pointer hover:text-[rgb(var(--primary-5))]"
@click="showNameInput"
/>
</div>
</template>
<a-form ref="formRef" :model="formModel" layout="vertical">
<a-select v-model="formModel.andOrType" :options="andOrTypeOptions" class="w-[170px]">
<template #prefix> {{ t('advanceFilter.meetTheFollowingConditions') }} </template>
</a-select>
<div
v-for="(item, listIndex) in formModel.list"
:key="item.dataIndex || `filter_item_${listIndex}`"
class="flex items-center gap-[8px]"
>
<a-form-item class="flex-1 overflow-hidden" :field="`list[${listIndex}].dataIndex`" hide-asterisk>
<a-select
v-model="item.dataIndex"
allow-search
@change="(val: SelectValue) => dataIndexChange(val, listIndex)"
>
<div
v-for="(option, currentOptionsIndex) in currentOptions(item.dataIndex as string)"
:key="option.dataIndex"
>
<a-option :value="option.dataIndex">
{{ t(option.title as string) }}
</a-option>
<a-divider
v-if="(props?.customList || [])?.length && (props.configList || []).length - 1 === currentOptionsIndex"
class="!my-1"
/>
</div>
</a-select>
</a-form-item>
<a-form-item :field="`list[${listIndex}].operator`" class="w-[120px]" hide-asterisk>
<a-select v-model="item.operator" :disabled="!item.dataIndex" @change="operatorChange(item, listIndex)">
<a-option v-for="option in operatorOptionsMap[item.type]" :key="option.value" :value="option.value">
{{ t(option.label as string) }}
</a-option>
</a-select>
</a-form-item>
<a-form-item class="flex-1 overflow-hidden" :field="`list[${listIndex}].value`" hide-asterisk>
<a-input
v-if="item.type === FilterType.INPUT"
v-model:model-value="item.value"
allow-clear
:disabled="isValueDisabled(item)"
:max-length="255"
:placeholder="t('advanceFilter.inputPlaceholder')"
/>
<a-textarea
v-else-if="item.type === FilterType.TEXTAREA"
v-model:model-value="item.value"
allow-clear
:disabled="isValueDisabled(item)"
:auto-size="{
minRows: 1,
maxRows: 1,
}"
:placeholder="t('advanceFilter.inputPlaceholder')"
:max-length="1000"
/>
<MsTagsInput
v-else-if="item.type === FilterType.TAGS_INPUT"
v-model:model-value="item.value"
:disabled="isValueDisabled(item)"
allow-clear
unique-value
retain-input-value
/>
<a-input-number
v-else-if="item.type === FilterType.NUMBER"
v-model:model-value="item.value"
allow-clear
:disabled="isValueDisabled(item)"
:max-length="255"
:placeholder="t('common.pleaseInput')"
/>
<MsSelect
v-else-if="item.type === FilterType.SELECT"
v-model:model-value="item.value"
allow-clear
allow-search
:placeholder="t('common.pleaseSelect')"
:disabled="isValueDisabled(item)"
:options="item.selectProps?.options || []"
v-bind="item.selectProps"
/>
<a-tree-select
v-else-if="item.type === FilterType.TREE_SELECT"
v-model:model-value="item.value"
:data="item.treeSelectData"
:disabled="isValueDisabled(item)"
v-bind="item.treeSelectProps"
/>
<a-date-picker
v-else-if="item.type === FilterType.DATE_PICKER && item.operator !== 'between'"
v-model:model-value="item.value"
show-time
format="YYYY-MM-DD hh:mm"
:disabled="isValueDisabled(item)"
/>
<a-range-picker
v-else-if="item.type === FilterType.DATE_PICKER && item.operator === 'between'"
v-model:model-value="item.value"
show-time
format="YYYY-MM-DD HH:mm"
:separator="t('common.to')"
:disabled="isValueDisabled(item)"
/>
<a-radio-group
v-else-if="item.type === FilterType.RADIO"
v-model:model-value="item.value"
:disabled="isValueDisabled(item)"
>
<a-radio
v-for="it of item.radioProps?.options || []"
:key="it[item.radioProps?.valueKey || 'value']"
:value="it[item.radioProps?.valueKey || 'value']"
>
{{ it[item.radioProps?.labelKey || 'label'] }}
</a-radio>
</a-radio-group>
<a-checkbox-group
v-else-if="item.type === FilterType.CHECKBOX"
v-model:model-value="item.value"
:disabled="isValueDisabled(item)"
>
<a-checkbox
v-for="it of item.checkProps?.options || []"
:key="it[item.checkProps?.valueKey || 'value']"
:value="it[item.checkProps?.valueKey || 'value']"
>
{{ it[item.checkProps?.labelKey || 'label'] }}
</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-button
v-if="formModel.list.length > 1"
type="outline"
class="arco-btn-outline--secondary"
@click="handleDeleteItem(listIndex)"
>
<template #icon> <MsIcon type="icon-icon_block_outlined" class="text-[var(--color-text-4)]" /> </template>
</a-button>
</div>
</a-form>
<MsButton type="text" class="mt-[5px]" @click="handleAddItem">
<MsIcon type="icon-icon_add_outlined" class="mr-[3px]" />
{{ t('advanceFilter.addCondition') }}
</MsButton>
<template #footer>
<div v-show="!isSaveAsView" class="flex items-center gap-[8px]">
<a-button type="primary" @click="handleFilter">{{ t('common.filter') }}</a-button>
<a-button class="mr-[16px]">{{ t('common.reset') }}</a-button>
<MsButton type="text" class="!text-[var(--color-text-1)]"> {{ t('common.save') }}</MsButton>
<MsButton type="text" class="!text-[var(--color-text-1)]" @click="isSaveAsView = true">
{{ t('advanceFilter.saveAsView') }}
</MsButton>
</div>
<div v-show="isSaveAsView" class="flex items-center gap-[8px]">
<a-input
v-model:model-value="saveAsViewName"
:placeholder="t('advanceFilter.viewNamePlaceholder')"
class="w-[240px]"
:max-length="255"
show-word-limit
/>
<a-button type="primary">{{ t('common.save') }}</a-button>
<a-button @click="handleCancelSaveAsView">{{ t('common.cancel') }}</a-button>
</div>
</template>
</MsDrawer>
</template>
<script lang="ts" setup>
import { FormInstance, InputInstance } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsSelect from '@/components/business/ms-select';
import { useI18n } from '@/hooks/useI18n';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import { defaultFormModelList, operatorOptionsMap } from './index';
import { AccordBelowType, BackEndEnum, FilterFormItem, FilterResult, FilterType } from './type';
const props = defineProps<{
configList: FilterFormItem[]; //
customList?: FilterFormItem[]; //
}>();
const emit = defineEmits<{
(e: 'handleFilter', value: FilterResult): void;
}>();
const visible = defineModel<boolean>('visible', { required: true });
const { t } = useI18n();
// TODO lmy
const formModel = ref<{ name: string; andOrType: AccordBelowType; list: FilterFormItem[] }>({
name: '111',
andOrType: 'AND',
list: [...defaultFormModelList],
});
const isShowNameInput = ref(false);
const nameInputRef = ref<InputInstance>();
function showNameInput() {
isShowNameInput.value = true;
nextTick(() => {
nameInputRef.value?.focus();
});
}
const andOrTypeOptions = [
{ value: 'AND', label: t('advanceFilter.and') },
{ value: 'OR', label: t('advanceFilter.or') },
];
function getListItemByDataIndex(dataIndex: string) {
return [...props.configList, ...(props.customList || [])].find((item) => item.dataIndex === dataIndex);
}
//
function valueIsArray(listItem: FilterFormItem) {
return (
listItem.selectProps?.multiple ||
[FilterType.CHECKBOX, FilterType.TAGS_INPUT].includes(listItem.type) ||
(listItem.type === FilterType.DATE_PICKER && listItem.operator === 'between')
);
}
//
const currentOptions = computed(() => {
return (currentDataIndex: string) => {
const otherDataIndices = formModel.value.list
.filter((listItem) => listItem.dataIndex !== currentDataIndex)
.map((item: FilterFormItem) => item.dataIndex);
return [...props.configList, ...(props.customList || [])]
.filter(({ dataIndex }) => !otherDataIndices.includes(dataIndex))
.map((item) => ({ ...item, label: t(item.title as string) }));
};
});
//
function dataIndexChange(dataIndex: SelectValue, index: number) {
const listItem = getListItemByDataIndex(dataIndex as string);
if (!listItem) return;
formModel.value.list[index] = { ...listItem };
formModel.value.list[index].value = valueIsArray(listItem) ? [] : '';
}
//
function operatorChange(item: FilterFormItem, index: number) {
formModel.value.list[index].value = valueIsArray(item) ? [] : '';
}
function isValueDisabled(item: FilterFormItem) {
return !item.dataIndex || ['EMPTY', 'NOT_EMPTY'].includes(item.operator as string);
}
function handleDeleteItem(index: number) {
if (formModel.value.list.length === 1) return;
formModel.value.list.splice(index, 1);
}
function handleAddItem() {
const item = {
dataIndex: '',
type: FilterType.INPUT,
operator: '',
value: '',
backendType: BackEndEnum.STRING,
};
formModel.value.list.push(item);
}
const formRef = ref<FormInstance>();
function handleFilter() {
formRef.value?.validate((errors) => {
if (!errors) {
// TODO lmy
emit('handleFilter', { accordBelow: 'AND', combine: {} });
}
});
}
const isSaveAsView = ref(false);
const saveAsViewName = ref('');
function handleCancelSaveAsView() {
isSaveAsView.value = false;
saveAsViewName.value = '';
}
</script>
<style lang="less" scoped>
:deep(.arco-form-item) {
margin-bottom: 8px;
}
</style>

View File

@ -1,35 +1,43 @@
export { default as FilterForm } from './FilterForm.vue';
import { BackEndEnum, FilterType } from './type';
export { default as MsAdvanceFilter } from './index.vue';
// const IN = { label: 'advanceFilter.operator.in', value: 'in' };
// const NOT_IN = { label: 'advanceFilter.operator.not_in', value: 'not_in' };
export const LIKE = { label: 'advanceFilter.operator.like', value: 'like' };
export const NOT_LIKE = { label: 'advanceFilter.operator.not_like', value: 'not_like' };
export const GT = { label: 'advanceFilter.operator.gt', value: 'GT' };
export const GE = { label: 'advanceFilter.operator.ge', value: 'GT_OR_EQUALS' };
export const LT = { label: 'advanceFilter.operator.lt', value: 'LT' };
export const LE = { label: 'advanceFilter.operator.le', value: 'LT_OR_EQUALS' };
export const EQUAL = { label: 'advanceFilter.operator.equal', value: 'EQUALS' };
export const NOT_EQUAL = { label: 'advanceFilter.operator.notEqual', value: 'NOT_EQUALS' };
export const BETWEEN = { label: 'advanceFilter.operator.between', value: 'between' };
export const NO_CHECK = { label: 'advanceFilter.operator.no_check', value: 'UNCHECK' };
export const CONTAINS = { label: 'advanceFilter.operator.contains', value: 'CONTAINS' };
export const NO_CONTAINS = { label: 'advanceFilter.operator.not_contains', value: 'NOT_CONTAINS' };
export const START_WITH = { label: 'advanceFilter.operator.start_with', value: 'START_WITH' };
export const END_WITH = { label: 'advanceFilter.operator.end_with', value: 'END_WITH' };
export const EMPTY = { label: 'advanceFilter.operator.empty', value: 'EMPTY' };
export const NOT_EMPTY = { label: 'advanceFilter.operator.not_empty', value: 'NOT_EMPTY' };
export const REGEX = { label: 'advanceFilter.operator.regexp', value: 'REGEX' };
export const LENGTH_EQUAL = { label: 'advanceFilter.operator.length.equal', value: 'LENGTH_EQUALS' };
export const LENGTH_GT = { label: 'advanceFilter.operator.length.gt', value: 'LENGTH_GT' };
export const LENGTH_GE = { label: 'advanceFilter.operator.length.ge', value: 'LENGTH_GT_OR_EQUALS' };
export const LENGTH_LT = { label: 'advanceFilter.operator.length.lt', value: 'LENGTH_LT' };
export const LENGTH_LE = { label: 'advanceFilter.operator.length.le', value: 'LENGTH_LT_OR_EQUALS' };
export const OPERATOR_MAP = {
string: [LIKE, NOT_LIKE, EQUAL, NOT_EQUAL],
number: [GT, GE, LT, LE, EQUAL, NOT_EQUAL, BETWEEN],
date: [GT, GE, LT, LE, EQUAL, NOT_EQUAL, BETWEEN],
array: [BETWEEN],
export const LIKE = { label: 'advanceFilter.operator.contains', value: 'like' }; // 包含
export const NOT_LIKE = { label: 'advanceFilter.operator.not_contains', value: 'not_like' }; // 不包含
export const GT = { label: 'advanceFilter.operator.gt', value: 'GT' }; // 大于
export const GE = { label: 'advanceFilter.operator.ge', value: 'GT_OR_EQUALS' }; // 大于等于
export const LT = { label: 'advanceFilter.operator.lt', value: 'LT' }; // 小于
export const LE = { label: 'advanceFilter.operator.le', value: 'LT_OR_EQUALS' }; // 小于等于
export const EQUAL = { label: 'advanceFilter.operator.equal', value: 'EQUALS' }; // 等于
export const NOT_EQUAL = { label: 'advanceFilter.operator.notEqual', value: 'NOT_EQUALS' }; // 不等于
export const BETWEEN = { label: 'advanceFilter.operator.between', value: 'between' }; // 介于
export const NO_CHECK = { label: 'advanceFilter.operator.no_check', value: 'UNCHECK' }; // 不校验
export const CONTAINS = { label: 'advanceFilter.operator.contains', value: 'CONTAINS' }; // 包含
export const NO_CONTAINS = { label: 'advanceFilter.operator.not_contains', value: 'NOT_CONTAINS' }; // 不包含
export const START_WITH = { label: 'advanceFilter.operator.start_with', value: 'START_WITH' }; // 以...开始
export const END_WITH = { label: 'advanceFilter.operator.end_with', value: 'END_WITH' }; // 以...结束
export const EMPTY = { label: 'advanceFilter.operator.empty', value: 'EMPTY' }; // 为空
export const NOT_EMPTY = { label: 'advanceFilter.operator.not_empty', value: 'NOT_EMPTY' }; // 不为空
export const REGEX = { label: 'advanceFilter.operator.regexp', value: 'REGEX' }; // 正则匹配
export const LENGTH_EQUAL = { label: 'advanceFilter.operator.length.equal', value: 'LENGTH_EQUALS' }; // 长度等于
export const LENGTH_GT = { label: 'advanceFilter.operator.length.gt', value: 'LENGTH_GT' }; // 长度大于
export const LENGTH_GE = { label: 'advanceFilter.operator.length.ge', value: 'LENGTH_GT_OR_EQUALS' }; // 长度大于等于
export const LENGTH_LT = { label: 'advanceFilter.operator.length.lt', value: 'LENGTH_LT' }; // 长度小于
export const LENGTH_LE = { label: 'advanceFilter.operator.length.le', value: 'LENGTH_LT_OR_EQUALS' }; // 长度小于等于
const COMMON_TEXT_OPERATORS = [LIKE, NOT_LIKE, EMPTY, NOT_EMPTY, EQUAL, NOT_EQUAL];
const COMMON_SELECTION_OPERATORS = [LIKE, NOT_LIKE, EMPTY, NOT_EMPTY];
export const operatorOptionsMap: Record<string, { value: string; label: string }[]> = {
[FilterType.INPUT]: COMMON_TEXT_OPERATORS,
[FilterType.TEXTAREA]: COMMON_TEXT_OPERATORS,
[FilterType.NUMBER]: [GT, LT, EQUAL, EMPTY, NOT_EMPTY],
[FilterType.RADIO]: COMMON_SELECTION_OPERATORS,
[FilterType.CHECKBOX]: COMMON_SELECTION_OPERATORS,
[FilterType.SELECT]: COMMON_SELECTION_OPERATORS,
[FilterType.TAGS_INPUT]: [EMPTY, LIKE, NOT_LIKE, LENGTH_LT, LENGTH_GT],
[FilterType.TREE_SELECT]: [LIKE, NOT_LIKE],
[FilterType.DATE_PICKER]: [BETWEEN, EQUAL, EMPTY, NOT_EMPTY],
};
export const timeSelectOptions = [GE, LE];
@ -155,8 +163,29 @@ export const CustomTypeMaps: Record<string, any> = {
},
};
export const MULTIPLE_OPERATOR_LIST = ['between'];
export function isMutipleOperator(operator: string) {
return MULTIPLE_OPERATOR_LIST.includes(operator);
}
export const defaultFormModelList = [
{
dataIndex: 'id',
title: 'caseManagement.featureCase.tableColumnID',
type: FilterType.INPUT,
operator: '',
value: '',
backendType: BackEndEnum.STRING,
},
{
dataIndex: 'name',
label: 'common.name',
type: FilterType.INPUT,
operator: '',
value: '',
backendType: BackEndEnum.STRING,
},
{
dataIndex: 'moduleId',
label: 'common.belongModule',
type: FilterType.TREE_SELECT,
operator: '',
value: '',
backendType: BackEndEnum.STRING,
},
];

View File

@ -31,22 +31,20 @@
@search="emit('keywordSearch', keyword, filterResult)"
@clear="handleClear"
></a-input-search>
<!-- <MsTag
:type="visible ? 'primary' : 'default'"
:theme="visible ? 'lightOutLine' : 'outline'"
size="large"
class="min-w-[64px] cursor-pointer"
no-margin
<a-button
v-if="props.showFilter"
type="outline"
:class="`${visible ? '' : 'arco-btn-outline--secondary'} p-[0_8px]`"
@click="handleOpenFilter"
>
<span :class="!visible ? 'text-[var(--color-text-4)]' : ''">
<icon-filter class="text-[16px]" />
<span class="ml-[4px]">
<span v-if="filterCount">{{ filterCount }}</span>
{{ t('common.filter') }}
</span>
</span>
</MsTag> -->
<template #icon>
<MsIcon
type="icon-icon_copy_outlined"
:class="`${visible ? 'text-[rgb(var(--primary-5))]' : 'text-[var(--color-text-4)]'}`"
/>
</template>
{{ t('common.filter') }}
</a-button>
<slot name="right"></slot>
<MsTag
@ -61,24 +59,20 @@
</MsTag>
</div>
</div>
<FilterForm
v-show="visible"
v-model:count="filterCount"
:row-count="props.rowCount"
:visible="visible"
<FilterDrawer
v-model:visible="visible"
:config-list="props.filterConfigList"
:custom-list="props.customFieldsConfigList"
class="mt-[8px]"
@on-search="handleFilter"
@data-index-change="dataIndexChange"
@reset="handleResetFilter"
@handle-filter="handleFilter"
/>
</template>
<script setup lang="ts">
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag from '../ms-tag/ms-tag.vue';
import FilterForm from './FilterForm.vue';
import FilterDrawer from './filterDrawer.vue';
import { useI18n } from '@/hooks/useI18n';
import { FilterFormItem, FilterResult } from './type';
@ -90,6 +84,7 @@
name?: string;
count?: number;
notShowInputSearch?: boolean;
showFilter?: boolean; //
}>();
const emit = defineEmits<{
@ -99,17 +94,13 @@
(e: 'refresh', value: FilterResult): void;
}>();
const { t } = useI18n();
const keyword = defineModel<string>('keyword', { default: '' });
const visible = ref(false);
const filterCount = ref(0);
const defaultFilterResult: FilterResult = { accordBelow: 'AND', combine: {} };
const filterResult = ref<FilterResult>({ ...defaultFilterResult });
const handleResetFilter = () => {
filterResult.value = { ...defaultFilterResult };
emit('advSearch', { ...defaultFilterResult });
};
const handleFilter = (filter: FilterResult) => {
filterResult.value = filter;
emit('advSearch', filter);
@ -119,10 +110,6 @@
emit('refresh', filterResult.value);
};
const dataIndexChange = (dataIndex: string) => {
emit('dataIndexChange', dataIndex);
};
const handleClear = () => {
keyword.value = '';
emit('keywordSearch', '', filterResult.value);

View File

@ -1,8 +1,4 @@
export default {
'advanceFilter.operator.like': 'like',
'advanceFilter.operator.not_like': 'not like',
'advanceFilter.operator.in': 'in',
'advanceFilter.operator.not_in': 'not in',
'advanceFilter.operator.gt': 'gt',
'advanceFilter.operator.ge': 'ge',
'advanceFilter.operator.lt': 'lt',
@ -18,21 +14,16 @@ export default {
'advanceFilter.operator.not_empty': 'not empty',
'advanceFilter.operator.regexp': 'Regular',
'advanceFilter.operator.between': 'between',
'advanceFilter.setFilterCondition': 'Set Filter Condition',
'advanceFilter.accordBelow': 'Accord below',
'advanceFilter.all': 'All',
'advanceFilter.any': 'Any',
'advanceFilter.condition': 'Condition',
'advanceFilter.addCondition': 'Add Condition',
'advanceFilter.reset': 'Reset',
'advanceFilter.filter': 'Filter',
'advanceFilter.plaseSelectFilterDataIndex': 'Please select a filter conditions',
'advanceFilter.plaseInputFilterContent': 'Please input filter content',
'advanceFilter.plaseSelectOperator': 'Please select an operator',
'advanceFilter.operator.length.equal': 'Length equal to',
'advanceFilter.operator.length.not_equal': 'Not equal in length',
'advanceFilter.operator.length.gt': 'Length greater than',
'advanceFilter.operator.length.ge': 'Length greater than or equal to',
'advanceFilter.operator.length.lt': 'Less than',
'advanceFilter.operator.length.le': 'Length less than or equal to',
'advanceFilter.saveAsView': 'Save as view',
'advanceFilter.viewNamePlaceholder': 'Please enter the view name',
'advanceFilter.meetTheFollowingConditions': 'Meet the following conditions',
'advanceFilter.and': 'And',
'advanceFilter.or': 'Or',
'advanceFilter.inputPlaceholder': 'Separate keywords with spaces',
'advanceFilter.addCondition': 'Add conditions',
};

View File

@ -1,15 +1,11 @@
export default {
'advanceFilter.operator.like': '包含',
'advanceFilter.operator.not_like': '不包含',
'advanceFilter.operator.in': '在列表中',
'advanceFilter.operator.not_in': '不在列表中',
'advanceFilter.operator.gt': '大于',
'advanceFilter.operator.ge': '大于等于',
'advanceFilter.operator.lt': '小于',
'advanceFilter.operator.le': '小于等于',
'advanceFilter.operator.equal': '等于',
'advanceFilter.operator.notEqual': '不等于',
'advanceFilter.operator.between': '介于',
'advanceFilter.operator.between': '区间',
'advanceFilter.operator.no_check': '不校验',
'advanceFilter.operator.contains': '包含',
'advanceFilter.operator.not_contains': '不包含',
@ -18,21 +14,17 @@ export default {
'advanceFilter.operator.empty': '为空',
'advanceFilter.operator.not_empty': '不为空',
'advanceFilter.operator.regexp': '正则匹配',
'advanceFilter.setFilterCondition': '设置筛选条件',
'advanceFilter.accordBelow': '满足以下',
'advanceFilter.all': '所有',
'advanceFilter.any': '任意',
'advanceFilter.condition': '条件',
'advanceFilter.addCondition': '添加条件',
'advanceFilter.reset': '重置',
'advanceFilter.filter': '过滤',
'advanceFilter.plaseSelectFilterDataIndex': '请选择过滤条件',
'advanceFilter.plaseInputFilterContent': '请输入筛选内容',
'advanceFilter.plaseSelectOperator': '请选择运算符',
'advanceFilter.operator.length.equal': '长度等于',
'advanceFilter.operator.length.not_equal': '长度不等于',
'advanceFilter.operator.length.gt': '长度大于',
'advanceFilter.operator.length.ge': '长度大于等于',
'advanceFilter.operator.length.lt': '长度小于',
'advanceFilter.operator.length.le': '长度小于等于',
'advanceFilter.saveAsView': '另存为视图',
'advanceFilter.viewNamePlaceholder': '请输入视图名称',
'advanceFilter.meetTheFollowingConditions': '符合以下条件',
'advanceFilter.and': '所有',
'advanceFilter.or': '任一',
'advanceFilter.inputPlaceholder': '关键字之间以空格进行分隔',
'advanceFilter.addCondition': '添加条件',
};

View File

@ -38,21 +38,21 @@ export enum FilterType {
NUMBER = 'Number',
SELECT = 'Select',
DATE_PICKER = 'DatePicker',
CASCADER = 'Cascader',
TAGS_INPUT = 'TagsInput',
TREE_SELECT = 'TreeSelect',
TEXTAREA = 'textArea',
RADIO = 'radio',
CHECKBOX = 'checkbox',
CASCADER = 'Cascader',
JIRAKEY = 'JIRAKEY',
}
export interface FilterFormItem {
dataIndex?: string; // 对应的row的数据key
title?: string; // 显示的label 国际化字符串定义在前端
type: FilterType; // 类型Input,Select,DatePicker,RangePicker
value?: any; // 值 字符串 和 数组
operator?: string; // 运算符号
dataIndex?: string; // 第一列下拉的value
title?: string; // 第一列下拉显示的label
operator?: string; // 第二列的值
type: FilterType; // 类型:判断第二列下拉数据和第三列显示形式
value?: any; // 第三列的值
cascaderOptions?: CascaderOption[]; // 级联选择的选项
backendType?: BackEndEnum; // 后端类型 string array time
selectProps?: Partial<MsSearchSelectProps>; // select的props, 参考 MsSelect

View File

@ -5,6 +5,7 @@
<template v-if="showType === 'list'">
<MsAdvanceFilter
v-model:keyword="keyword"
show-filter
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:search-placeholder="t('caseManagement.featureCase.searchPlaceholder')"
@ -976,7 +977,6 @@
selectAll: batchParams.value.selectAll,
selectIds: batchParams.value.selectedIds || [],
keyword: keyword.value,
combine: batchParams.value.condition,
};
}
//
@ -1604,24 +1604,10 @@
console.log(error);
}
}
const filterResult = ref<FilterResult>({ accordBelow: 'AND', combine: {} });
//
const currentSelectParams = ref<BatchActionQueryParams>({ selectAll: false, currentSelectCount: 0 });
//
const handleAdvSearch = (filter: FilterResult) => {
filterResult.value = filter;
const { accordBelow, combine } = filter;
const handleAdvSearch = async (filter: FilterResult) => {
setAdvanceFilter(filter);
currentSelectParams.value = {
...currentSelectParams.value,
condition: {
keyword: keyword.value,
searchMode: accordBelow,
filter: propsRes.value.filter,
combine,
},
};
initData();
loadList();
};
//
async function handleStatusChange(record: any) {