chore: 优化 Table 渲染性能

This commit is contained in:
2betop 2023-09-06 16:00:44 +08:00
parent df59d92a4d
commit fd93dedf2f
25 changed files with 865 additions and 526 deletions

View File

@ -1845,6 +1845,7 @@ popOver 的其它配置请参考 [popover](./popover)
| resizable | `boolean` | `true` | 列宽度是否支持调整 | | | resizable | `boolean` | `true` | 列宽度是否支持调整 | |
| selectable | `boolean` | `false` | 支持勾选 | | | selectable | `boolean` | `false` | 支持勾选 | |
| multiple | `boolean` | `false` | 勾选 icon 是否为多选样式`checkbox` 默认为`radio` | | | multiple | `boolean` | `false` | 勾选 icon 是否为多选样式`checkbox` 默认为`radio` | |
| lazyRenderAfter | `number` | `100` | 用来控制从第几行开始懒渲染行,用来渲染大表格时有用 | |
### 列配置属性表 ### 列配置属性表

View File

@ -1,4 +1,4 @@
module.exports = [ const list = [
{ {
engine: 'Trident', engine: 'Trident',
browser: 'Internet Explorer 4.0', browser: 'Internet Explorer 4.0',
@ -1202,8 +1202,21 @@ module.exports = [
version: '-', version: '-',
grade: 'U' grade: 'U'
} }
].map(function (item, index) { ];
return Object.assign({}, item, {
id: index + 1 // 多来点测试数据
module.exports = list
.concat(list)
.concat(list)
.concat(list)
.concat(list)
.concat(list)
.concat(list)
.concat(list)
.concat(list)
.concat(list)
.map(function (item, index) {
return Object.assign({}, item, {
id: index + 1
});
}); });
});

View File

@ -57,3 +57,22 @@ export const createMockMediaMatcher =
return true; return true;
} }
}); });
// Mock IntersectionObserver
class IntersectionObserver {
observe = jest.fn();
disconnect = jest.fn();
unobserve = jest.fn();
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: IntersectionObserver
});
Object.defineProperty(global, 'IntersectionObserver', {
writable: true,
configurable: true,
value: IntersectionObserver
});

View File

@ -46,7 +46,7 @@
"esm" "esm"
], ],
"dependencies": { "dependencies": {
"amis-formula": "^3.4.0", "amis-formula": "*",
"classnames": "2.3.2", "classnames": "2.3.2",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"hoist-non-react-statics": "^3.3.2", "hoist-non-react-statics": "^3.3.2",
@ -58,8 +58,8 @@
"moment": "^2.19.4", "moment": "^2.19.4",
"papaparse": "^5.3.0", "papaparse": "^5.3.0",
"qs": "6.9.7", "qs": "6.9.7",
"react-intersection-observer": "9.5.2",
"react-json-view": "1.21.3", "react-json-view": "1.21.3",
"react-visibility-sensor": "5.1.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"uncontrollable": "7.2.1" "uncontrollable": "7.2.1"
}, },

View File

