feat: CRUD & Table 嵌套支持懒加载 Close: #8411 (#8517)

This commit is contained in:
liaoxuezhi 2023-10-27 10:31:25 +08:00 committed by GitHub
parent b0f12d7557
commit cd5cd3f40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 814 additions and 99 deletions

View File

@ -200,11 +200,11 @@ CRUD 组件对数据源接口的数据结构要求如下:
| orderDir | 'asc'/'desc' | 排序方式 | | orderDir | 'asc'/'desc' | 排序方式 |
| keywords | string | 搜索关键字 | | keywords | string | 搜索关键字 |
### 解析Query原始类型 ### 解析 Query 原始类型
> `3.5.0`及以上版本 > `3.5.0`及以上版本
`syncLocation`开启后CRUD在初始化数据域时将会对url中的Query进行转换将原始类型的字符串格式的转化为同位类型目前仅支持**布尔类型** `syncLocation`开启后CRUD 在初始化数据域时,将会对 url 中的 Query 进行转换,将原始类型的字符串格式的转化为同位类型,目前仅支持**布尔类型**
``` ```
"true" ==> true "true" ==> true
@ -213,7 +213,6 @@ CRUD 组件对数据源接口的数据结构要求如下:
如果只想保持字符串格式,可以设置`"parsePrimitiveQuery": false`关闭该特性,具体效果参考[示例](../../../examples/crud/parse-primitive-query)。 如果只想保持字符串格式,可以设置`"parsePrimitiveQuery": false`关闭该特性,具体效果参考[示例](../../../examples/crud/parse-primitive-query)。
## 功能 ## 功能
既然这个渲染器叫增删改查,那接下来分开介绍这几个功能吧。 既然这个渲染器叫增删改查,那接下来分开介绍这几个功能吧。
@ -584,6 +583,61 @@ Cards 模式支持 [Cards](./cards) 中的所有功能。
} }
``` ```
## 嵌套
当行数据中存在 `children` 字段时CRUD 会自动识别为树形数据,并支持展开收起。
```schema: scope="body"
{
"type": "crud",
"name": "crud",
"syncLocation": false,
"api": "/api/mock2/crud/table2",
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
}
]
}
```
## 嵌套懒加载
如果数据量比较大不适合一次性加载,可以配置 `deferApi` 接口,结合行数据中标记 `defer: true` 属性,实现懒加载。
```schema: scope="body"
{
"type": "crud",
"name": "crud",
"syncLocation": false,
"api": "/api/mock2/crud/table6",
"deferApi": "/api/mock2/crud/table6?parentId=${id}",
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
}
]
}
```
## 查询条件表单 ## 查询条件表单
大部分表格展示有对数据进行检索的需求CRUD 自身支持通过配置`filter`,实现查询条件过滤表单。`filter` 配置实际上同 [Form](./form/index) 组件,因此支持绝大部分`form`的功能。 大部分表格展示有对数据进行检索的需求CRUD 自身支持通过配置`filter`,实现查询条件过滤表单。`filter` 配置实际上同 [Form](./form/index) 组件,因此支持绝大部分`form`的功能。
@ -3173,6 +3227,7 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据,
| title | `string` | `""` | 可设置成空,当设置成空时,没有标题栏 | | title | `string` | `""` | 可设置成空,当设置成空时,没有标题栏 |
| className | `string` | | 表格外层 Dom 的类名 | | className | `string` | | 表格外层 Dom 的类名 |
| api | [API](../../docs/types/api) | | CRUD 用来获取列表数据的 api。 | | api | [API](../../docs/types/api) | | CRUD 用来获取列表数据的 api。 |
| deferApi | [API](../../docs/types/api) | | 当行数据中有 defer 属性时,用此接口进一步加载内容 |
| loadDataOnce | `boolean` | | 是否一次性加载所有数据(前端分页) | | loadDataOnce | `boolean` | | 是否一次性加载所有数据(前端分页) |
| loadDataOnceFetchOnFilter | `boolean` | `true` | 在开启 loadDataOnce 时filter 时是否去重新请求 api | | loadDataOnceFetchOnFilter | `boolean` | `true` | 在开启 loadDataOnce 时filter 时是否去重新请求 api |
| source | `string` | | 数据映射接口返回某字段的值,不设置会默认使用接口返回的`${items}`或者`${rows}`,也可以设置成上层数据源的内容 | | source | `string` | | 数据映射接口返回某字段的值,不设置会默认使用接口返回的`${items}`或者`${rows}`,也可以设置成上层数据源的内容 |

