From bac12a2e5f8553826c9bc9733f1bc667b24bed9c Mon Sep 17 00:00:00 2001 From: lurunze1226 Date: Thu, 23 Nov 2023 11:09:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20CRUD=E7=BB=84=E4=BB=B6matchFunc?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BD=BF=E7=94=A8matchSorter=E5=87=BD?= =?UTF-8?q?=E6=95=B0;=20docs:=20=E5=89=8D=E7=AB=AF=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E7=9A=84=E4=BD=BF=E7=94=A8=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/crud.md | 505 +++++++++--------- examples/components/CRUD/MatchFunc.jsx | 2 + packages/amis-core/src/store/crud.ts | 25 +- packages/amis-editor/src/plugin/CRUD.tsx | 35 +- .../src/renderer/APIAdaptorControl.tsx | 2 +- 5 files changed, 314 insertions(+), 255 deletions(-) diff --git a/docs/zh-CN/components/crud.md b/docs/zh-CN/components/crud.md index 33958f4fe..72e877884 100755 --- a/docs/zh-CN/components/crud.md +++ b/docs/zh-CN/components/crud.md @@ -1600,7 +1600,7 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在 分页有两种模式: -##### 1. 知道数据总数 +**1. 知道数据总数** 如果后端可以知道数据总数时,接口返回格式如下: @@ -1624,7 +1624,7 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在 该模式下,会自动计算总页码数,渲染出有页码的分页组件 -##### 2. 不知道数据总数 +**2. 不知道数据总数** 如果后端无法知道数据总数,那么可以返回`hasNext`字段,来标识是否有下一页。 @@ -1650,6 +1650,261 @@ crud 组件支持通过配置`headerToolbar`和`footerToolbar`属性,实现在 > 如果总数据只够展示一页,则默认不显示该分页组件 +#### 前端分页 + +如果你的数据并不是很大,而且后端不方便做分页和条件过滤操作,那么通过配置`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; + /* 列配置 */ + columns: any; + /** match-sorter 匹配函数 */ + matchSorter: (items: any[], value: string, options?: MatchSorterOptions) => any[] + } + ): boolean; +} +``` + +具体效果请参考[示例](../../../examples/crud/match-func),从`3.6.0`版本开始,`options`中支持使用`matchSorter`函数处理复杂的过滤场景,比如前缀匹配、模糊匹配等,更多详细内容推荐查看[match-sorter](https://github.com/kentcdodds/match-sorter)。 + ### 批量操作 在`headerToolbar`或者`footerToolbar`数组中添加`bulkActions`字符串,并且在 crud 上配置`bulkActions`行为按钮数组,可以实现选中表格项并批量操作的功能。 @@ -2928,252 +3183,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; - /* 列配置 */ - columns: any; - } - ): boolean; -} -``` - -具体效果请参考[示例](../../../examples/crud/match-func)。 - ## 动态列 > since 1.1.6 diff --git a/examples/components/CRUD/MatchFunc.jsx b/examples/components/CRUD/MatchFunc.jsx index 279cfcc4a..46c8867cc 100644 --- a/examples/components/CRUD/MatchFunc.jsx +++ b/examples/components/CRUD/MatchFunc.jsx @@ -32,6 +32,8 @@ export default { query: Record, /* 列配置 */ columns: any; + /** match-sorter 匹配函数 */ + matchSorter: (items: any[], value: string, options?: MatchSorterOptions) => any[] } ): boolean; }` diff --git a/packages/amis-core/src/store/crud.ts b/packages/amis-core/src/store/crud.ts index cc6ec8631..5bc78ca34 100644 --- a/packages/amis-core/src/store/crud.ts +++ b/packages/amis-core/src/store/crud.ts @@ -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; /* 列配置 */ columns: any; + /** + * match-sorter 匹配函数 + * @doc https://github.com/kentcdodds/match-sorter + */ + matchSorter: ( + items: any[], + value: string, + options?: MatchSorterOptions + ) => 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 = []; + /** 搜索 query 值为数组的情况 */ value.forEach(item => { arrItems = [ ...arrItems, diff --git a/packages/amis-editor/src/plugin/CRUD.tsx b/packages/amis-editor/src/plugin/CRUD.tsx index dc0bbc7c5..a6f50b363 100644 --- a/packages/amis-editor/src/plugin/CRUD.tsx +++ b/packages/amis-editor/src/plugin/CRUD.tsx @@ -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( + '搜索匹配函数', + '自定义搜索匹配函数,当开启loadDataOnce时,会基于该函数计算的匹配结果进行过滤,主要用于处理列字段类型较为复杂或者字段值格式和后端返回不一致的场景。matchSorter函数用于处理复杂的过滤场景,比如模糊匹配等,更多详细内容推荐查看match-sorter。' + ), + 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[]\n}' + ) + } + ], + placeholder: `return items;`, + visibleOn: '${loadDataOnce === true}' + }, + getSchemaTpl('switch', { label: '开启定时刷新', name: 'interval', diff --git a/packages/amis-editor/src/renderer/APIAdaptorControl.tsx b/packages/amis-editor/src/renderer/APIAdaptorControl.tsx index d7d23478d..993268a34 100644 --- a/packages/amis-editor/src/renderer/APIAdaptorControl.tsx +++ b/packages/amis-editor/src/renderer/APIAdaptorControl.tsx @@ -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 ? {}