fix: improve filters
This commit is contained in:
		
							parent
							
								
									e692d7bb5f
								
							
						
					
					
						commit
						519f8de40b
					
				| @ -157,14 +157,14 @@ const OP_MAP = { | ||||
|   select: [ | ||||
|     {label: '等于', value: 'eq', selected: true}, | ||||
|     {label: '不等于', value: 'ne'}, | ||||
|     {label: '包含', value: '$anyOf'}, | ||||
|     {label: '不包含', value: '$noneOf'}, | ||||
|     {label: '包含', value: 'in'}, | ||||
|     {label: '不包含', value: 'notIn'}, | ||||
|     {label: '非空', value: '$notNull'}, | ||||
|     {label: '为空', value: '$null'}, | ||||
|   ], | ||||
|   multipleSelect: [ | ||||
|     {label: '等于', value: 'eq', selected: true}, | ||||
|     {label: '不等于', value: 'ne'}, | ||||
|     {label: '等于', value: '$match', selected: true}, | ||||
|     {label: '不等于', value: '$notMatch'}, | ||||
|     {label: '包含', value: '$anyOf'}, | ||||
|     {label: '不包含', value: '$noneOf'}, | ||||
|     {label: '非空', value: '$notNull'}, | ||||
| @ -217,8 +217,8 @@ const op = { | ||||
|   checkbox: OP_MAP.boolean, | ||||
|   boolean: OP_MAP.boolean, | ||||
|   select: OP_MAP.select, | ||||
|   multipleSelect: OP_MAP.select, | ||||
|   checkboxes: OP_MAP.select, | ||||
|   multipleSelect: OP_MAP.multipleSelect, | ||||
|   checkboxes: OP_MAP.multipleSelect, | ||||
|   radio: OP_MAP.select, | ||||
|   upload: OP_MAP.file, | ||||
|   attachment: OP_MAP.file, | ||||
| @ -237,7 +237,13 @@ const controls = { | ||||
|   string: StringInput, | ||||
|   textarea: StringInput, | ||||
|   number: InputNumber, | ||||
|   percent: InputNumber, | ||||
|   percent: (props) => ( | ||||
|     <InputNumber  | ||||
|       formatter={value => value ? `${value}%` : ''} | ||||
|       parser={value => value.replace('%', '')} | ||||
|       {...props} | ||||
|     /> | ||||
|   ), | ||||
|   boolean: BooleanControl, | ||||
|   checkbox: BooleanControl, | ||||
|   select: OptionControl, | ||||
| @ -302,13 +308,13 @@ export function FilterItem(props: FilterItemProps) { | ||||
|     const field = fields.find(field => field.name === props.dataSource.column); | ||||
|     if (field) { | ||||
|       setField(field); | ||||
|       setType(field.component.type); | ||||
|       // console.log(dataSource);
 | ||||
|       let componentType = field.component.type; | ||||
|       if (field.component.type === 'select' && field.multiple) { | ||||
|         componentType = 'multipleSelect'; | ||||
|       } | ||||
|       setType(componentType); | ||||
|     } | ||||
|     setDataSource({...props.dataSource}); | ||||
|     // if (['boolean', 'checkbox'].indexOf(type) !== -1) {
 | ||||
|     //   onChange({...dataSource, op: undefined});
 | ||||
|     // }
 | ||||
|   }, [ | ||||
|     props.dataSource, type, | ||||
|   ]); | ||||
| @ -328,10 +334,12 @@ export function FilterItem(props: FilterItemProps) { | ||||
|       <Select value={dataSource.column} | ||||
|         onChange={(value) => { | ||||
|           const field = fields.find(field => field.name === value); | ||||
|           if (field) { | ||||
|             setType(field.component.type); | ||||
|           let componentType = field.component.type; | ||||
|           if (field.component.type === 'select' && field.multiple) { | ||||
|             componentType = 'multipleSelect'; | ||||
|           } | ||||
|           onChange({...dataSource, column: value, op: get(op, [field.component.type, 0, 'value']), value: undefined}); | ||||
|           setType(componentType); | ||||
|           onChange({...dataSource, column: value, op: get(op, [componentType, 0, 'value']), value: undefined}); | ||||
|         }} | ||||
|         style={{ width: 120 }}  | ||||
|         placeholder={'选择字段'}> | ||||
|  | ||||
| @ -284,4 +284,9 @@ export default class Database { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public getFieldByPath(fieldPath: string) { | ||||
|     const [tableName, fieldName] = fieldPath.split('.'); | ||||
|     return this.getTable(tableName).getField(fieldName); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -312,7 +312,7 @@ export class ARRAY extends Column { | ||||
|   public readonly options: Options.ArrayOptions; | ||||
| 
 | ||||
|   public getDataType() { | ||||
|     return DataTypes.JSON; | ||||
|     return DataTypes.JSONB; | ||||
|   } | ||||
| 
 | ||||
|   public getAttributeOptions() { | ||||
| @ -325,6 +325,9 @@ export class ARRAY extends Column { | ||||
| } | ||||
| 
 | ||||
| export class JSON extends Column { | ||||
|   public getDataType() { | ||||
|     return DataTypes.JSONB; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class JSONB extends Column { | ||||
|  | ||||
| @ -20,7 +20,11 @@ for (const key in Op) { | ||||
| // 通用
 | ||||
| 
 | ||||
| // 是否为空:数据库意义的 null
 | ||||
| op.set('$null', () => ({ [Op.is]: null })); | ||||
| op.set('$null', (value, {fieldPath, database}) => { | ||||
|   // const field = database.getFieldByPath(fieldPath);
 | ||||
|   // console.log({field});
 | ||||
|   return { [Op.is]: null }; | ||||
| }); | ||||
| op.set('$notNull', () => ({ [Op.not]: null })); | ||||
| 
 | ||||
| op.set('$isTruly', () => ({ | ||||
| @ -55,9 +59,20 @@ op.set('$notEndsWith', (value: string) => ({ [Op.notILike]: `%${value}` })); | ||||
| // 多选(JSON)类型
 | ||||
| 
 | ||||
| // 包含组中任意值(命名来源:`Array.prototype.some`)
 | ||||
| op.set('$anyOf', (values: any[]) => ({ | ||||
|   [Op.or]: toArray(values).map(value => ({ [Op.contains]: value })) | ||||
| })); | ||||
| op.set('$anyOf', (values: any[], options) => { | ||||
|   if (!values) { | ||||
|     return Sequelize.literal(''); | ||||
|   } | ||||
|   values = Array.isArray(values) ? values : [values]; | ||||
|   if (values.length === 0) { | ||||
|     return Sequelize.literal(''); | ||||
|   } | ||||
|   const { field, fieldPath } = options; | ||||
|   const column = fieldPath.split('.').map(name => `"${name}"`).join('.'); | ||||
|   const sql = values.map(value => `(${column})::jsonb @> '${JSON.stringify(value)}'`).join(' OR '); | ||||
|   console.log(sql); | ||||
|   return Sequelize.literal(sql); | ||||
| }); | ||||
| // 包含组中所有值
 | ||||
| op.set('$allOf', (values: any) => ({ [Op.contains]: toArray(values) })); | ||||
| // TODO(bug): 不包含组中任意值
 | ||||
| @ -66,6 +81,9 @@ op.set('$noneOf', (values: any[], options) => { | ||||
|     return Sequelize.literal(''); | ||||
|   } | ||||
|   values = Array.isArray(values) ? values : [values]; | ||||
|   if (values.length === 0) { | ||||
|     return Sequelize.literal(''); | ||||
|   } | ||||
|   const { field, fieldPath } = options; | ||||
|   const column = fieldPath.split('.').map(name => `"${name}"`).join('.'); | ||||
|   const sql = values.map(value => `(${column})::jsonb @> '${JSON.stringify(value)}'`).join(' OR '); | ||||
| @ -73,14 +91,26 @@ op.set('$noneOf', (values: any[], options) => { | ||||
|   return Sequelize.literal(`not (${sql})`); | ||||
| }); | ||||
| // 与组中值匹配
 | ||||
| op.set('$match', (values: any[]) => { | ||||
| op.set('$match', (values: any[], options) => { | ||||
|   const array = toArray(values); | ||||
|   return { | ||||
|     [Op.contains]: array, | ||||
|     [Op.contained]: array | ||||
|   }; | ||||
|   if (values.length === 0) { | ||||
|     return Sequelize.literal(''); | ||||
|   } | ||||
|   const { field, fieldPath } = options; | ||||
|   const column = fieldPath.split('.').map(name => `"${name}"`).join('.'); | ||||
|   const sql = `(${column})::jsonb @> '${JSON.stringify(array)}' AND (${column})::jsonb <@ '${JSON.stringify(array)}'` | ||||
|   return Sequelize.literal(sql); | ||||
| }); | ||||
| op.set('$notMatch', (values: any[], options) => { | ||||
|   const array = toArray(values); | ||||
|   if (values.length === 0) { | ||||
|     return Sequelize.literal(''); | ||||
|   } | ||||
|   const { field, fieldPath } = options; | ||||
|   const column = fieldPath.split('.').map(name => `"${name}"`).join('.'); | ||||
|   const sql = `(${column})::jsonb @> '${JSON.stringify(array)}' AND (${column})::jsonb <@ '${JSON.stringify(array)}'` | ||||
|   return Sequelize.literal(`not (${sql})`); | ||||
|   // return Sequelize.literal(`(not (${sql})) AND ${column} IS NULL`);
 | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| export default op; | ||||
|  | ||||
| @ -20,7 +20,7 @@ export function toWhere(options: any, context: ToWhereContext = {}) { | ||||
|   if (Array.isArray(options)) { | ||||
|     return options.map((item) => toWhere(item, context)); | ||||
|   } | ||||
|   const { prefix, model, associations = {}, ctx, dialect } = context; | ||||
|   const { prefix, model, associations = {}, ctx, dialect, database } = context; | ||||
|   const items = {}; | ||||
|   // 先处理「点号」的问题
 | ||||
|   for (const key in options) { | ||||
| @ -54,7 +54,11 @@ export function toWhere(options: any, context: ToWhereContext = {}) { | ||||
|       switch (typeof opKey) { | ||||
|         case 'function': | ||||
|           const name = model ? model.options.name.plural : ''; | ||||
|           const result = opKey(items[key], { model, fieldPath: name ? `${name}.${prefix}` : prefix }); | ||||
|           const result = opKey(items[key], {  | ||||
|             model, | ||||
|             database, | ||||
|             fieldPath: name ? `${name}.${prefix}` : prefix, | ||||
|           }); | ||||
|           if (result.constructor.name === 'Literal') { | ||||
|             values['$__literals'] = values['$__literals'] || []; | ||||
|             values['$__literals'].push(result); | ||||
| @ -184,7 +188,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) { | ||||
|   } | ||||
| 
 | ||||
|   const { fields = [], filter } = options; | ||||
|   const { model, sourceAlias, associations = {}, ctx, dialect } = context; | ||||
|   const { model, sourceAlias, associations = {}, ctx, database, dialect } = context; | ||||
| 
 | ||||
|   let where = options.where || {}; | ||||
| 
 | ||||
| @ -193,6 +197,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) { | ||||
|       model, | ||||
|       associations, | ||||
|       ctx, | ||||
|       database, | ||||
|     }) || {}; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -157,6 +157,7 @@ export const multipleSelect = { | ||||
|     type: 'json', // json 过滤
 | ||||
|     filterable: true, | ||||
|     dataSource: [], | ||||
|     defaultValue: [], | ||||
|     multiple: true, // 需要重点考虑
 | ||||
|     component: { | ||||
|       type: 'select', | ||||
| @ -184,6 +185,7 @@ export const checkboxes = { | ||||
|     type: 'json', | ||||
|     filterable: true, | ||||
|     dataSource: [], | ||||
|     defaultValue: [], | ||||
|     component: { | ||||
|       type: 'checkboxes', | ||||
|     }, | ||||
| @ -387,7 +389,7 @@ export const createdBy = { | ||||
|     interface: 'createdBy', | ||||
|     type: 'createdBy', | ||||
|     // name: 'createdBy',
 | ||||
|     filterable: true, | ||||
|     // filterable: true,
 | ||||
|     target: 'users', | ||||
|     labelField: 'nickname', | ||||
|     foreignKey: 'created_by_id', | ||||
| @ -405,7 +407,7 @@ export const updatedBy = { | ||||
|     interface: 'updatedBy', | ||||
|     // name: 'updatedBy',
 | ||||
|     type: 'updatedBy', | ||||
|     filterable: true, | ||||
|     // filterable: true,
 | ||||
|     target: 'users', | ||||
|     labelField: 'nickname', | ||||
|     foreignKey: 'created_by_id', | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user