View File

@ -1825,6 +1825,7 @@ popOver 的其它配置请参考 [popover](./popover)
| type | `string` | | `"type"` 指定为 table 渲染器 | | | type | `string` | | `"type"` 指定为 table 渲染器 | |
| title | `string` | | 标题 | | | title | `string` | | 标题 | |
| source | `string` | `${items}` | 数据源, 绑定当前环境变量 | | | source | `string` | `${items}` | 数据源, 绑定当前环境变量 | |
| deferApi | [API](../../docs/types/api) | | 当行数据中有 defer 属性时,用此接口进一步加载内容 |
| affixHeader | `boolean` | `true` | 是否固定表头 | | | affixHeader | `boolean` | `true` | 是否固定表头 | |
| columnsTogglable | `auto` 或者 `boolean` | `auto` | 展示列显示开关, 自动即:列数量大于或等于 5 个时自动开启 | | | columnsTogglable | `auto` 或者 `boolean` | `auto` | 展示列显示开关, 自动即:列数量大于或等于 5 个时自动开启 | |
| placeholder | `string` 或者 `SchemaTpl` | `暂无数据` | 当没数据的时候的文字提示 | | | placeholder | `string` 或者 `SchemaTpl` | `暂无数据` | 当没数据的时候的文字提示 | |

View File