@ -440,14 +440,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
exprProps = {}; exprProps = {};
} }
// style 支持公式
if (schema.style) {
// schema.style是readonly属性
schema = {...schema, style: buildStyle(schema.style, detectData)};
}
const isClassComponent = Component.prototype?.isReactComponent; const isClassComponent = Component.prototype?.isReactComponent;
const $schema = {...schema, ...exprProps};
let props = { let props = {
...theme.getRendererConfig(renderer.name), ...theme.getRendererConfig(renderer.name),
...restSchema, ...restSchema,
@ -459,7 +452,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
defaultActiveKey: defaultActiveKey, defaultActiveKey: defaultActiveKey,
propKey: propKey, propKey: propKey,
$path: $path, $path: $path,
$schema: $schema, $schema: schema,
ref: this.refFn, ref: this.refFn,
render: this.renderChild, render: this.renderChild,
rootStore, rootStore,
@ -468,6 +461,11 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
mobileUI: schema.useMobileUI === false ? false : rest.mobileUI mobileUI: schema.useMobileUI === false ? false : rest.mobileUI
}; };
// style 支持公式
if (schema.style) {
(props as any).style = buildStyle(schema.style, detectData);
}
if (disable !== undefined) { if (disable !== undefined) {
(props as any).disabled = disable; (props as any).disabled = disable;
} }
@ -478,7 +476,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
// 自动解析变量模式,主要是方便直接引入第三方组件库,无需为了支持变量封装一层 // 自动解析变量模式,主要是方便直接引入第三方组件库,无需为了支持变量封装一层
if (renderer.autoVar) { if (renderer.autoVar) {
for (const key of Object.keys($schema)) { for (const key of Object.keys(schema)) {
if (typeof props[key] === 'string') { if (typeof props[key] === 'string') {
props[key] = resolveVariableAndFilter( props[key] = resolveVariableAndFilter(
props[key], props[key],

View File

@ -5,7 +5,7 @@
*/ */
import React from 'react'; import React from 'react';
import VisibilitySensor from 'react-visibility-sensor'; import {InView} from 'react-intersection-observer';
export interface LazyComponentProps { export interface LazyComponentProps {
component?: React.ElementType; component?: React.ElementType;
@ -13,7 +13,6 @@ export interface LazyComponentProps {
placeholder?: React.ReactNode; placeholder?: React.ReactNode;
unMountOnHidden?: boolean; unMountOnHidden?: boolean;
childProps?: object; childProps?: object;
visiblilityProps?: object;
[propName: string]: any; [propName: string]: any;
} }
@ -56,7 +55,7 @@ export default class LazyComponent extends React.Component<
this.mounted = false; this.mounted = false;
} }
handleVisibleChange(visible: boolean) { handleVisibleChange(visible: boolean, entry?: any) {
this.setState({ this.setState({
visible: visible visible: visible
}); });
@ -91,7 +90,6 @@ export default class LazyComponent extends React.Component<
placeholder, placeholder,
unMountOnHidden, unMountOnHidden,
childProps, childProps,
visiblilityProps,
partialVisibility, partialVisibility,
children, children,
...rest ...rest
@ -102,33 +100,42 @@ export default class LazyComponent extends React.Component<
// 需要监听从可见到不可见。 // 需要监听从可见到不可见。
if (unMountOnHidden) { if (unMountOnHidden) {
return ( return (
<VisibilitySensor <InView
{...visiblilityProps}
partialVisibility={partialVisibility}
onChange={this.handleVisibleChange} onChange={this.handleVisibleChange}
threshold={partialVisibility ? 0 : 1}
> >
<div className="visibility-sensor"> {({ref}) => {
{Component && visible ? ( return (
<Component {...rest} {...childProps} /> <div
) : children && visible ? ( ref={ref}
children className={`visibility-sensor ${visible ? 'in' : ''}`}
) : ( >
placeholder {Component && visible ? (
)} <Component {...rest} {...childProps} />
</div> ) : children && visible ? (
</VisibilitySensor> children
) : (
placeholder
)}
</div>
);
}}
</InView>
); );
} }
if (!visible) { if (!visible) {
return ( return (
<VisibilitySensor <InView
{...visiblilityProps}
partialVisibility={partialVisibility}
onChange={this.handleVisibleChange} onChange={this.handleVisibleChange}
threshold={partialVisibility ? 0 : 1}
> >
<div className="visibility-sensor">{placeholder}</div> {({ref}) => (
</VisibilitySensor> <div ref={ref} className="visibility-sensor">
{placeholder}
</div>
)}
</InView>
); );
} else if (Component) { } else if (Component) {
// 只监听不可见到可见,一旦可见了,就销毁检查。 // 只监听不可见到可见,一旦可见了,就销毁检查。

View File

@ -3,7 +3,7 @@ import {IFormStore, IFormItemStore} from '../store/form';
import debouce from 'lodash/debounce'; import debouce from 'lodash/debounce';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import {RendererProps, Renderer} from '../factory'; import {RendererProps, Renderer, getRendererByName} from '../factory';
import {ComboStore, IComboStore, IUniqueGroup} from '../store/combo'; import {ComboStore, IComboStore, IUniqueGroup} from '../store/combo';
import { import {
anyChanged, anyChanged,
@ -32,7 +32,7 @@ import {FormBaseControl, FormItemWrap} from './Item';
import {Api} from '../types'; import {Api} from '../types';
import {TableStore} from '../store/table'; import {TableStore} from '../store/table';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import {callStrFunction} from '../utils'; import {callStrFunction, changedEffect} from '../utils';
export interface ControlOutterProps extends RendererProps { export interface ControlOutterProps extends RendererProps {
formStore?: IFormStore; formStore?: IFormStore;
@ -121,6 +121,8 @@ export function wrapControl<
onChange, onChange,
data, data,
inputGroupControl, inputGroupControl,
colIndex,
rowIndex,
$schema: { $schema: {
name, name,
id, id,
@ -154,17 +156,14 @@ export function wrapControl<
this.setPrinstineValue = this.setPrinstineValue.bind(this); this.setPrinstineValue = this.setPrinstineValue.bind(this);
this.controlRef = this.controlRef.bind(this); this.controlRef = this.controlRef.bind(this);
this.handleBlur = this.handleBlur.bind(this); this.handleBlur = this.handleBlur.bind(this);
this.validate = this.validate.bind(this);
this.flushChange = this.flushChange.bind(this);
if (!name) { if (!name) {
// 一般情况下这些表单项都是需要 name 的,提示一下 // 一般情况下这些表单项都是需要 name 的,提示一下
if ( if (
typeof type === 'string' && typeof type === 'string' &&
(type.startsWith('input-') || getRendererByName(type)?.isFormItem
type.endsWith('select') ||
type === 'switch' ||
type === 'textarea' ||
type === 'radios') &&
type !== 'input-group'
) { ) {
console.warn('name is required', this.props.$schema); console.warn('name is required', this.props.$schema);
} }
@ -178,7 +177,9 @@ export function wrapControl<
path: this.props.$path, path: this.props.$path,
storeType: FormItemStore.name, storeType: FormItemStore.name,
parentId: store?.id, parentId: store?.id,
name name,
colIndex: colIndex !== undefined ? colIndex : undefined,
rowIndex: rowIndex !== undefined ? rowIndex : undefined
}) as IFormItemStore; }) as IFormItemStore;
this.model = model; this.model = model;
// @issue 打算干掉这个 // @issue 打算干掉这个
@ -226,6 +227,7 @@ export function wrapControl<
if (propValue !== undefined && propValue !== null) { if (propValue !== undefined && propValue !== null) {
// 同步 value: 优先使用 props 中的 value // 同步 value: 优先使用 props 中的 value
model.changeTmpValue(propValue, 'controlled'); model.changeTmpValue(propValue, 'controlled');
model.setIsControlled(true);
} else { } else {
const isExp = isExpression(value); const isExp = isExpression(value);
@ -332,12 +334,10 @@ export function wrapControl<
componentDidUpdate(prevProps: OuterProps) { componentDidUpdate(prevProps: OuterProps) {
const props = this.props; const props = this.props;
const form = props.formStore;
const model = this.model; const model = this.model;
if ( model &&
model && changedEffect(
anyChanged(
[ [
'id', 'id',
'validations', 'validations',
@ -362,34 +362,17 @@ export function wrapControl<
'extraName' 'extraName'
], ],
prevProps.$schema, prevProps.$schema,
props.$schema props.$schema,
) changes => {
) { model.config({
model.config({ ...changes,
required: props.$schema.required,
id: props.$schema.id, // todo 优化后面两个
unique: props.$schema.unique, isValueSchemaExp: isExpression(props.$schema.value),
value: props.$schema.value, inputGroupControl: props?.inputGroupControl
isValueSchemaExp: isExpression(props.$schema.value), } as any);
rules: props.$schema.validations, }
multiple: props.$schema.multiple, );
delimiter: props.$schema.delimiter,
valueField: props.$schema.valueField,
labelField: props.$schema.labelField,
joinValues: props.$schema.joinValues,
extractValue: props.$schema.extractValue,
messages: props.$schema.validationErrors,
selectFirst: props.$schema.selectFirst,
autoFill: props.$schema.autoFill,
clearValueOnHidden: props.$schema.clearValueOnHidden,
validateApi: props.$schema.validateApi,
minLength: props.$schema.minLength,
maxLength: props.$schema.maxLength,
label: props.$schema.label,
inputGroupControl: props?.inputGroupControl,
extraName: props.$schema.extraName
});
}
// 此处需要同时考虑 defaultValue 和 value // 此处需要同时考虑 defaultValue 和 value
if (model && typeof props.value !== 'undefined') { if (model && typeof props.value !== 'undefined') {
@ -605,13 +588,16 @@ export function wrapControl<
result = [await this.model.validate(data)]; result = [await this.model.validate(data)];
} }
if (result && result.length) { const valid = !result.some(item => item === false);
if (result.indexOf(false) > -1) { formItemDispatchEvent?.(
formItemDispatchEvent('formItemValidateError', data); valid ? 'formItemValidateSucc' : 'formItemValidateError',
} else { data
formItemDispatchEvent('formItemValidateSucc', data); );
} return valid;
} }
flushChange() {
this.lazyEmitChange.flush();
} }
handleChange( handleChange(
@ -862,6 +848,8 @@ export function wrapControl<
getValue: this.getValue, getValue: this.getValue,
prinstine: model ? model.prinstine : undefined, prinstine: model ? model.prinstine : undefined,
setPrinstineValue: this.setPrinstineValue, setPrinstineValue: this.setPrinstineValue,
onValidate: this.validate,
onFlushChange: this.flushChange,
// !没了这个, tree 里的 options 渲染会出问题 // !没了这个, tree 里的 options 渲染会出问题
_filteredOptions: this.model?.filteredOptions _filteredOptions: this.model?.filteredOptions
}; };

View File

@ -117,26 +117,27 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
replace: boolean = false replace: boolean = false
) { ) {
const originQuery = self.query; const originQuery = self.query;
self.query = replace const query: any = replace
? { ? {
...values ...values
} }
: { : {
...self.query, ...originQuery,
...values ...values
}; };
if (self.query[pageField || 'page']) { if (isObjectShallowModified(originQuery, query, false)) {
self.page = parseInt(self.query[pageField || 'page'], 10); if (query[pageField || 'page']) {
} self.page = parseInt(query[pageField || 'page'], 10);
}
if (self.query[perPageField || 'perPage']) { if (query[perPageField || 'perPage']) {
self.perPage = parseInt(self.query[perPageField || 'perPage'], 10); self.perPage = parseInt(query[perPageField || 'perPage'], 10);
} }
updater && self.query = query;
isObjectShallowModified(originQuery, self.query, false) && updater && setTimeout(updater.bind(null, `?${qsstringify(query)}`), 4);
setTimeout(updater.bind(null, `?${qsstringify(self.query)}`), 4); }
} }
const fetchInitData: ( const fetchInitData: (

View File

@ -49,7 +49,7 @@ export const FormStore = ServiceStore.named('FormStore')
while (pool.length) { while (pool.length) {
const current = pool.shift()!; const current = pool.shift()!;
if (current.storeType === 'FormItemStore') { if (current.storeType === 'FormItemStore' && !current.isControlled) {
formItems.push(current); formItems.push(current);
} else if ( } else if (
!['ComboStore', 'TableStore', 'FormStore'].includes(current.storeType) !['ComboStore', 'TableStore', 'FormStore'].includes(current.storeType)

View File

@ -67,6 +67,7 @@ const getSelectedOptionsCache: any = {
export const FormItemStore = StoreNode.named('FormItemStore') export const FormItemStore = StoreNode.named('FormItemStore')
.props({ .props({
isFocused: false, isFocused: false,
isControlled: false, // 是否是受控表单项,通常是用在别的组件里面
type: '', type: '',
label: '', label: '',
unique: false, unique: false,
@ -107,7 +108,9 @@ export const FormItemStore = StoreNode.named('FormItemStore')
resetValue: types.optional(types.frozen(), ''), resetValue: types.optional(types.frozen(), ''),
validateOnChange: false, validateOnChange: false,
/** 当前表单项所属的InputGroup父元素, 用于收集InputGroup的子元素 */ /** 当前表单项所属的InputGroup父元素, 用于收集InputGroup的子元素 */
inputGroupControl: types.optional(types.frozen(), {}) inputGroupControl: types.optional(types.frozen(), {}),
colIndex: types.frozen(),
rowIndex: types.frozen()
}) })
.views(self => { .views(self => {
function getForm(): any { function getForm(): any {
@ -358,28 +361,30 @@ export const FormItemStore = StoreNode.named('FormItemStore')
inputGroupControl?.name != null && inputGroupControl?.name != null &&
(self.inputGroupControl = inputGroupControl); (self.inputGroupControl = inputGroupControl);
rules = { if (typeof rules !== 'undefined' || self.required) {
...rules, rules = {
isRequired: self.required || rules?.isRequired ...rules,
}; isRequired: self.required || rules?.isRequired
};
// todo 这个弄个配置由渲染器自己来决定 // todo 这个弄个配置由渲染器自己来决定
// 暂时先这样 // 暂时先这样
if (~['input-text', 'textarea'].indexOf(self.type)) { if (~['input-text', 'textarea'].indexOf(self.type)) {
if (typeof minLength === 'number') { if (typeof minLength === 'number') {
rules.minLength = minLength; rules.minLength = minLength;
}
if (typeof maxLength === 'number') {
rules.maxLength = maxLength;
}
} }
if (typeof maxLength === 'number') { if (isObjectShallowModified(rules, self.rules)) {
rules.maxLength = maxLength; self.rules = rules;
clearError('builtin');
self.validated = false;
} }
} }
if (isObjectShallowModified(rules, self.rules)) {
self.rules = rules;
clearError('builtin');
self.validated = false;
}
} }
function focus() { function focus() {
@ -1334,6 +1339,10 @@ export const FormItemStore = StoreNode.named('FormItemStore')
} }
} }
function setIsControlled(value: any) {
self.isControlled = !!value;
}
return { return {
focus, focus,
blur, blur,
@ -1359,7 +1368,8 @@ export const FormItemStore = StoreNode.named('FormItemStore')
changeEmitedValue, changeEmitedValue,
addSubFormItem, addSubFormItem,
removeSubFormItem, removeSubFormItem,
loadAutoUpdateData loadAutoUpdateData,
setIsControlled
}; };
}); });

View File

@ -1,7 +1,11 @@
import {Instance, types} from 'mobx-state-tree'; import {Instance, types} from 'mobx-state-tree';
import {parseQuery} from '../utils/helper'; import {parseQuery} from '../utils/helper';
import {ServiceStore} from './service'; import {ServiceStore} from './service';
import {createObjectFromChain, extractObjectChain} from '../utils'; import {
createObjectFromChain,
extractObjectChain,
isObjectShallowModified
} from '../utils';
export const RootStore = ServiceStore.named('RootStore') export const RootStore = ServiceStore.named('RootStore')
.props({ .props({
@ -42,7 +46,10 @@ export const RootStore = ServiceStore.named('RootStore')
self.runtimeErrorStack = errorStack; self.runtimeErrorStack = errorStack;
}, },
updateLocation(location?: any, parseFn?: Function) { updateLocation(location?: any, parseFn?: Function) {
self.query = parseFn ? parseFn(location) : parseQuery(location); const query = parseFn ? parseFn(location) : parseQuery(location);
if (isObjectShallowModified(query, self.query, false)) {
self.query = query;
}
} }
})); }));

View File

@ -111,6 +111,7 @@ 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, expandable: false,
checkdisable: false, checkdisable: false,
@ -119,7 +120,9 @@ export const Row = types
types.array(types.late((): IAnyModelType => Row)), types.array(types.late((): IAnyModelType => Row)),
[] []
), ),
depth: types.number // 当前children位于第几层便于使用getParent获取最顶层TableStore depth: types.number, // 当前children位于第几层便于使用getParent获取最顶层TableStore
appeared: true,
lazyRender: false
}) })
.views(self => ({ .views(self => ({
get checked(): boolean { get checked(): boolean {
@ -321,6 +324,10 @@ export const Row = types
index++; index++;
} }
} }
},
markAppeared(value: any) {
value && (self.appeared = !!value);
} }
})); }));
@ -344,6 +351,8 @@ export const TableStore = iRendererStore
), ),
'asc' 'asc'
), ),
loading: false,
canAccessSuperData: false,
draggable: false, draggable: false,
dragging: false, dragging: false,
selectable: false, selectable: false,
@ -365,7 +374,8 @@ export const TableStore = iRendererStore
keepItemSelectionOnPageChange: false, keepItemSelectionOnPageChange: false,
// 导出 Excel 按钮的 loading 状态 // 导出 Excel 按钮的 loading 状态
exportExcelLoading: false, exportExcelLoading: false,
searchFormExpanded: false // 用来控制搜索框是否展开了,那个自动根据 searchable 生成的表单 autoGenerateFilter searchFormExpanded: false, // 用来控制搜索框是否展开了,那个自动根据 searchable 生成的表单 autoGenerateFilter
lazyRenderAfter: 100
}) })
.views(self => { .views(self => {
function getColumnsExceptBuiltinTypes() { function getColumnsExceptBuiltinTypes() {
@ -688,10 +698,12 @@ export const TableStore = iRendererStore
return getUnSelectedRows(); return getUnSelectedRows();
}, },
get falttenedRows() {
return flattenTree<IRow>(self.rows);
},
get checkableRows() { get checkableRows() {
return flattenTree<IRow>(self.rows).filter( return this.falttenedRows.filter((item: IRow) => item.checkable);
(item: IRow) => item.checkable
);
}, },
get expandableRows() { get expandableRows() {
@ -821,6 +833,10 @@ export const TableStore = iRendererStore
style.right = right; style.right = right;
} }
return [style, stickyClassName]; return [style, stickyClassName];
},
get items() {
return self.rows.concat();
} }
}; };
}) })
@ -832,46 +848,60 @@ export const TableStore = iRendererStore
} }
function update(config: Partial<STableStore>) { function update(config: Partial<STableStore>) {
config.primaryField !== void 0 && config.primaryField !== undefined &&
(self.primaryField = config.primaryField); (self.primaryField = config.primaryField);
config.selectable !== void 0 && (self.selectable = config.selectable); config.selectable !== undefined && (self.selectable = config.selectable);
config.columnsTogglable !== void 0 && config.columnsTogglable !== undefined &&
(self.columnsTogglable = config.columnsTogglable); (self.columnsTogglable = config.columnsTogglable);
config.draggable !== void 0 && (self.draggable = config.draggable); config.draggable !== undefined && (self.draggable = config.draggable);
if (typeof config.orderBy === 'string') { if (
typeof config.orderBy === 'string' ||
typeof config.orderDir === 'string'
) {
setOrderByInfo( setOrderByInfo(
config.orderBy, config.orderBy ?? self.orderBy,
config.orderDir === 'desc' ? 'desc' : 'asc' config.orderDir !== undefined
? config.orderDir === 'desc'
? 'desc'
: 'asc'
: self.orderDir
); );
} }
config.multiple !== void 0 && (self.multiple = config.multiple); config.multiple !== undefined && (self.multiple = config.multiple);
config.footable !== void 0 && (self.footable = config.footable); config.footable !== undefined && (self.footable = config.footable);
config.expandConfig !== void 0 && config.expandConfig !== undefined &&
(self.expandConfig = config.expandConfig); (self.expandConfig = config.expandConfig);
config.itemCheckableOn !== void 0 && config.itemCheckableOn !== undefined &&
(self.itemCheckableOn = config.itemCheckableOn); (self.itemCheckableOn = config.itemCheckableOn);
config.itemDraggableOn !== void 0 && config.itemDraggableOn !== undefined &&
(self.itemDraggableOn = config.itemDraggableOn); (self.itemDraggableOn = config.itemDraggableOn);
config.hideCheckToggler !== void 0 && config.hideCheckToggler !== undefined &&
(self.hideCheckToggler = !!config.hideCheckToggler); (self.hideCheckToggler = !!config.hideCheckToggler);
config.combineNum !== void 0 && config.combineNum !== undefined &&
(self.combineNum = parseInt(config.combineNum as any, 10) || 0); (self.combineNum = parseInt(config.combineNum as any, 10) || 0);
config.combineFromIndex !== void 0 && config.combineFromIndex !== undefined &&
(self.combineFromIndex = (self.combineFromIndex =
parseInt(config.combineFromIndex as any, 10) || 0); parseInt(config.combineFromIndex as any, 10) || 0);
config.maxKeepItemSelectionLength !== void 0 && config.maxKeepItemSelectionLength !== undefined &&
(self.maxKeepItemSelectionLength = config.maxKeepItemSelectionLength); (self.maxKeepItemSelectionLength = config.maxKeepItemSelectionLength);
config.keepItemSelectionOnPageChange !== void 0 && config.keepItemSelectionOnPageChange !== undefined &&
(self.keepItemSelectionOnPageChange = (self.keepItemSelectionOnPageChange =
config.keepItemSelectionOnPageChange); config.keepItemSelectionOnPageChange);
config.exportExcelLoading !== undefined && config.exportExcelLoading !== undefined &&
(self.exportExcelLoading = config.exportExcelLoading); (self.exportExcelLoading = config.exportExcelLoading);
config.loading !== undefined && (self.loading = config.loading);
config.canAccessSuperData !== undefined &&
(self.canAccessSuperData = !!config.canAccessSuperData);
typeof config.lazyRenderAfter === 'number' &&
self.lazyRenderAfter === config.lazyRenderAfter;
if (config.columns && Array.isArray(config.columns)) { if (config.columns && Array.isArray(config.columns)) {
let columns: Array<SColumn> = config.columns let columns: Array<SColumn> = config.columns
.filter(column => column) .filter(column => column)
@ -1112,7 +1142,8 @@ export const TableStore = iRendererStore
depth: number, depth: number,
pindex: number, pindex: number,
parentId: string, parentId: string,
path: string = '' path: string = '',
nThRef: {index: number}
): any { ): any {
depth += 1; depth += 1;
return children.map((item, index) => { return children.map((item, index) => {
@ -1131,6 +1162,7 @@ export const TableStore = iRendererStore
path: `${path}${index}`, path: `${path}${index}`,
depth: depth, depth: depth,
index: index, index: index,
nth: nThRef.index++,
newIndex: index, newIndex: index,
pristine: item, pristine: item,
data: item, data: item,
@ -1142,7 +1174,8 @@ export const TableStore = iRendererStore
depth, depth,
index, index,
id, id,
`${path}${index}.` `${path}${index}.`,
nThRef
) )
: [], : [],
expandable: !!( expandable: !!(
@ -1164,6 +1197,7 @@ 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 = {
@ -1180,6 +1214,7 @@ 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}`,
@ -1187,7 +1222,7 @@ export const TableStore = iRendererStore
rowSpans: {}, rowSpans: {},
children: children:
item && Array.isArray(item.children) item && Array.isArray(item.children)
? initChildren(item.children, 1, index, id, `${index}.`) ? initChildren(item.children, 1, index, id, `${index}.`, nThRef)
: [], : [],
expandable: !!( expandable: !!(
(item && Array.isArray(item.children) && item.children.length) || (item && Array.isArray(item.children) && item.children.length) ||
@ -1208,6 +1243,21 @@ 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);
// 前 20 个直接渲染,后面的按需渲染
if (
self.lazyRenderAfter &&
self.falttenedRows.length > self.lazyRenderAfter
) {
for (
let i = self.lazyRenderAfter, len = self.falttenedRows.length;
i < len;
i++
) {
self.falttenedRows[i].appeared = false;
self.falttenedRows[i].lazyRender = true;
}
}
const expand = self.footable && self.footable.expand; const expand = self.footable && self.footable.expand;
if ( if (
expand === 'first' || expand === 'first' ||

View File

@ -502,3 +502,30 @@ export function warning(cat: Category, msg: string, ext?: object) {
console.groupEnd(); console.groupEnd();
store.logs.push(log); store.logs.push(log);
} }
// 辅助定位是因为什么属性变化导致了组件更新
export function traceProps(props: any, prevProps: any, componentName: string) {
console.log(
componentName,
Object.keys(props)
.map(key => {
if (props[key] !== prevProps[key]) {
if (key === 'data') {
return `data[${Object.keys(props[key])
.map(item => {
if (props[key][item] !== prevProps[key][item]) {
return `data.${item}`;
}
return '';
})
.filter(item => item)
.join(', ')}]`;
}
return key;
}
return '';
})
.filter(item => item)
);
}

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import {isObservable, isObservableArray} from 'mobx'; import {isObservable, isObservableArray} from 'mobx';
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq';
import last from 'lodash/last' import last from 'lodash/last';
import merge from 'lodash/merge' import merge from 'lodash/merge';
import isPlainObject from 'lodash/isPlainObject' import isPlainObject from 'lodash/isPlainObject';
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual';
import isNaN from 'lodash/isNaN' import isNaN from 'lodash/isNaN';
import isNumber from 'lodash/isNumber' import isNumber from 'lodash/isNumber';
import isString from 'lodash/isString' import isString from 'lodash/isString';
import qs from 'qs'; import qs from 'qs';
import type {Schema, PlainObject, FunctionPropertyNames} from '../types'; import type {Schema, PlainObject, FunctionPropertyNames} from '../types';
@ -196,9 +196,37 @@ export function anyChanged(
to: {[propName: string]: any}, to: {[propName: string]: any},
strictMode: boolean = true strictMode: boolean = true
): boolean { ): boolean {
return (typeof attrs === 'string' ? attrs.split(/\s*,\s*/) : attrs).some( return (
key => (strictMode ? from[key] !== to[key] : from[key] != to[key]) typeof attrs === 'string'
); ? attrs.split(',').map(item => item.trim())
: attrs
).some(key => (strictMode ? from[key] !== to[key] : from[key] != to[key]));
}
type Mutable<T> = {
-readonly [k in keyof T]: T[k];
};
export function changedEffect<T extends Record<string, any>>(
attrs: string | Array<string>,
origin: T,
data: T,
effect: (changes: Partial<Mutable<T>>) => void,
strictMode: boolean = true
) {
const changes: Partial<T> = {};
const keys =
typeof attrs === 'string'
? attrs.split(',').map(item => item.trim())
: attrs;
keys.forEach(key => {
if (strictMode ? origin[key] !== data[key] : origin[key] != data[key]) {
(changes as any)[key] = data[key];
}
});
Object.keys(changes).length && effect(changes);
} }
export function rmUndefined(obj: PlainObject) { export function rmUndefined(obj: PlainObject) {

View File

@ -15,4 +15,8 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
> .visibility-sensor {
height: 100%; // 修复图表高度为 0 visibility-sensor 无法触发的问题
}
} }

View File

@ -1027,6 +1027,13 @@
} }
} }
} }
// table 骨架样式
&-emptyBlock {
background-color: #eaebed;
border-radius: 5px;
line-height: 15px;
}
} }
.#{$ns}InputTable { .#{$ns}InputTable {
@ -1074,6 +1081,7 @@
> .#{$ns}Button, > .#{$ns}Button,
> .#{$ns}Button--disabled-wrap > .#{$ns}Button { > .#{$ns}Button--disabled-wrap > .#{$ns}Button {
margin: px2rem(3px); margin: px2rem(3px);
height: auto;
} }
> .#{$ns}Button--disabled-wrap > .#{$ns}Button--link { > .#{$ns}Button--disabled-wrap > .#{$ns}Button--link {

View File

@ -440,7 +440,7 @@ exports[`Renderer:input table add 1`] = `
/> />
<col <col
data-index="5" data-index="5"
style="width: 100px;" style="width: 150px;"
/> />
</colgroup> </colgroup>
<thead> <thead>
@ -520,34 +520,24 @@ exports[`Renderer:input table add 1`] = `
class="" class=""
> >
<div <div
class="cxd-Form cxd-Form--normal cxd-Form--quickEdit" class="cxd-Form-item cxd-Form-item--normal"
novalidate="" data-role="form-item"
> >
<input
style="display: none;"
type="submit"
value="aa"
/>
<div <div
class="cxd-Form-item cxd-Form-item--normal" class="cxd-Form-control cxd-TextControl"
data-role="form-item"
> >
<div <div
class="cxd-Form-control cxd-TextControl" class="cxd-TextControl-input"
> >
<div <input
class="cxd-TextControl-input" autocomplete="off"
> class=""
<input name="a"
autocomplete="off" placeholder=""
class="" size="10"
name="a" type="text"
placeholder="" value="aa"
size="10" />
type="text"
value="bb"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -556,33 +546,24 @@ exports[`Renderer:input table add 1`] = `
class="" class=""
> >
<div <div
class="cxd-Form cxd-Form--normal cxd-Form--quickEdit" class="cxd-Form-item cxd-Form-item--normal"
novalidate="" data-role="form-item"
> >
<input
style="display: none;"
type="submit"
/>
<div <div
class="cxd-Form-item cxd-Form-item--normal" class="cxd-Form-control cxd-TextControl"
data-role="form-item"
> >
<div <div
class="cxd-Form-control cxd-TextControl" class="cxd-TextControl-input"
> >
<div <input
class="cxd-TextControl-input" autocomplete="off"
> class=""
<input name="b"
autocomplete="off" placeholder=""
class="" size="10"
name="b" type="text"
placeholder="" value="bb"
size="10" />
type="text"
value=""
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -867,74 +848,65 @@ exports[`Renderer:input-table cell selects delete 1`] = `
class="" class=""
> >
<div <div
class="cxd-Form cxd-Form--normal cxd-Form--quickEdit" class="cxd-Form-item cxd-Form-item--normal"
novalidate="" data-role="form-item"
> >
<input
style="display: none;"
type="submit"
/>
<div <div
class="cxd-Form-item cxd-Form-item--normal" class="cxd-SelectControl cxd-Form-control"
data-role="form-item"
> >
<div <div
class="cxd-SelectControl cxd-Form-control" aria-expanded="false"
aria-haspopup="listbox"
class="cxd-Select cxd-Select--multi"
role="combobox"
tabindex="0"
> >
<div <div
aria-expanded="false" class="cxd-Select-valueWrap"
aria-haspopup="listbox"
class="cxd-Select cxd-Select--multi"
role="combobox"
tabindex="0"
> >
<div <div
class="cxd-Select-valueWrap" class="cxd-Select-value"
> >
<div <span
class="cxd-Select-value" class="cxd-Select-valueLabel"
> >
<span s2
class="cxd-Select-valueLabel" </span>
> <span
s2 class="cxd-Select-valueIcon"
</span>
<span
class="cxd-Select-valueIcon"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</span>
</div>
<div
class="cxd-Select-value"
> >
<span <icon-mock
class="cxd-Select-valueLabel" classname="icon icon-close"
> icon="close"
s3 />
</span> </span>
<span
class="cxd-Select-valueIcon"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</span>
</div>
</div> </div>
<span <div
class="cxd-Select-arrow" class="cxd-Select-value"
> >
<icon-mock <span
classname="icon icon-right-arrow-bold" class="cxd-Select-valueLabel"
icon="right-arrow-bold" >
/> s3
</span> </span>
<span
class="cxd-Select-valueIcon"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</span>
</div>
</div> </div>
<span
class="cxd-Select-arrow"
>
<icon-mock
classname="icon icon-right-arrow-bold"
icon="right-arrow-bold"
/>
</span>
</div> </div>
</div> </div>
</div> </div>
@ -1265,128 +1237,119 @@ exports[`Renderer:input-table with combo column 1`] = `
class="" class=""
> >
<div <div
class="cxd-Form cxd-Form--normal cxd-Form--quickEdit" class="cxd-Form-item cxd-Form-item--normal"
novalidate="" data-role="form-item"
> >
<input
style="display: none;"
type="submit"
/>
<div <div
class="cxd-Form-item cxd-Form-item--normal" class="cxd-ComboControl cxd-Form-control"
data-role="form-item"
> >
<div <div
class="cxd-ComboControl cxd-Form-control" class="cxd-Combo cxd-Combo--single cxd-Combo--hor"
> >
<div <div
class="cxd-Combo cxd-Combo--single cxd-Combo--hor" class="cxd-Combo-item"
> >
<div <div
class="cxd-Combo-item" class="cxd-Combo-itemInner"
> >
<div <div
class="cxd-Combo-itemInner" class="cxd-Form cxd-Form--row cxd-Combo-form"
novalidate=""
> >
<input
style="display: none;"
type="submit"
/>
<div <div
class="cxd-Form cxd-Form--row cxd-Combo-form" class="cxd-Form-row"
novalidate=""
> >
<input
style="display: none;"
type="submit"
/>
<div <div
class="cxd-Form-row" class="cxd-Form-col"
> >
<div <div
class="cxd-Form-col" class="cxd-Form-item cxd-Form-item--row"
data-role="form-item"
> >
<div <div
class="cxd-Form-item cxd-Form-item--row" class="cxd-Form-rowInner"
data-role="form-item"
> >
<div <div
class="cxd-Form-rowInner" class="cxd-NumberControl cxd-Form-control"
> >
<div <div
class="cxd-NumberControl cxd-Form-control" class="cxd-Number cxd-Number--borderFull"
> >
<div <div
class="cxd-Number cxd-Number--borderFull" class="cxd-Number-handler-wrap"
> >
<div <span
class="cxd-Number-handler-wrap" aria-disabled="false"
aria-label="Increase Value"
class="cxd-Number-handler cxd-Number-handler-up"
role="button"
unselectable="on"
> >
<span <span
aria-disabled="false" class="cxd-Number-handler-up-inner"
aria-label="Increase Value"
class="cxd-Number-handler cxd-Number-handler-up"
role="button"
unselectable="on" unselectable="on"
>
<span
class="cxd-Number-handler-up-inner"
unselectable="on"
/>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="cxd-Number-handler cxd-Number-handler-down"
role="button"
unselectable="on"
>
<span
class="cxd-Number-handler-down-inner"
unselectable="on"
/>
</span>
</div>
<div
class="cxd-Number-input-wrap"
>
<input
aria-valuenow="88"
autocomplete="off"
class="cxd-Number-input"
placeholder="请手动输入分数"
role="spinbutton"
step="1"
value="88"
/> />
</div> </span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="cxd-Number-handler cxd-Number-handler-down"
role="button"
unselectable="on"
>
<span
class="cxd-Number-handler-down-inner"
unselectable="on"
/>
</span>
</div>
<div
class="cxd-Number-input-wrap"
>
<input
aria-valuenow="88"
autocomplete="off"
class="cxd-Number-input"
placeholder="请手动输入分数"
role="spinbutton"
step="1"
value="88"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div
class="cxd-Form-col"
>
<div <div
class="cxd-Form-col" class="cxd-Form-item cxd-Form-item--row"
data-role="form-item"
> >
<div <div
class="cxd-Form-item cxd-Form-item--row" class="cxd-Form-rowInner"
data-role="form-item"
> >
<div <div
class="cxd-Form-rowInner" class="cxd-Form-control cxd-TextControl"
> >
<div <div
class="cxd-Form-control cxd-TextControl" class="cxd-TextControl-input"
> >
<div <input
class="cxd-TextControl-input" autocomplete="off"
> class=""
<input name="comment"
autocomplete="off" placeholder="请手动输入意见"
class="" size="10"
name="comment" type="text"
placeholder="请手动输入意见" value="this is comment msg!!"
size="10" />
type="text"
value="this is comment msg!!"
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -72,7 +72,8 @@
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"video-react": "0.15.0", "video-react": "0.15.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5",
"react-intersection-observer": "9.5.2"
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-free": "^6.1.1",

View File

@ -58,6 +58,7 @@ import {
import type {PaginationProps} from './Pagination'; import type {PaginationProps} from './Pagination';
import {isAlive} from 'mobx-state-tree'; import {isAlive} from 'mobx-state-tree';
import isPlainObject from 'lodash/isPlainObject'; import isPlainObject from 'lodash/isPlainObject';
import memoize from 'lodash/memoize';
export type CRUDBultinToolbarType = export type CRUDBultinToolbarType =
| 'columns-toggler' | 'columns-toggler'
@ -473,6 +474,10 @@ export default class CRUD extends React.Component<CRUDProps, any> {
/** 父容器, 主要用于定位CRUD内部popover的挂载点 */ /** 父容器, 主要用于定位CRUD内部popover的挂载点 */
parentContainer: Element | null; parentContainer: Element | null;
filterOnEvent = memoize(onEvent =>
omitBy(onEvent, (event, key: any) => !INNER_EVENTS.includes(key))
);
constructor(props: CRUDProps) { constructor(props: CRUDProps) {
super(props); super(props);
@ -548,8 +553,8 @@ export default class CRUD extends React.Component<CRUDProps, any> {
} }
componentDidMount() { componentDidMount() {
const {store, autoGenerateFilter, columns} = this.props; const {store, autoGenerateFilter, perPageField, columns} = this.props;
if (this.props.perPage) { if (this.props.perPage && !store.query[perPageField || 'perPage']) {
store.changePage(store.page, this.props.perPage); store.changePage(store.page, this.props.perPage);
} }
@ -662,6 +667,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
componentWillUnmount() { componentWillUnmount() {
this.mounted = false; this.mounted = false;
clearTimeout(this.timer); clearTimeout(this.timer);
this.filterOnEvent.cache.clear?.();
} }
/** 查找CRUD最近层级的父窗口 */ /** 查找CRUD最近层级的父窗口 */
@ -2451,10 +2457,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
...rest, ...rest,
// 通用事件 例如cus-event 如果直接透传给table 则会被触发2次 // 通用事件 例如cus-event 如果直接透传给table 则会被触发2次
// 因此只将下层组件table、cards中自定义事件透传下去 否则通过crud配置了也不会执行 // 因此只将下层组件table、cards中自定义事件透传下去 否则通过crud配置了也不会执行
onEvent: omitBy( onEvent: this.filterOnEvent(onEvent),
onEvent,
(event, key: any) => !INNER_EVENTS.includes(key)
),
columns: store.columns ?? rest.columns, columns: store.columns ?? rest.columns,
type: mode || 'table' type: mode || 'table'
}, },

View File

@ -19,7 +19,6 @@ import {
ApiObject, ApiObject,
autobind, autobind,
isExpression, isExpression,
ITableStore,
isPureVariable, isPureVariable,
resolveVariableAndFilter, resolveVariableAndFilter,
getRendererByName, getRendererByName,
@ -283,9 +282,9 @@ export default class FormTable extends React.Component<TableProps, TableState> {
entries: SimpleMap<any, number>; entries: SimpleMap<any, number>;
entityId: number = 1; entityId: number = 1;
subForms: any = {}; subForms: any = {};
subFormItems: any = {};
rowPrinstine: Array<any> = []; rowPrinstine: Array<any> = [];
editting: any = {}; editting: any = {};
tableStore?: ITableStore;
constructor(props: TableProps) { constructor(props: TableProps) {
super(props); super(props);
@ -305,6 +304,7 @@ export default class FormTable extends React.Component<TableProps, TableState> {
this.handleRadioChange = this.handleRadioChange.bind(this); this.handleRadioChange = this.handleRadioChange.bind(this);
this.getEntryId = this.getEntryId.bind(this); this.getEntryId = this.getEntryId.bind(this);
this.subFormRef = this.subFormRef.bind(this); this.subFormRef = this.subFormRef.bind(this);
this.subFormItemRef = this.subFormItemRef.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.emitValue = this.emitValue.bind(this); this.emitValue = this.emitValue.bind(this);
} }
@ -382,6 +382,10 @@ export default class FormTable extends React.Component<TableProps, TableState> {
this.subForms[`${x}-${y}`] = form; this.subForms[`${x}-${y}`] = form;
} }
subFormItemRef(form: any, x: number, y: number) {
this.subFormItems[`${x}-${y}`] = form;
}
async validate(): Promise<string | void> { async validate(): Promise<string | void> {
const {value, translate: __, columns} = this.props; const {value, translate: __, columns} = this.props;
const minLength = this.resolveVariableProps(this.props, 'minLength'); const minLength = this.resolveVariableProps(this.props, 'minLength');
@ -442,16 +446,18 @@ export default class FormTable extends React.Component<TableProps, TableState> {
} }
} }
if (!this.tableStore) return;
// 校验子项 // 校验子项
const children = this.tableStore.children.filter( const subFormItemss: Array<any> = [];
item => item?.storeType === 'FormItemStore' Object.keys(this.subFormItems).forEach(
key =>
this.subFormItems[key] && subFormItemss.push(this.subFormItems[key])
); );
const results = await Promise.all( const results = await Promise.all(
children.map(item => item.validate(this.props.value)) subFormItemss.map(item => item.props.onValidate())
); );
let msg = ~results.indexOf(false) ? __('Form.validateFailed') : '';
return msg;
} }
async emitValue() { async emitValue() {
@ -728,6 +734,12 @@ export default class FormTable extends React.Component<TableProps, TableState> {
); );
subForms.forEach(form => form.flush()); subForms.forEach(form => form.flush());
const subFormItems: Array<any> = [];
Object.keys(this.subFormItems).forEach(
key => this.subFormItems[key] && subFormItems.push(this.subFormItems[key])
);
subFormItems.forEach(item => item.props.onFlushChange?.());
const validateForms: Array<any> = []; const validateForms: Array<any> = [];
Object.keys(this.subForms).forEach(key => { Object.keys(this.subForms).forEach(key => {
const arr = key.split('-'); const arr = key.split('-');
@ -1229,7 +1241,7 @@ export default class FormTable extends React.Component<TableProps, TableState> {
label: __('Table.operation'), label: __('Table.operation'),
className: 'v-middle nowrap', className: 'v-middle nowrap',
fixed: 'right', fixed: 'right',
width: 100, width: 150,
innerClassName: 'm-n' innerClassName: 'm-n'
}; };
columns.push(operation); columns.push(operation);
@ -1468,7 +1480,6 @@ export default class FormTable extends React.Component<TableProps, TableState> {
while (ref && ref.getWrappedInstance) { while (ref && ref.getWrappedInstance) {
ref = ref.getWrappedInstance(); ref = ref.getWrappedInstance();
} }
this.tableStore = ref?.props?.store;
} }
computedAddBtnDisabled() { computedAddBtnDisabled() {
@ -1553,6 +1564,7 @@ export default class FormTable extends React.Component<TableProps, TableState> {
onSaveOrder: this.handleSaveTableOrder, onSaveOrder: this.handleSaveTableOrder,
buildItemProps: this.buildItemProps, buildItemProps: this.buildItemProps,
quickEditFormRef: this.subFormRef, quickEditFormRef: this.subFormRef,
quickEditFormItemRef: this.subFormItemRef,
columnsTogglable: columnsTogglable, columnsTogglable: columnsTogglable,
combineNum: combineNum, combineNum: combineNum,
combineFromIndex: combineFromIndex, combineFromIndex: combineFromIndex,

View File

@ -5,7 +5,7 @@
import React from 'react'; import React from 'react';
import {findDOMNode} from 'react-dom'; import {findDOMNode} from 'react-dom';
import {RendererProps, noop} from 'amis-core'; import {RendererProps, getRendererByName, noop} from 'amis-core';
import hoistNonReactStatic from 'hoist-non-react-statics'; import hoistNonReactStatic from 'hoist-non-react-statics';
import {ActionObject} from 'amis-core'; import {ActionObject} from 'amis-core';
import keycode from 'keycode'; import keycode from 'keycode';
@ -121,8 +121,10 @@ export const HocQuickEdit =
this.handleWindowKeyPress = this.handleWindowKeyPress.bind(this); this.handleWindowKeyPress = this.handleWindowKeyPress.bind(this);
this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this); this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this);
this.formRef = this.formRef.bind(this); this.formRef = this.formRef.bind(this);
this.formItemRef = this.formItemRef.bind(this);
this.handleInit = this.handleInit.bind(this); this.handleInit = this.handleInit.bind(this);
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleFormItemChange = this.handleFormItemChange.bind(this);
this.state = { this.state = {
isOpened: false isOpened: false
@ -152,6 +154,17 @@ export const HocQuickEdit =
quickEditFormRef(ref, colIndex, rowIndex); quickEditFormRef(ref, colIndex, rowIndex);
} }
} }
formItemRef(ref: any) {
const {quickEditFormItemRef, rowIndex, colIndex} = this.props;
if (quickEditFormItemRef) {
while (ref && ref.getWrappedInstance) {
ref = ref.getWrappedInstance();
}
quickEditFormItemRef(ref, colIndex, rowIndex);
}
}
handleWindowKeyPress(e: Event) { handleWindowKeyPress(e: Event) {
const ns = this.props.classPrefix; const ns = this.props.classPrefix;
@ -337,6 +350,17 @@ export const HocQuickEdit =
); );
} }
handleFormItemChange(value: any) {
const {onQuickChange, quickEdit, name} = this.props;
onQuickChange(
{[name!]: value},
(quickEdit as QuickEditConfig).saveImmediately,
false,
quickEdit as QuickEditConfig
);
}
openQuickEdit() { openQuickEdit() {
currentOpened = this; currentOpened = this;
this.setState({ this.setState({
@ -526,6 +550,51 @@ export const HocQuickEdit =
); );
} }
renderInlineForm() {
const {
render,
classnames: cx,
canAccessSuperData,
disabled,
value,
name
} = this.props;
const schema: any = this.buildSchema();
// 有且只有一个表单项时,直接渲染表单项
if (
Array.isArray(schema.body) &&
schema.body.length === 1 &&
!schema.body[0].unique && // 唯一模式还不支持
!schema.body[0].value && // 不能有默认值表达式什么的情况
schema.body[0].name &&
schema.body[0].name === name &&
schema.body[0].type &&
getRendererByName(schema.body[0].type)?.isFormItem
) {
return render('inline-form-item', schema.body[0], {
mode: 'normal',
value: value || '',
onChange: this.handleFormItemChange,
ref: this.formItemRef
});
}
return render('inline-form', schema, {
value: undefined,
wrapperComponent: 'div',
className: cx('Form--quickEdit'),
ref: this.formRef,
simpleMode: true,
onInit: this.handleInit,
onChange: this.handleChange,
formLazyChange: false,
canAccessSuperData,
disabled
});
}
render() { render() {
const { const {
onQuickChange, onQuickChange,
@ -556,20 +625,7 @@ export const HocQuickEdit =
(quickEdit as QuickEditConfig).isFormMode (quickEdit as QuickEditConfig).isFormMode
) { ) {
return ( return (
<Component {...this.props}> <Component {...this.props}>{this.renderInlineForm()}</Component>
{render('inline-form', this.buildSchema(), {
value: undefined,
wrapperComponent: 'div',
className: cx('Form--quickEdit'),
ref: this.formRef,
simpleMode: true,
onInit: this.handleInit,
onChange: this.handleChange,
formLazyChange: false,
canAccessSuperData,
disabled
})}
</Component>
); );
} else { } else {
return ( return (

View File

@ -2,7 +2,7 @@ import React from 'react';
import {ClassNamesFn, RendererEvent} from 'amis-core'; import {ClassNamesFn, RendererEvent} from 'amis-core';
import {SchemaNode, ActionObject} from 'amis-core'; import {SchemaNode, ActionObject} from 'amis-core';
import {TableRow} from './TableRow'; import TableRow from './TableRow';
import {filter} from 'amis-core'; import {filter} from 'amis-core';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {trace, reaction} from 'mobx'; import {trace, reaction} from 'mobx';
@ -91,7 +91,8 @@ export class TableBody extends React.Component<TableBodyProps> {
onRowClick, onRowClick,
onRowDbClick, onRowDbClick,
onRowMouseEnter, onRowMouseEnter,
onRowMouseLeave onRowMouseLeave,
store
} = this.props; } = this.props;
return rows.map((item: IRow, rowIndex: number) => { return rows.map((item: IRow, rowIndex: number) => {
@ -99,6 +100,7 @@ export class TableBody extends React.Component<TableBodyProps> {
const doms = [ const doms = [
<TableRow <TableRow
{...itemProps} {...itemProps}
store={store}
itemAction={itemAction} itemAction={itemAction}
classnames={cx} classnames={cx}
checkOnItemClick={checkOnItemClick} checkOnItemClick={checkOnItemClick}
@ -136,6 +138,7 @@ export class TableBody extends React.Component<TableBodyProps> {
doms.push( doms.push(
<TableRow <TableRow
{...itemProps} {...itemProps}
store={store}
itemAction={itemAction} itemAction={itemAction}
classnames={cx} classnames={cx}
checkOnItemClick={checkOnItemClick} checkOnItemClick={checkOnItemClick}

View File

@ -81,53 +81,60 @@ export interface TableContentProps extends LocaleProps {
dispatchEvent?: Function; dispatchEvent?: Function;
onEvent?: OnEventProps; onEvent?: OnEventProps;
loading?: boolean; loading?: boolean;
columnWidthReady?: boolean;
// 以下纯粹是为了监控
someChecked?: boolean;
allChecked?: boolean;
isSelectionThresholdReached?: boolean;
orderBy?: string;
orderDir?: string;
} }
@observer export function renderItemActions(
export class TableContent extends React.Component<TableContentProps> { props: Pick<
static renderItemActions( TableContentProps,
props: Pick< 'itemActions' | 'render' | 'store' | 'classnames'
TableContentProps, >
'itemActions' | 'render' | 'store' | 'classnames' ) {
> const {itemActions, render, store, classnames: cx} = props;
) {
const {itemActions, render, store, classnames: cx} = props;
if (!store.hoverRow) { if (!store.hoverRow) {
return null; return null;
}
const finalActions = Array.isArray(itemActions)
? itemActions.filter(action => !action.hiddenOnHover)
: [];
if (!finalActions.length) {
return null;
}
return (
<ItemActionsWrapper store={store} classnames={cx}>
<div className={cx('Table-itemActions')}>
{finalActions.map((action, index) =>
render(
`itemAction/${index}`,
{
...(action as any),
isMenuItem: true
},
{
key: index,
item: store.hoverRow,
data: store.hoverRow!.locals,
rowIndex: store.hoverRow!.index
}
)
)}
</div>
</ItemActionsWrapper>
);
} }
const finalActions = Array.isArray(itemActions)
? itemActions.filter(action => !action.hiddenOnHover)
: [];
if (!finalActions.length) {
return null;
}
return (
<ItemActionsWrapper store={store} classnames={cx}>
<div className={cx('Table-itemActions')}>
{finalActions.map((action, index) =>
render(
`itemAction/${index}`,
{
...(action as any),
isMenuItem: true
},
{
key: index,
item: store.hoverRow,
data: store.hoverRow!.locals,
rowIndex: store.hoverRow!.index
}
)
)}
</div>
</ItemActionsWrapper>
);
}
export class TableContent extends React.PureComponent<TableContentProps> {
render() { render() {
const { const {
placeholder, placeholder,
@ -166,7 +173,8 @@ export class TableContent extends React.Component<TableContentProps> {
store, store,
dispatchEvent, dispatchEvent,
onEvent, onEvent,
loading loading,
columnWidthReady
} = this.props; } = this.props;
const tableClassName = cx('Table-table', this.props.tableClassName); const tableClassName = cx('Table-table', this.props.tableClassName);
@ -182,7 +190,7 @@ export class TableContent extends React.Component<TableContentProps> {
ref={tableRef} ref={tableRef}
className={cx( className={cx(
tableClassName, tableClassName,
store.columnWidthReady ? 'is-layout-fixed' : undefined columnWidthReady ? 'is-layout-fixed' : undefined
)} )}
> >
<ColGroup columns={columns} store={store} /> <ColGroup columns={columns} store={store} />
@ -293,7 +301,6 @@ export class TableContent extends React.Component<TableContentProps> {
affixRow={affixRow} affixRow={affixRow}
data={data} data={data}
rowsProps={{ rowsProps={{
data,
dispatchEvent, dispatchEvent,
onEvent onEvent
}} }}
@ -304,3 +311,27 @@ export class TableContent extends React.Component<TableContentProps> {
); );
} }
} }
export default observer((props: TableContentProps) => {
const store = props.store;
// 分析 table/index.tsx 中的 renderHeadCell 依赖了以下属性
// store.someChecked;
// store.allChecked;
// store.isSelectionThresholdReached;
// store.allExpanded;
// store.orderBy
// store.orderDir
return (
<TableContent
{...props}
columnWidthReady={store.columnWidthReady}
someChecked={store.someChecked}
allChecked={store.allChecked}
isSelectionThresholdReached={store.isSelectionThresholdReached}
orderBy={store.orderBy}
orderDir={store.orderDir}
/>
);
});

View File

@ -1,9 +1,10 @@
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import React from 'react'; import React from 'react';
import type {IColumn, IRow} from 'amis-core/lib/store/table'; import type {IColumn, IRow} from 'amis-core/lib/store/table';
import {RendererEvent, RendererProps} from 'amis-core'; import {RendererEvent, RendererProps, autobind, traceProps} from 'amis-core';
import {Action} from '../Action'; import {Action} from '../Action';
import {isClickOnInput, createObject} from 'amis-core'; import {isClickOnInput} from 'amis-core';
import {useInView} from 'react-intersection-observer';
interface TableRowProps extends Pick<RendererProps, 'render'> { interface TableRowProps extends Pick<RendererProps, 'render'> {
onCheck: (item: IRow, value: boolean, shift?: boolean) => Promise<void>; onCheck: (item: IRow, value: boolean, shift?: boolean) => Promise<void>;
@ -38,31 +39,36 @@ interface TableRowProps extends Pick<RendererProps, 'render'> {
[propName: string]: any; [propName: string]: any;
} }
@observer export class TableRow extends React.PureComponent<
export class TableRow extends React.Component<TableRowProps> { TableRowProps & {
// reaction?: () => void; expanded: boolean;
constructor(props: TableRowProps) { id: string;
super(props); newIndex: number;
this.handleAction = this.handleAction.bind(this); isHover: boolean;
this.handleQuickChange = this.handleQuickChange.bind(this); checked: boolean;
this.handleChange = this.handleChange.bind(this); modified: boolean;
this.handleItemClick = this.handleItemClick.bind(this); moved: boolean;
this.handleDbClick = this.handleDbClick.bind(this); depth: number;
this.handleMouseEnter = this.handleMouseEnter.bind(this); expandable: boolean;
this.handleMouseLeave = this.handleMouseLeave.bind(this); appeard?: boolean;
checkdisable: boolean;
trRef?: React.Ref<any>;
} }
> {
@autobind
handleMouseEnter(e: React.MouseEvent<HTMLTableRowElement>) { handleMouseEnter(e: React.MouseEvent<HTMLTableRowElement>) {
const {item, itemIndex, onRowMouseEnter} = this.props; const {item, itemIndex, onRowMouseEnter} = this.props;
onRowMouseEnter?.(item?.data, itemIndex); onRowMouseEnter?.(item?.data, itemIndex);
} }
@autobind
handleMouseLeave(e: React.MouseEvent<HTMLTableRowElement>) { handleMouseLeave(e: React.MouseEvent<HTMLTableRowElement>) {
const {item, itemIndex, onRowMouseLeave} = this.props; const {item, itemIndex, onRowMouseLeave} = this.props;
onRowMouseLeave?.(item?.data, itemIndex); onRowMouseLeave?.(item?.data, itemIndex);
} }
// 定义点击一行的行为,通过 itemAction配置 // 定义点击一行的行为,通过 itemAction配置
@autobind
async handleItemClick(e: React.MouseEvent<HTMLTableRowElement>) { async handleItemClick(e: React.MouseEvent<HTMLTableRowElement>) {
if (isClickOnInput(e)) { if (isClickOnInput(e)) {
return; return;
@ -92,16 +98,19 @@ export class TableRow extends React.Component<TableRowProps> {
} }
} }
@autobind
handleDbClick(e: React.MouseEvent<HTMLTableRowElement>) { handleDbClick(e: React.MouseEvent<HTMLTableRowElement>) {
const {item, itemIndex, onRowDbClick} = this.props; const {item, itemIndex, onRowDbClick} = this.props;
onRowDbClick?.(item?.data, itemIndex); onRowDbClick?.(item?.data, itemIndex);
} }
@autobind
handleAction(e: React.UIEvent<any>, action: Action, ctx: any) { handleAction(e: React.UIEvent<any>, action: Action, ctx: any) {
const {onAction, item} = this.props; const {onAction, item} = this.props;
onAction && onAction(e, action, ctx || item.locals); onAction && onAction(e, action, ctx || item.locals);
} }
@autobind
handleQuickChange( handleQuickChange(
values: object, values: object,
saveImmediately?: boolean, saveImmediately?: boolean,
@ -116,6 +125,7 @@ export class TableRow extends React.Component<TableRowProps> {
onQuickChange(item, values, saveImmediately, savePristine, options); onQuickChange(item, values, saveImmediately, savePristine, options);
} }
@autobind
handleChange( handleChange(
value: any, value: any,
name: string, name: string,
@ -157,18 +167,32 @@ export class TableRow extends React.Component<TableRowProps> {
parent, parent,
itemAction, itemAction,
onEvent, onEvent,
expanded,
id,
newIndex,
isHover,
checked,
modified,
moved,
depth,
expandable,
appeard,
trRef,
...rest ...rest
} = this.props; } = this.props;
if (footableMode) { if (footableMode) {
if (!item.expanded) { if (!expanded) {
return null; return null;
} }
return ( return (
<tr <tr
data-id={item.id} ref={trRef}
data-index={item.newIndex} data-id={id}
data-index={newIndex}
onClick={ onClick={
checkOnItemClick || itemAction || onEvent?.rowClick checkOnItemClick || itemAction || onEvent?.rowClick
? this.handleItemClick ? this.handleItemClick
@ -178,10 +202,10 @@ export class TableRow extends React.Component<TableRowProps> {
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
className={cx(itemClassName, { className={cx(itemClassName, {
'is-hovered': item.isHover, 'is-hovered': isHover,
'is-checked': item.checked, 'is-checked': checked,
'is-modified': item.modified, 'is-modified': modified,
'is-moved': item.moved, 'is-moved': moved,
[`Table-tr--hasItemAction`]: itemAction, // 就是为了加鼠标效果 [`Table-tr--hasItemAction`]: itemAction, // 就是为了加鼠标效果
[`Table-tr--odd`]: itemIndex % 2 === 0, [`Table-tr--odd`]: itemIndex % 2 === 0,
[`Table-tr--even`]: itemIndex % 2 === 1 [`Table-tr--even`]: itemIndex % 2 === 1
@ -208,20 +232,26 @@ export class TableRow extends React.Component<TableRowProps> {
</th> </th>
) : null} ) : null}
{renderCell( {appeard ? (
`${regionPrefix}${itemIndex}/${column.index}`, renderCell(
column, `${regionPrefix}${itemIndex}/${column.index}`,
item, column,
{ item,
...rest, {
width: null, ...rest,
rowIndex: itemIndex, width: null,
colIndex: column.index, rowIndex: itemIndex,
key: column.index, colIndex: column.index,
onAction: this.handleAction, key: column.index,
onQuickChange: this.handleQuickChange, onAction: this.handleAction,
onChange: this.handleChange onQuickChange: this.handleQuickChange,
} onChange: this.handleChange
}
)
) : (
<td key={column.index}>
<div className={cx('Table-emptyBlock')}>&nbsp;</div>
</td>
)} )}
</tr> </tr>
))} ))}
@ -238,6 +268,7 @@ export class TableRow extends React.Component<TableRowProps> {
return ( return (
<tr <tr
ref={trRef}
onClick={ onClick={
checkOnItemClick || itemAction || onEvent?.rowClick checkOnItemClick || itemAction || onEvent?.rowClick
? this.handleItemClick ? this.handleItemClick
@ -246,36 +277,79 @@ export class TableRow extends React.Component<TableRowProps> {
onDoubleClick={this.handleDbClick} onDoubleClick={this.handleDbClick}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
data-index={item.depth === 1 ? item.newIndex : undefined} data-index={depth === 1 ? newIndex : undefined}
data-id={item.id} data-id={id}
className={cx( className={cx(
itemClassName, itemClassName,
{ {
'is-hovered': item.isHover, 'is-hovered': isHover,
'is-checked': item.checked, 'is-checked': checked,
'is-modified': item.modified, 'is-modified': modified,
'is-moved': item.moved, 'is-moved': moved,
'is-expanded': item.expanded && item.expandable, 'is-expanded': expanded && expandable,
'is-expandable': item.expandable, 'is-expandable': expandable,
[`Table-tr--hasItemAction`]: itemAction, [`Table-tr--hasItemAction`]: itemAction,
[`Table-tr--odd`]: itemIndex % 2 === 0, [`Table-tr--odd`]: itemIndex % 2 === 0,
[`Table-tr--even`]: itemIndex % 2 === 1 [`Table-tr--even`]: itemIndex % 2 === 1
}, },
`Table-tr--${item.depth}th` `Table-tr--${depth}th`
)} )}
> >
{columns.map(column => {columns.map(column =>
renderCell(`${itemIndex}/${column.index}`, column, item, { appeard ? (
...rest, renderCell(`${itemIndex}/${column.index}`, column, item, {
rowIndex: itemIndex, ...rest,
colIndex: column.index, rowIndex: itemIndex,
key: column.index, colIndex: column.index,
onAction: this.handleAction, key: column.index,
onQuickChange: this.handleQuickChange, onAction: this.handleAction,
onChange: this.handleChange onQuickChange: this.handleQuickChange,
}) onChange: this.handleChange
})
) : (
<td key={column.index}>
<div className={cx('Table-emptyBlock')}>&nbsp;</div>
</td>
)
)} )}
</tr> </tr>
); );
} }
} }
// 换成 mobx-react-lite 模式
export default observer((props: TableRowProps) => {
const item = props.item;
const store = props.store;
const columns = props.columns;
const canAccessSuperData =
store.canAccessSuperData ||
columns.some(item => item.pristine.canAccessSuperData);
const {ref, inView} = useInView({
threshold: 0,
onChange: item.markAppeared,
skip: !item.lazyRender
});
return (
<TableRow
{...props}
trRef={ref}
expanded={item.expanded}
id={item.id}
newIndex={item.newIndex}
isHover={item.isHover}
checked={item.checked}
modified={item.modified}
moved={item.moved}
depth={item.depth}
expandable={item.expandable}
checkdisable={item.checkdisable}
// data 在 TableRow 里面没有使用,这里写上是为了当列数据变化的时候 TableRow 重新渲染,
// 不是 item.locals 的原因是 item.locals 会变化多次,比如父级上下文变化也会进来,但是 item.data 只会变化一次。
data={canAccessSuperData ? item.locals : item.data}
appeard={item.lazyRender ? item.appeared || inView : true}
/>
);
});

View File

@ -17,6 +17,7 @@ import {Button} from 'amis-ui';
import {TableStore, ITableStore, padArr} from 'amis-core'; import {TableStore, ITableStore, padArr} from 'amis-core';
import { import {
anyChanged, anyChanged,
changedEffect,
getScrollParent, getScrollParent,
difference, difference,
autobind, autobind,
@ -38,7 +39,7 @@ import {TableCell} from './TableCell';
import type {AutoGenerateFilterObject} from '../CRUD'; import type {AutoGenerateFilterObject} from '../CRUD';
import {HeadCellFilterDropDown} from './HeadCellFilterDropdown'; import {HeadCellFilterDropDown} from './HeadCellFilterDropdown';
import {HeadCellSearchDropDown} from './HeadCellSearchDropdown'; import {HeadCellSearchDropDown} from './HeadCellSearchDropdown';
import {TableContent} from './TableContent'; import TableContent, {renderItemActions} from './TableContent';
import { import {
BaseSchema, BaseSchema,
SchemaApi, SchemaApi,
@ -183,6 +184,12 @@ export type TableColumnObject = {
*/ */
canAccessSuperData?: boolean; canAccessSuperData?: boolean;
/**
* 100
* @default 100
*/
lazyRenderAfter?: number;
/** /**
* style作为单元格自定义样式的配置 * style作为单元格自定义样式的配置
*/ */
@ -569,7 +576,10 @@ export default class Table extends React.Component<TableProps, object> {
keepItemSelectionOnPageChange, keepItemSelectionOnPageChange,
maxKeepItemSelectionLength, maxKeepItemSelectionLength,
onQuery, onQuery,
autoGenerateFilter autoGenerateFilter,
loading,
canAccessSuperData,
lazyRenderAfter
} = props; } = props;
let combineNum = props.combineNum; let combineNum = props.combineNum;
@ -597,7 +607,10 @@ export default class Table extends React.Component<TableProps, object> {
combineNum, combineNum,
combineFromIndex, combineFromIndex,
keepItemSelectionOnPageChange, keepItemSelectionOnPageChange,
maxKeepItemSelectionLength maxKeepItemSelectionLength,
loading,
canAccessSuperData,
lazyRenderAfter
}); });
if ( if (
@ -770,58 +783,49 @@ export default class Table extends React.Component<TableProps, object> {
const props = this.props; const props = this.props;
const store = props.store; const store = props.store;
if ( changedEffect(
anyChanged( [
[ 'selectable',
'selectable', 'columnsTogglable',
'columnsTogglable', 'draggable',
'draggable', 'orderBy',
'orderBy', 'orderDir',
'orderDir', 'multiple',
'multiple', 'footable',
'footable', 'primaryField',
'primaryField', 'itemCheckableOn',
'itemCheckableOn', 'itemDraggableOn',
'itemDraggableOn', 'hideCheckToggler',
'hideCheckToggler', 'combineNum',
'combineNum', 'combineFromIndex',
'combineFromIndex', 'expandConfig',
'expandConfig' 'columns',
], 'loading',
prevProps, 'canAccessSuperData',
props 'lazyRenderAfter'
) ],
) { prevProps,
let combineNum = props.combineNum; props,
if (typeof combineNum === 'string') { changes => {
combineNum = parseInt( if (
resolveVariableAndFilter(combineNum, props.data, '| raw'), changes.hasOwnProperty('combineNum') &&
10 typeof changes.combineNum === 'string'
); ) {
changes.combineNum = parseInt(
resolveVariableAndFilter(
changes.combineNum as string,
props.data,
'| raw'
),
10
);
}
if (changes.orderBy && !props.onQuery) {
delete changes.orderBy;
}
store.update(changes as any);
} }
store.update({ );
selectable: props.selectable,
columnsTogglable: props.columnsTogglable,
draggable: props.draggable,
orderBy: props.onQuery ? props.orderBy : undefined,
orderDir: props.orderDir,
multiple: props.multiple,
primaryField: props.primaryField,
footable: props.footable,
itemCheckableOn: props.itemCheckableOn,
itemDraggableOn: props.itemDraggableOn,
hideCheckToggler: props.hideCheckToggler,
combineNum: combineNum,
combineFromIndex: props.combineFromIndex,
expandConfig: props.expandConfig
});
}
if (prevProps.columns !== props.columns) {
store.update({
columns: props.columns
});
}
if ( if (
anyChanged(['source', 'value', 'items'], prevProps, props) || anyChanged(['source', 'value', 'items'], prevProps, props) ||
@ -1091,6 +1095,27 @@ export default class Table extends React.Component<TableProps, object> {
} }
} }
// 校验直接放在单元格里面的表单项
const subFormItems = store.children.filter(
item => item?.storeType === 'FormItemStore'
);
if (subFormItems.length) {
const result = await Promise.all(
subFormItems.map(item => {
let ctx = {};
if (item.rowIndex && store.rows[item.rowIndex]) {
ctx = store.rows[item.rowIndex].data;
}
return item.validate(ctx);
})
);
if (~result.indexOf(false)) {
return;
}
}
const rows = store.modifiedRows.map(item => item.data); const rows = store.modifiedRows.map(item => item.data);
const rowIndexes = store.modifiedRows.map(item => item.path); const rowIndexes = store.modifiedRows.map(item => item.path);
const diff = store.modifiedRows.map(item => const diff = store.modifiedRows.map(item =>
@ -1149,6 +1174,14 @@ export default class Table extends React.Component<TableProps, object> {
key => this.subForms[key] && subForms.push(this.subForms[key]) key => this.subForms[key] && subForms.push(this.subForms[key])
); );
subForms.forEach(item => item.clearErrors()); subForms.forEach(item => item.clearErrors());
// 去掉错误提示
const subFormItems = store.children.filter(
item => item?.storeType === 'FormItemStore'
);
if (subFormItems.length) {
subFormItems.map(item => item.reset());
}
} }
bulkUpdate(value: any, items: Array<object>) { bulkUpdate(value: any, items: Array<object>) {
@ -1824,6 +1857,9 @@ export default class Table extends React.Component<TableProps, object> {
data data
} = this.props; } = this.props;
// 注意,这里用关了哪些 store 里面的东西TableContent 里面得也用一下
// 因为 renderHeadCell 是 TableContent 回调的tableContent 不重新渲染,这里面也不会重新渲染
const style = {...props.style}; const style = {...props.style};
const [stickyStyle, stickyClassName] = store.getStickyStyles( const [stickyStyle, stickyClassName] = store.getStickyStyles(
column, column,
@ -2713,7 +2749,6 @@ export default class Table extends React.Component<TableProps, object> {
itemActions, itemActions,
dispatchEvent, dispatchEvent,
onEvent, onEvent,
loading = false,
loadingConfig loadingConfig
} = this.props; } = this.props;
@ -2723,7 +2758,7 @@ export default class Table extends React.Component<TableProps, object> {
return ( return (
<> <>
{TableContent.renderItemActions({ {renderItemActions({
store, store,
classnames: cx, classnames: cx,
render, render,
@ -2746,7 +2781,7 @@ export default class Table extends React.Component<TableProps, object> {
classnames={cx} classnames={cx}
columns={store.filteredColumns} columns={store.filteredColumns}
columnsGroup={store.columnGroup} columnsGroup={store.columnGroup}
rows={store.rows} rows={store.items} // store.rows 是没有变更的,所以不会触发更新
placeholder={placeholder} placeholder={placeholder}
render={render} render={render}
onMouseMove={ onMouseMove={
@ -2781,10 +2816,10 @@ export default class Table extends React.Component<TableProps, object> {
translate={translate} translate={translate}
dispatchEvent={dispatchEvent} dispatchEvent={dispatchEvent}
onEvent={onEvent} onEvent={onEvent}
loading={loading} loading={store.loading} // store 的同步较慢,所以统一用 store 来下发,否则会出现 props 和 store 变化触发子节点两次 re-rerender
/> />
<Spinner loadingConfig={loadingConfig} overlay show={loading} /> <Spinner loadingConfig={loadingConfig} overlay show={store.loading} />
</> </>
); );
} }