Merge pull request #8862 from lurunze1226/feat-editor-crud-matchfunc-control

feat: CRUD组件matchFunc支持使用matchSorter函数; docs: 前端分页的使用提示
This commit is contained in:
wutong 2023-11-23 19:36:01 +08:00 committed by GitHub
commit a60d174037
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 314 additions and 255 deletions

View File

@ -1606,7 +1606,7 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在
分页有两种模式:
##### 1. 知道数据总数
**1. 知道数据总数**
如果后端可以知道数据总数时,接口返回格式如下:
@ -1630,7 +1630,7 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在
该模式下,会自动计算总页码数,渲染出有页码的分页组件
##### 2. 不知道数据总数
**2. 不知道数据总数**
如果后端无法知道数据总数,那么可以返回`hasNext`字段,来标识是否有下一页。
@ -1656,6 +1656,261 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在
> 如果总数据只够展示一页,则默认不显示该分页组件
#### 前端分页
如果你的数据并不是很大,而且后端不方便做分页和条件过滤操作,那么通过配置`loadDataOnce`实现前端一次性加载并支持分页和条件过滤操作。
<div class="p-4 text-base text-gray-800 rounded-lg bg-gray-50" role="alert">
<span class="font-medium text-gray-800 block">温馨提示</span>
<span class="block">开启<code>loadDataOnce</code>后,搜索和过滤将交给组件处理,默认对所有字段采用模糊匹配(比如:<code>mi</code>将会匹配<code>amis</code>。如果首次加载数据时设置了预设条件导致接口返回的数据集合未按照此规则过滤则可能导致切换页码后分页错误。此时有2种方案处理</span>
<span class="block" style="text-indent: 2em">1. 将接口返回的列表数据按照所有字段模糊匹配的规则处理</span>
<span class="block" style="text-indent: 2em">2. 配置<a href="#匹配函数"><code>matchFunc</code></a>,自定义处理过滤</span>
</div>
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade",
"sortable": true
}
]
}
```
配置一次性加载后,基本的分页、快速排序操作将会在前端进行完成。如果想实现前端检索(目前是模糊搜索),可以在 table 的 `columns` 对应项配置 `searchable` 来实现。
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version",
"searchable": {
"type": "select",
"name": "version",
"label": "Engine version",
"clearable": true,
"multiple": true,
"searchable": true,
"checkAll": true,
"options": ["1.7", "3.3", "5.6"],
"maxTagCount": 10,
"extractValue": true,
"joinValues": false,
"delimiter": ',',
"defaultCheckAll": false,
"checkAllLabel": "全选"
}
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
> **注意:**如果你的数据量较大,请务必使用服务端分页的方案,过多的前端数据展示,会显著影响前端页面的性能
另外前端一次性加载当有查寻条件的时候,默认还是会重新请求一次,如果配置 `loadDataOnceFetchOnFilter``false` 则为前端过滤。
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"loadDataOnceFetchOnFilter": false,
"autoGenerateFilter": true,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version",
"searchable": {
"type": "select",
"name": "version",
"label": "Engine version",
"clearable": true,
"multiple": true,
"searchable": true,
"checkAll": true,
"options": [
"1.7",
"3.3",
"5.6"
],
"maxTagCount": 10,
"extractValue": true,
"joinValues": false,
"delimiter": ",",
"defaultCheckAll": false,
"checkAllLabel": "全选"
}
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
`loadDataOnceFetchOnFilter` 配置成 `true` 则会强制重新请求接口比如以下用法
> 此时如果不配置或者配置为 `false` 是前端直接过滤,不过记得配置 name 为行数据中的属性,如果行数据中没有对应属性则不会起作用
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"loadDataOnceFetchOnFilter": true,
"headerToolbar": [
{
"type": "search-box",
"name": "keywords"
}
],
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version",
"searchable": {
"type": "select",
"name": "version",
"label": "Engine version",
"clearable": true,
"multiple": true,
"searchable": true,
"checkAll": true,
"options": [
"1.7",
"3.3",
"5.6"
],
"maxTagCount": 10,
"extractValue": true,
"joinValues": false,
"delimiter": ",",
"defaultCheckAll": false,
"checkAllLabel": "全选"
}
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
##### 匹配函数
> `3.5.0` 及以上版本
支持自定义匹配函数`matchFunc`,当开启`loadDataOnce`时,会基于该函数计算的匹配结果进行过滤,主要用于处理列字段类型较为复杂或者字段值格式和后端返回不一致的场景,函数签名如下:
```typescript
interface CRUDMatchFunc {
(
/* 当前列表的全量数据 */
items: any,
/* 最近一次接口返回的全量数据 */
itemsRaw: any,
/** 相关配置 */
options?: {
/* 查询参数 */
query: Record<string, any>;
/* 列配置 */
columns: any;
/** match-sorter 匹配函数 */
matchSorter: (items: any[], value: string, options?: MatchSorterOptions<any>) => any[]
}
): boolean;
}
```
具体效果请参考[示例](../../../examples/crud/match-func),从`3.6.0`版本开始,`options`中支持使用`matchSorter`函数处理复杂的过滤场景,比如前缀匹配、模糊匹配等,更多详细内容推荐查看[match-sorter](https://github.com/kentcdodds/match-sorter)。
### 批量操作
在`headerToolbar`或者`footerToolbar`数组中添加`bulkActions`字符串,并且在 crud 上配置`bulkActions`行为按钮数组,可以实现选中表格项并批量操作的功能。
@ -2934,252 +3189,6 @@ CRUD 中不限制有多少个单条操作、添加一个操作对应的添加一
`syncLocation`开启后数据域经过地址栏同步后原始值被转化为字符串同步回数据域但布尔值boolean同步后不符合预期数据结构导致组件渲染出错。比如查询条件表单中包含[Checkbox](./form/checkbox)组件,此时可以设置`{"trueValue": "1", "falseValue": "0"}`,将真值和假值设置为字符串格式规避。从`3.5.0`版本开始,已经支持[`parsePrimitiveQuery`](#解析query原始类型),该配置默认开启。
## 前端一次性加载
如果你的数据并不是很大,而且后端不方便做分页和条件过滤操作,那么通过配置`loadDataOnce`实现前端一次性加载并支持分页和条件过滤操作。
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade",
"sortable": true
}
]
}
```
配置一次性加载后,基本的分页、快速排序操作将会在前端进行完成。如果想实现前端检索(目前是模糊搜索),可以在 table 的 `columns` 对应项配置 `searchable` 来实现。
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version",
"searchable": {
"type": "select",
"name": "version",
"label": "Engine version",
"clearable": true,
"multiple": true,
"searchable": true,
"checkAll": true,
"options": ["1.7", "3.3", "5.6"],
"maxTagCount": 10,
"extractValue": true,
"joinValues": false,
"delimiter": ',',
"defaultCheckAll": false,
"checkAllLabel": "全选"
}
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
> **注意:**如果你的数据量较大,请务必使用服务端分页的方案,过多的前端数据展示,会显著影响前端页面的性能
另外前端一次性加载当有查寻条件的时候,默认还是会重新请求一次,如果配置 `loadDataOnceFetchOnFilter``false` 则为前端过滤。
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"loadDataOnceFetchOnFilter": false,
"autoGenerateFilter": true,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version",
"searchable": {
"type": "select",
"name": "version",
"label": "Engine version",
"clearable": true,
"multiple": true,
"searchable": true,
"checkAll": true,
"options": [
"1.7",
"3.3",
"5.6"
],
"maxTagCount": 10,
"extractValue": true,
"joinValues": false,
"delimiter": ",",
"defaultCheckAll": false,
"checkAllLabel": "全选"
}
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
`loadDataOnceFetchOnFilter` 配置成 `true` 则会强制重新请求接口比如以下用法
> 此时如果不配置或者配置为 `false` 是前端直接过滤,不过记得配置 name 为行数据中的属性,如果行数据中没有对应属性则不会起作用
```schema: scope="body"
{
"type": "crud",
"syncLocation": false,
"api": "/api/mock2/sample",
"loadDataOnce": true,
"loadDataOnceFetchOnFilter": true,
"headerToolbar": [
{
"type": "search-box",
"name": "keywords"
}
],
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version",
"searchable": {
"type": "select",
"name": "version",
"label": "Engine version",
"clearable": true,
"multiple": true,
"searchable": true,
"checkAll": true,
"options": [
"1.7",
"3.3",
"5.6"
],
"maxTagCount": 10,
"extractValue": true,
"joinValues": false,
"delimiter": ",",
"defaultCheckAll": false,
"checkAllLabel": "全选"
}
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
```
### 匹配函数
> `3.5.0` 及以上版本
支持自定义匹配函数`matchFunc`,当开启`loadDataOnce`时,会基于该函数计算的匹配结果进行过滤,主要用于处理列字段类型较为复杂或者字段值格式和后端返回不一致的场景,函数签名如下:
```typescript
interface CRUDMatchFunc {
(
/* 当前列表的全量数据 */
items: any,
/* 最近一次接口返回的全量数据 */
itemsRaw: any,
/** 相关配置 */
options?: {
/* 查询参数 */
query: Record<string, any>;
/* 列配置 */
columns: any;
}
): boolean;
}
```
具体效果请参考[示例](../../../examples/crud/match-func)。
## 动态列
> since 1.1.6