@ -2,11 +2,9 @@ export default {
title: '支持多层嵌套,列数据中有 children 字段即可。建议不超过10层', title: '支持多层嵌套,列数据中有 children 字段即可。建议不超过10层',
body: { body: {
type: 'crud', type: 'crud',
api: '/api/mock2/crud/table2', api: '/api/mock2/crud/table6',
deferApi: '/api/mock2/crud/table6?parentId=${id}',
saveOrderApi: '/api/mock2/form/saveData', saveOrderApi: '/api/mock2/form/saveData',
expandConfig: {
expand: 'all'
},
draggable: true, draggable: true,
columns: [ columns: [
{ {

View File

@ -0,0 +1,527 @@
function findInTree(tree, id) {
let ret = null;
tree.some(function (item) {
if (item.id == id) {
ret = item;
return true;
}
if (item.children) {
ret = findInTree(item.children, id);
return !!ret;
}
});
return ret;
}
module.exports = function (req, res) {
const perPage = 10;
const page = req.query.page || 1;
let items = data.concat();
if (req.query.parentId) {
const item = findInTree(items, req.query.parentId);
if (!item) {
res.json({
status: 404,
msg: 'Not Found'
});
return;
}
res.json({
status: 0,
msg: 'ok',
data: {
...item,
children: Array.isArray(item.children)
? item.children.map(item => ({
...item,
children: undefined,
defer: !!(Array.isArray(item.children) && item.children.length)
}))
: []
}
});
return;
}
const ret = {
status: 0,
msg: 'ok',
data: {
count: items.length,
rows: items
.concat()
.splice((page - 1) * perPage, perPage)
.map(item => ({
...item,
children: undefined,
defer: !!(Array.isArray(item.children) && item.children.length)
}))
}
};
res.json(ret);
};
module.exports.cache = true;
const data = [
{
engine: 'Trident',
browser: 'Internet Explorer 4.0',
platform: 'Win 95+',
version: '4',
grade: 'X'
},
{
engine: 'Trident',
browser: 'Internet Explorer 5.0',
platform: 'Win 95+',
version: '5',
grade: 'C'
},
{
engine: 'Trident',
browser: 'Internet Explorer 5.5',
platform: 'Win 95+',
version: '5.5',
grade: 'A'
},
{
engine: 'Trident',
browser: 'Internet Explorer 6',
platform: 'Win 98+',
version: '6',
grade: 'A'
},
{
engine: 'Trident',
browser: 'Internet Explorer 7',
platform: 'Win XP SP2+',
version: '7',
grade: 'A'
},
{
engine: 'Trident',
browser: 'AOL browser (AOL desktop)',
platform: 'Win XP',
version: '6',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Firefox 1.0',
platform: 'Win 98+ / OSX.2+',
version: '1.7',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Firefox 1.5',
platform: 'Win 98+ / OSX.2+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Firefox 2.0',
platform: 'Win 98+ / OSX.2+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Firefox 3.0',
platform: 'Win 2k+ / OSX.3+',
version: '1.9',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Camino 1.0',
platform: 'OSX.2+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Camino 1.5',
platform: 'OSX.3+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Netscape 7.2',
platform: 'Win 95+ / Mac OS 8.6-9.2',
version: '1.7',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Netscape Browser 8',
platform: 'Win 98SE+',
version: '1.7',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Netscape Navigator 9',
platform: 'Win 98+ / OSX.2+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.0',
platform: 'Win 95+ / OSX.1+',
version: '1',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.1',
platform: 'Win 95+ / OSX.1+',
version: '1.1',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.2',
platform: 'Win 95+ / OSX.1+',
version: '1.2',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.3',
platform: 'Win 95+ / OSX.1+',
version: '1.3',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.4',
platform: 'Win 95+ / OSX.1+',
version: '1.4',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.5',
platform: 'Win 95+ / OSX.1+',
version: '1.5',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.6',
platform: 'Win 95+ / OSX.1+',
version: '1.6',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.7',
platform: 'Win 98+ / OSX.1+',
version: '1.7',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Mozilla 1.8',
platform: 'Win 98+ / OSX.1+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Seamonkey 1.1',
platform: 'Win 98+ / OSX.2+',
version: '1.8',
grade: 'A'
},
{
engine: 'Gecko',
browser: 'Epiphany 2.20',
platform: 'Gnome',
version: '1.8',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'Safari 1.2',
platform: 'OSX.3',
version: '125.5',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'Safari 1.3',
platform: 'OSX.3',
version: '312.8',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'Safari 2.0',
platform: 'OSX.4+',
version: '419.3',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'Safari 3.0',
platform: 'OSX.4+',
version: '522.1',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'OmniWeb 5.5',
platform: 'OSX.4+',
version: '420',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'iPod Touch / iPhone',
platform: 'iPod',
version: '420.1',
grade: 'A'
},
{
engine: 'Webkit',
browser: 'S60',
platform: 'S60',
version: '413',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 7.0',
platform: 'Win 95+ / OSX.1+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 7.5',
platform: 'Win 95+ / OSX.2+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 8.0',
platform: 'Win 95+ / OSX.2+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 8.5',
platform: 'Win 95+ / OSX.2+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 9.0',
platform: 'Win 95+ / OSX.3+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 9.2',
platform: 'Win 88+ / OSX.3+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera 9.5',
platform: 'Win 88+ / OSX.3+',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Opera for Wii',
platform: 'Wii',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Nokia N800',
platform: 'N800',
version: '-',
grade: 'A'
},
{
engine: 'Presto',
browser: 'Nintendo DS browser',
platform: 'Nintendo DS',
version: '8.5',
grade: 'C'
},
{
engine: 'KHTML',
browser: 'Konqureror 3.1',
platform: 'KDE 3.1',
version: '3.1',
grade: 'C'
},
{
engine: 'KHTML',
browser: 'Konqureror 3.3',
platform: 'KDE 3.3',
version: '3.3',
grade: 'A'
},
{
engine: 'KHTML',
browser: 'Konqureror 3.5',
platform: 'KDE 3.5',
version: '3.5',
grade: 'A'
},
{
engine: 'Tasman',
browser: 'Internet Explorer 4.5',
platform: 'Mac OS 8-9',
version: '-',
grade: 'X'
},
{
engine: 'Tasman',
browser: 'Internet Explorer 5.1',
platform: 'Mac OS 7.6-9',
version: '1',
grade: 'C'
},
{
engine: 'Tasman',
browser: 'Internet Explorer 5.2',
platform: 'Mac OS 8-X',
version: '1',
grade: 'C'
},
{
engine: 'Misc',
browser: 'NetFront 3.1',
platform: 'Embedded devices',
version: '-',
grade: 'C'
},
{
engine: 'Misc',
browser: 'NetFront 3.4',
platform: 'Embedded devices',
version: '-',
grade: 'A'
},
{
engine: 'Misc',
browser: 'Dillo 0.8',
platform: 'Embedded devices',
version: '-',
grade: 'X'
},
{
engine: 'Misc',
browser: 'Links',
platform: 'Text only',
version: '-',
grade: 'X'
},
{
engine: 'Misc',
browser: 'Lynx',
platform: 'Text only',
version: '-',
grade: 'X'
},
{
engine: 'Misc',
browser: 'IE Mobile',
platform: 'Windows Mobile 6',
version: '-',
grade: 'C'
},
{
engine: 'Misc',
browser: 'PSP browser',
platform: 'PSP',
version: '-',
grade: 'C'
},
{
engine: 'Other browsers',
browser: 'All others',
platform: '-',
version: '-',
grade: 'U'
}
].map(function (item, index) {
return Object.assign({}, item, {
id: index + 1,
children:
Math.random() > 0.5
? undefined
: [
{
engine: 'Trident',
browser: 'Internet Explorer 4.0',
platform: 'Win 95+',
version: '4',
grade: 'X'
},
{
engine: 'Trident',
browser: 'Internet Explorer 5.0',
platform: 'Win 95+',
version: '5',
grade: 'C'
},
{
engine: 'Other browsers',
browser: 'All others',
platform: '-',
version: '-',
grade: 'U'
}
].map(function (child, i) {
return Object.assign({}, child, {
id: (index + 1) * 100 + i + 1,
children: [
{
engine: 'Trident',
browser: 'Internet Explorer 4.0',
platform: 'Win 95+',
version: '4',
grade: 'X'
},
{
engine: 'Misc',
browser: 'Internet Explorer 5.0',
platform: 'Win 95+',
version: '5',
grade: 'C'
},
{
engine: 'Misc',
browser: 'Internet Explorer 5.0',
platform: 'Win 95+',
version: '1',
grade: 'A'
}
].map(function (child, i) {
return Object.assign({}, child, {
id: (i + 1) * 100 + (index + 1) * 1000 + i + 1
});
})
});
})
});
});

View File

@ -27,15 +27,20 @@ module.exports = function (req, res) {
return require(file)(req, res); return require(file)(req, res);
} else if (exist(jsFile)) { } else if (exist(jsFile)) {
let file = require.resolve(path.join(DIRNAME, jsFile)); let file = require.resolve(path.join(DIRNAME, jsFile));
delete require.cache[file]; let mod = require(file);
if (!mod.cache) {
delete require.cache[file];
mod = require(file);
}
if (req.query.waitSeconds) { if (req.query.waitSeconds) {
return setTimeout(function () { return setTimeout(function () {
require(file)(req, res); mod(req, res);
}, parseInt(req.query.waitSeconds, 10) * 1000); }, parseInt(req.query.waitSeconds, 10) * 1000);
} }
return require(file)(req, res); return mod(req, res);
} }
if (exist(jsonFile)) { if (exist(jsonFile)) {
if (req.query.waitSeconds) { if (req.query.waitSeconds) {

View File

@ -39,6 +39,45 @@ import {getStoreById} from './manager';
*/ */
const PARTITION_INDEX = 3; const PARTITION_INDEX = 3;
function initChildren(
children: Array<any>,
depth: number,
pindex: number,
parentId: string,
path: string = ''
): any {
depth += 1;
return children.map((item, index) => {
item = isObject(item)
? item
: {
item
};
const id = item.__id ?? guid();
return {
// id: String(item && (item as any)[self.primaryField] || `${pindex}-${depth}-${key}`),
id: String(id),
parentId: String(parentId),
key: String(`${pindex}-${depth}-${index}`),
path: `${path}${index}`,
depth: depth,
index: index,
newIndex: index,
pristine: item,
data: item,
defer: !!item.defer,
loaded: false,
loading: false,
rowSpans: {},
children:
item && Array.isArray(item.children)
? initChildren(item.children, depth, index, id, `${path}${index}.`)
: []
};
});
}
export const Column = types export const Column = types
.model('Column', { .model('Column', {
label: types.optional(types.frozen(), undefined), label: types.optional(types.frozen(), undefined),
@ -127,20 +166,33 @@ export const Row = types
rowSpans: types.frozen({} as any), rowSpans: types.frozen({} as any),
index: types.number, index: types.number,
newIndex: types.number, newIndex: types.number,
nth: 0,
path: '', // 行数据的位置 path: '', // 行数据的位置
expandable: false,
checkdisable: false, checkdisable: false,
isHover: false, isHover: false,
children: types.optional( children: types.optional(
types.array(types.late((): IAnyModelType => Row)), types.array(types.late((): IAnyModelType => Row)),
[] []
), ),
defer: false, // 是否为懒数据
loaded: false, // 懒数据是否加载完了
loading: false, // 懒数据是否正在加载
error: '', // 懒数据加载失败的错误信息
depth: types.number, // 当前children位于第几层便于使用getParent获取最顶层TableStore depth: types.number, // 当前children位于第几层便于使用getParent获取最顶层TableStore
appeared: true, appeared: true,
lazyRender: false lazyRender: false
}) })
.views(self => ({ .views(self => ({
get expandable(): boolean {
let table: any;
return !!(
(self && self.children.length) ||
(self && self.defer && !self.loaded) ||
((table = getParent(self, self.depth * 2) as any) &&
table.footable &&
table.footableColumns.length)
);
},
get checked(): boolean { get checked(): boolean {
return (getParent(self, self.depth * 2) as ITableStore).isSelected( return (getParent(self, self.depth * 2) as ITableStore).isSelected(
self as IRow self as IRow
@ -320,36 +372,70 @@ export const Row = types
}); });
if (Array.isArray(data.children)) { if (Array.isArray(data.children)) {
const arr = data.children; this.replaceChildren(data.children);
const pool = arr.concat(); }
},
// 把多的删了先 replaceChildren(children: Array<any>) {
if (self.children.length > arr.length) { const arr = children;
self.children.splice(arr.length, self.children.length - arr.length); const pool = arr.concat();
}
// 把多的删了先
let index = 0; if (self.children.length > arr.length) {
const len = self.children.length; self.children.splice(arr.length, self.children.length - arr.length);
while (pool.length) { }
// 因为父级id未更新所以需要将子级的parentId正确指向父级id
const item = { let index = 0;
...pool.shift(), const len = self.children.length;
parentId: self.id while (pool.length) {
}!; // 因为父级id未更新所以需要将子级的parentId正确指向父级id
const item = {
if (index < len) { ...pool.shift(),
self.children[index].replaceWith(item); parentId: self.id
} else { }!;
const row = Row.create(item);
self.children.push(row); if (index < len) {
} self.children[index].replaceWith(item);
index++; } else {
const row = Row.create(item);
self.children.push(row);
} }
index++;
} }
}, },
markAppeared(value: any) { markAppeared(value: any) {
value && (self.appeared = !!value); value && (self.appeared = !!value);
},
markLoading(value: any) {
self.loading = !!value;
},
markLoaded(value: any) {
self.loaded = !!value;
},
setError(value: any) {
self.error = String(value);
},
resetDefered() {
self.error = '';
self.loaded = false;
},
setDeferData({children, ...rest}: any) {
self.data = {
...self.data,
...rest
};
if (Array.isArray(children)) {
this.replaceChildren(
initChildren(children, self.depth, self.index, self.id, self.path)
);
}
} }
})); }));
@ -1238,55 +1324,6 @@ export const TableStore = iRendererStore
return combineCell(arr, keys); return combineCell(arr, keys);
} }
function initChildren(
children: Array<any>,
depth: number,
pindex: number,
parentId: string,
path: string = '',
nThRef: {index: number}
): any {
depth += 1;
return children.map((item, index) => {
item = isObject(item)
? item
: {
item
};
const id = item.__id ?? guid();
return {
// id: String(item && (item as any)[self.primaryField] || `${pindex}-${depth}-${key}`),
id: String(id),
parentId: String(parentId),
key: String(`${pindex}-${depth}-${index}`),
path: `${path}${index}`,
depth: depth,
index: index,
nth: nThRef.index++,
newIndex: index,
pristine: item,
data: item,
rowSpans: {},
children:
item && Array.isArray(item.children)
? initChildren(
item.children,
depth,
index,
id,
`${path}${index}.`,
nThRef
)
: [],
expandable: !!(
(item && Array.isArray(item.children) && item.children.length) ||
(self.footable && self.footableColumns.length)
)
};
});
}
function initRows( function initRows(
rows: Array<any>, rows: Array<any>,
getEntryId?: (entry: any, index: number) => string, getEntryId?: (entry: any, index: number) => string,
@ -1298,7 +1335,6 @@ export const TableStore = iRendererStore
/* 避免输入内容为非数组挂掉 */ /* 避免输入内容为非数组挂掉 */
rows = !Array.isArray(rows) ? [] : rows; rows = !Array.isArray(rows) ? [] : rows;
const nThRef = {index: 0};
let arr: Array<SRow> = rows.map((item, index) => { let arr: Array<SRow> = rows.map((item, index) => {
if (!isObject(item)) { if (!isObject(item)) {
item = { item = {
@ -1315,20 +1351,18 @@ export const TableStore = iRendererStore
key: String(`${index}-1-${index}`), key: String(`${index}-1-${index}`),
depth: 1, // 最大父节点默认为第一层,逐层叠加 depth: 1, // 最大父节点默认为第一层,逐层叠加
index: index, index: index,
nth: nThRef.index++,
newIndex: index, newIndex: index,
pristine: item, pristine: item,
path: `${index}`, path: `${index}`,
data: item, data: item,
rowSpans: {}, rowSpans: {},
defer: !!item.defer,
loaded: false,
loading: false,
children: children:
item && Array.isArray(item.children) item && Array.isArray(item.children)
? initChildren(item.children, 1, index, id, `${index}.`, nThRef) ? initChildren(item.children, 1, index, id, `${index}.`)
: [], : []
expandable: !!(
(item && Array.isArray(item.children) && item.children.length) ||
(self.footable && self.footableColumns.length)
)
}; };
}); });
@ -1342,7 +1376,9 @@ export const TableStore = iRendererStore
} }
replaceRow(arr, reUseRow); replaceRow(arr, reUseRow);
self.isNested = self.rows.some(item => item.children.length); self.isNested = self.rows.some(
item => item.children.length || (item.defer && !item.loaded)
);
// 前 20 个直接渲染,后面的按需渲染 // 前 20 个直接渲染,后面的按需渲染
if ( if (

View File

@ -620,6 +620,10 @@
> thead > tr > th.#{$ns}Table-primayCell, > thead > tr > th.#{$ns}Table-primayCell,
> tbody > tr > td.#{$ns}Table-primayCell { > tbody > tr > td.#{$ns}Table-primayCell {
white-space: nowrap; // 树形表格展示标题栏不要换行 white-space: nowrap; // 树形表格展示标题栏不要换行
> .#{$ns}Spinner {
vertical-align: middle;
}
} }
} }
@ -864,6 +868,14 @@
} }
} }
&-retryBtn {
color: var(--Form-feedBack-color);
cursor: pointer;
&:hover {
color: var(--Form-feedBack-color);
}
}
&-expandBtn, &-expandBtn,
&-expandBtn2 { &-expandBtn2 {
position: relative; position: relative;

View File

@ -197,6 +197,8 @@ register('de-DE', {
'Options.editLabel': 'Bearbeiten {{label}}', 'Options.editLabel': 'Bearbeiten {{label}}',
'Options.label': 'Option', 'Options.label': 'Option',
'Options.createFailed': 'Erstellen fehlgeschlagen', 'Options.createFailed': 'Erstellen fehlgeschlagen',
'Options.retry':
"Laden fehlgeschlagen '{{reason}}', klicken Sie auf Wiederholen",
'placeholder.empty': '<Empty>', 'placeholder.empty': '<Empty>',
'placeholder.enter': 'Eingabe', 'placeholder.enter': 'Eingabe',
'placeholder.noData': 'Keine Daten', 'placeholder.noData': 'Keine Daten',

View File

@ -189,6 +189,7 @@ register('en-US', {
'Options.editLabel': 'Edit {{label}}', 'Options.editLabel': 'Edit {{label}}',
'Options.label': 'option', 'Options.label': 'option',
'Options.createFailed': 'create failed, please check', 'Options.createFailed': 'create failed, please check',
'Options.retry': "Loading failed '{{reason}}', click retry",
'placeholder.empty': '<Empty>', 'placeholder.empty': '<Empty>',
'placeholder.enter': 'Enter', 'placeholder.enter': 'Enter',
'placeholder.noData': 'No data', 'placeholder.noData': 'No data',

View File

@ -194,6 +194,7 @@ register('zh-CN', {
'Options.editLabel': '编辑{{label}}', 'Options.editLabel': '编辑{{label}}',
'Options.label': '选项', 'Options.label': '选项',
'Options.createFailed': '新增失败,请仔细检查', 'Options.createFailed': '新增失败,请仔细检查',
'Options.retry': '加载失败「{{reason}}」,点击重试',
'placeholder.empty': '<空>', 'placeholder.empty': '<空>',
'placeholder.enter': '请输入', 'placeholder.enter': '请输入',
'placeholder.noData': '暂无数据', 'placeholder.noData': '暂无数据',

View File

@ -132,6 +132,11 @@ export interface CRUDCommonSchema extends BaseSchema, SpinnerExtraProps {
*/ */
api?: SchemaApi; api?: SchemaApi;
/**
* API defer: true API
*/
deferApi?: SchemaApi;
/** /**
* *
*/ */

View File

@ -9,7 +9,7 @@ import {
buildTrackExpression, buildTrackExpression,
evalTrackExpression evalTrackExpression
} from 'amis-core'; } from 'amis-core';
import {BadgeObject, Checkbox, Icon} from 'amis-ui'; import {BadgeObject, Checkbox, Icon, Spinner} from 'amis-ui';
import React from 'react'; import React from 'react';
export interface CellProps extends ThemeProps { export interface CellProps extends ThemeProps {
@ -32,6 +32,7 @@ export interface CellProps extends ThemeProps {
popOverContainer?: any; popOverContainer?: any;
quickEditFormRef: any; quickEditFormRef: any;
onImageEnlarge?: any; onImageEnlarge?: any;
translate: (key: string, ...args: Array<any>) => string;
} }
export default function Cell({ export default function Cell({
@ -51,7 +52,8 @@ export default function Cell({
onDragStart, onDragStart,
popOverContainer, popOverContainer,
quickEditFormRef, quickEditFormRef,
onImageEnlarge onImageEnlarge,
translate: __
}: CellProps) { }: CellProps) {
if (column.name && item.rowSpans[column.name] === 0) { if (column.name && item.rowSpans[column.name] === 0) {
return null; return null;
@ -134,7 +136,18 @@ export default function Cell({
/> />
); );
prefix.push( prefix.push(
item.expandable ? ( item.loading ? (
<Spinner key="loading" size="sm" show />
) : item.error ? (
<a
className={cx('Table-retryBtn')}
key="retryBtn"
onClick={item.resetDefered}
data-tooltip={__('Options.retry', {reason: item.error})}
>
<Icon icon="retry" className="icon" />
</a>
) : item.expandable ? (
<a <a
key="expandBtn2" key="expandBtn2"
className={cx('Table-expandBtn2', item.expanded ? 'is-active' : '')} className={cx('Table-expandBtn2', item.expanded ? 'is-active' : '')}
@ -169,7 +182,13 @@ export default function Cell({
); );
} }
return [prefix, affix, addtionalClassName]; return [prefix, affix, addtionalClassName];
}, [item.expandable, item.expanded, column.isPrimary]); }, [
item.expandable,
item.expanded,
item.error,
item.loading,
column.isPrimary
]);
// 根据条件缓存 data避免孩子重复渲染 // 根据条件缓存 data避免孩子重复渲染
const hasCustomTrackExpression = const hasCustomTrackExpression =

View File

@ -60,6 +60,8 @@ export class TableRow extends React.PureComponent<
depth: number; depth: number;
expandable: boolean; expandable: boolean;
appeard?: boolean; appeard?: boolean;
loading?: boolean;
error?: string;
checkdisable: boolean; checkdisable: boolean;
trRef?: React.Ref<any>; trRef?: React.Ref<any>;
isNested?: boolean; isNested?: boolean;
@ -362,6 +364,8 @@ export default observer((props: TableRowProps) => {
depth={item.depth} depth={item.depth}
expandable={item.expandable} expandable={item.expandable}
checkdisable={item.checkdisable} checkdisable={item.checkdisable}
loading={item.loading}
error={item.error}
// data 在 TableRow 里面没有使用,这里写上是为了当列数据变化的时候 TableRow 重新渲染, // data 在 TableRow 里面没有使用,这里写上是为了当列数据变化的时候 TableRow 重新渲染,
// 不是 item.locals 的原因是 item.locals 会变化多次,比如父级上下文变化也会进来,但是 item.data 只会变化一次。 // 不是 item.locals 的原因是 item.locals 会变化多次,比如父级上下文变化也会进来,但是 item.data 只会变化一次。
data={canAccessSuperData ? item.locals : item.data} data={canAccessSuperData ? item.locals : item.data}

View File

@ -7,7 +7,8 @@ import {
SchemaExpression, SchemaExpression,
position, position,
animation, animation,
evalExpressionWithConditionBuilder evalExpressionWithConditionBuilder,
isEffectiveApi
} from 'amis-core'; } from 'amis-core';
import {Renderer, RendererProps} from 'amis-core'; import {Renderer, RendererProps} from 'amis-core';
import {SchemaNode, ActionObject, Schema} from 'amis-core'; import {SchemaNode, ActionObject, Schema} from 'amis-core';
@ -71,6 +72,7 @@ import ColGroup from './ColGroup';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import AutoFilterForm from './AutoFilterForm'; import AutoFilterForm from './AutoFilterForm';
import Cell from './Cell'; import Cell from './Cell';
import {reaction} from 'mobx';
/** /**
* *
@ -343,6 +345,11 @@ export interface TableSchema extends BaseSchema {
* table layout * table layout
*/ */
tableLayout?: 'fixed' | 'auto'; tableLayout?: 'fixed' | 'auto';
/**
* API defer: true API
*/
deferApi?: SchemaApi;
} }
export interface TableProps extends RendererProps, SpinnerExtraProps { export interface TableProps extends RendererProps, SpinnerExtraProps {
@ -640,6 +647,18 @@ export default class Table extends React.Component<TableProps, object> {
formItem && isAlive(formItem) && formItem.setSubStore(store); formItem && isAlive(formItem) && formItem.setSubStore(store);
Table.syncRows(store, this.props, undefined) && this.syncSelected(); Table.syncRows(store, this.props, undefined) && this.syncSelected();
this.toDispose.push(
reaction(
() =>
store
.getExpandedRows()
.filter(
row => row.defer && !row.loaded && !row.loading && !row.error
),
(rows: Array<IRow>) => rows.forEach(this.loadDeferredRow)
)
);
} }
static syncRows( static syncRows(
@ -711,6 +730,34 @@ export default class Table extends React.Component<TableProps, object> {
} }
} }
@autobind
async loadDeferredRow(row: IRow) {
const {env} = this.props;
const deferApi = row.data.deferApi || this.props.deferApi;
if (!isEffectiveApi(deferApi)) {
throw new Error('deferApi is required');
}
try {
row.markLoading(true);
const response = await env.fetcher(deferApi, row.locals);
if (!response.ok) {
throw new Error(response.msg);
}
row.setDeferData(response.data);
row.markLoaded(true);
row.setError('');
} catch (e) {
row.setError(e.message);
env.notify('error', e.message);
} finally {
row.markLoading(false);
}
}
/** /**
* *
* css dom hack * css dom hack
@ -2024,7 +2071,8 @@ export default class Table extends React.Component<TableProps, object> {
checkOnItemClick, checkOnItemClick,
popOverContainer, popOverContainer,
canAccessSuperData, canAccessSuperData,
itemBadge itemBadge,
translate
} = this.props; } = this.props;
return ( return (
@ -2047,6 +2095,7 @@ export default class Table extends React.Component<TableProps, object> {
popOverContainer={this.getPopOverContainer} popOverContainer={this.getPopOverContainer}
quickEditFormRef={this.subFormRef} quickEditFormRef={this.subFormRef}
onImageEnlarge={this.handleImageEnlarge} onImageEnlarge={this.handleImageEnlarge}
translate={translate}
/> />
); );
} }