View File

@ -32,6 +32,8 @@ export default {
query: Record<string, any>,
/* 列配置 */
columns: any;
/** match-sorter 匹配函数 */
matchSorter: (items: any[], value: string, options?: MatchSorterOptions<any>) => any[]
}
): boolean;
}`

View File

@ -18,6 +18,8 @@ import {normalizeApiResponseData} from '../utils/api';
import {matchSorter} from 'match-sorter';
import {filter} from '../utils/tpl';
import type {MatchSorterOptions} from 'match-sorter';
interface MatchFunc {
(
/* 当前列表的全量数据 */
@ -30,6 +32,15 @@ interface MatchFunc {
query: Record<string, any>;
/* 列配置 */
columns: any;
/**
* match-sorter
* @doc https://github.com/kentcdodds/match-sorter
*/
matchSorter: (
items: any[],
value: string,
options?: MatchSorterOptions<any>
) => any[];
}
): any;
}
@ -223,7 +234,8 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
if (matchFunc && typeof matchFunc === 'function') {
items = matchFunc(items, self.data.itemsRaw, {
query: self.query,
columns: options.columns
columns: options.columns,
matchSorter: matchSorter
});
} else {
if (Array.isArray(options.columns)) {
@ -395,8 +407,11 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
};
if (options.loadDataOnce) {
// 记录原始集合,后续可能基于原始数据做排序查找。
data.itemsRaw = oItems || oRows;
/**
* 1.
* 2. items rows
*/
data.itemsRaw = oItems || oRows || rowsData.concat();
let filteredItems = rowsData.concat();
if (Array.isArray(options.columns)) {
@ -656,7 +671,8 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
if (matchFunc && typeof matchFunc === 'function') {
items = matchFunc(items, items.concat(), {
query: self.query,
columns: options.columns
columns: options.columns,
matchSorter: matchSorter
});
} else {
if (Array.isArray(options.columns)) {
@ -673,6 +689,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
if (value.length > 0) {
const arr = [...items];
let arrItems: Array<any> = [];
/** 搜索 query 值为数组的情况 */
value.forEach(item => {
arrItems = [
...arrItems,

View File

@ -1,9 +1,8 @@
import {toast, normalizeApiResponseData} from 'amis';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import React from 'react';
import {getEventControlConfig} from '../renderer/event-control/helper';
import {genCodeSchema} from '../renderer/APIAdaptorControl';
import {
getI18nEnabled,
jsonToJsonSchema,
@ -1210,6 +1209,38 @@ export class CRUDPlugin extends BasePlugin {
}
}),
{
name: 'matchFunc',
type: 'ae-functionEditorControl',
allowFullscreen: true,
mode: 'normal',
label: tipedLabel(
'搜索匹配函数',
'自定义搜索匹配函数,当开启<code>loadDataOnce</code>时,会基于该函数计算的匹配结果进行过滤,主要用于处理列字段类型较为复杂或者字段值格式和后端返回不一致的场景。<code>matchSorter</code>函数用于处理复杂的过滤场景,比如模糊匹配等,更多详细内容推荐查看<a href="https://github.com/kentcdodds/match-sorter" target="_blank">match-sorter</a>。'
),
renderLabel: true,
params: [
{
label: 'items',
tip: genCodeSchema('/* 当前列表的全量数据 */\nitems: any[]')
},
{
label: 'itemsRaw',
tip: genCodeSchema(
'/* 最近一次接口返回的全量数据 */\nitemsRaw: any[]'
)
},
{
label: 'options',
tip: genCodeSchema(
'/* 额外的配置 */\noptions?: {\n /* 查询参数 */\n query: Record < string, any>;\n /* 列配置 */\n columns: any;\n /** match-sorter 匹配函数 */\n matchSorter: (items: any[], value: string, options?: MatchSorterOptions<any>) => any[]\n}'
)
}
],
placeholder: `return items;`,
visibleOn: '${loadDataOnce === true}'
},
getSchemaTpl('switch', {
label: '开启定时刷新',
name: 'interval',

View File

@ -197,7 +197,7 @@ export class APIAdaptorControlRenderer extends APIAdaptorControl {}
* @param size width, height, tooltip时计算会偏移
* @returns
*/
const genCodeSchema = (code: string, size?: string[]) => ({
export const genCodeSchema = (code: string, size?: string[]) => ({
type: 'container',
...(!size
? {}