Merge pull request #901 from 2betop/master

CRUD 性能优化
This commit is contained in:
RickCole 2020-08-27 14:41:16 +08:00 committed by GitHub
commit b321bb1366
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1722 additions and 1305 deletions

View File

@ -11,7 +11,8 @@ node ./build/generate-search-data.js
fis3 release gh-pages -c
node ./build/upload2cdn.js $1 $2
# 不走 cdn 了
# node ./build/upload2cdn.js $1 $2
echo "pushing"

View File

@ -471,6 +471,35 @@ order: 67
}
```
可以结合 truncate 用来优化表格中的长内容展示,比如默认只展示 20 个字符,剩下的点击查看更多出现。
```schema:height="600" scope="body"
{
"type": "crud",
"api": "https://houtai.baidu.com/api/sample?waitSeconds=1",
"columns": [
{
"name": "id",
"label": "ID"
},
{
"type": "tpl",
"name": "engine",
"label": "Rendering engine",
"tpl": "${engine|truncate:2}",
"popOver": {
"body": {
"type": "tpl",
"tpl": "${engine}"
}
}
}
]
}
```
> 示例内容没那么长,直接配置成 2 个字符了。
### 表头样式
可以配置`"isHead": true`,来让当前列以表头的样式展示。应用场景是:

View File

@ -683,7 +683,7 @@ if (fis.project.currentMedia() === 'publish') {
]
});
ghPages.match('*', {
domain: 'https://bce.bdstatic.com/fex/amis-gh-pages',
domain: '/amis',
deploy: [
fis.plugin('skip-packed'),
fis.plugin('local-deliver', {

View File

@ -46,7 +46,7 @@
"keycode": "^2.1.9",
"lodash": "^4.17.15",
"match-sorter": "2.2.1",
"mobx": "^4.5.0 && <= 4.15.4",
"mobx": "^4.5.0",
"mobx-react": "^6.1.4",
"mobx-state-tree": "^3.7.0",
"moment": "^2.19.3",

View File

@ -105,10 +105,6 @@ function createScopedTools(
reload(target: string, ctx: any) {
const scoped = this;
if (target === 'window') {
return location.reload();
}
let targets =
typeof target === 'string' ? target.split(/\s*,\s*/) : target;
targets.forEach(name => {
@ -128,8 +124,19 @@ function createScopedTools(
name = name.substring(0, idx);
}
const component = scoped.getComponentByName(name);
component && component.reload && component.reload(subPath, query, ctx);
if (name === 'window') {
if (query) {
const link = location.pathname + '?' + qsstringify(query);
env ? env.updateLocation(link, true) : location.replace(link);
} else {
location.reload();
}
} else {
const component = scoped.getComponentByName(name);
component &&
component.reload &&
component.reload(subPath, query, ctx);
}
});
},

View File

@ -68,6 +68,7 @@ export interface RendererBasicConfig {
test: RegExp | TestFunc;
name?: string;
storeType?: string;
shouldSyncSuperStore?: (store: any, props: any, prevProps: any) => boolean;
storeExtendsData?: boolean; // 是否需要继承上层数据。
weight?: number; // 权重,值越低越优先命中。
isolateScope?: boolean;
@ -232,7 +233,8 @@ export function registerRenderer(config: RendererConfig): RendererConfig {
if (config.storeType && config.component) {
config.component = HocStoreFactory({
storeType: config.storeType,
extendsData: config.storeExtendsData
extendsData: config.storeExtendsData,
shouldSyncSuperStore: config.shouldSyncSuperStore
})(observer(config.component));
}
@ -335,7 +337,9 @@ export interface RootRendererProps {
[propName: string]: any;
}
const RootStoreContext = React.createContext<IRendererStore>(undefined as any);
export const RootStoreContext = React.createContext<IRendererStore>(
undefined as any
);
export class RootRenderer extends React.Component<RootRendererProps> {
state = {
@ -653,6 +657,7 @@ class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
export function HocStoreFactory(renderer: {
storeType: string;
extendsData?: boolean;
shouldSyncSuperStore?: (store: any, props: any, prevProps: any) => boolean;
}): any {
return function <T extends React.ComponentType<RendererProps>>(Component: T) {
type Props = Omit<
@ -698,12 +703,13 @@ export function HocStoreFactory(renderer: {
this.renderChild = this.renderChild.bind(this);
this.refFn = this.refFn.bind(this);
const store = (this.store = rootStore.addStore({
const store = rootStore.addStore({
id: guid(),
path: this.props.$path,
storeType: renderer.storeType,
parentId: this.props.store ? this.props.store.id : ''
} as any));
}) as IIRendererStore;
this.store = store;
if (renderer.extendsData === false) {
store.initData(
@ -750,6 +756,12 @@ export function HocStoreFactory(renderer: {
const props = this.props;
const store = this.store;
if (
renderer.shouldSyncSuperStore?.(store, nextProps, props) === false
) {
return;
}
if (renderer.extendsData === false) {
if (
props.defaultData !== nextProps.defaultData ||
@ -1099,3 +1111,50 @@ export function getRenderers() {
export function getRendererByName(name: string) {
return find(renderers, item => item.name === name);
}
export function withRootStore<
T extends React.ComponentType<
React.ComponentProps<T> & {
rootStore: IRendererStore;
}
>
>(ComposedComponent: T) {
type OuterProps = JSX.LibraryManagedAttributes<
T,
Omit<React.ComponentProps<T>, 'rootStore'>
>;
const result = hoistNonReactStatic(
class extends React.Component<OuterProps> {
static displayName = `WithRootStore(${
ComposedComponent.displayName || ComposedComponent.name
})`;
static contextType = RootStoreContext;
static ComposedComponent = ComposedComponent;
render() {
const rootStore = this.context;
const injectedProps: {
rootStore: IRendererStore;
} = {
rootStore
};
return (
<ComposedComponent
{...(this.props as JSX.LibraryManagedAttributes<
T,
React.ComponentProps<T>
>)}
{...injectedProps}
/>
);
}
},
ComposedComponent
);
return result as typeof result & {
ComposedComponent: T;
};
}

View File

@ -117,7 +117,7 @@ import './renderers/Page';
import './renderers/Panel';
import './renderers/Plain';
import './renderers/Spinner';
import './renderers/Table';
import './renderers/Table/index';
import './renderers/Tabs';
import './renderers/Tpl';
import './renderers/Mapping';

View File

@ -11,6 +11,7 @@ import {Icon} from '../components/icons';
import {ModalStore, IModalStore} from '../store/modal';
import {findDOMNode} from 'react-dom';
import {Spinner} from '../components';
import {IServiceStore} from '../store/service';
export interface DialogProps extends RendererProps {
title?: string; // 标题
@ -518,7 +519,9 @@ export default class Dialog extends React.Component<DialogProps, DialogState> {
storeType: ModalStore.name,
storeExtendsData: false,
name: 'dialog',
isolateScope: true
isolateScope: true,
shouldSyncSuperStore: (store: IServiceStore, props: any) =>
store.dialogOpen || props.show
})
export class DialogRenderer extends Dialog {
static contextType = ScopedContext;

View File

@ -10,6 +10,7 @@ import {findDOMNode} from 'react-dom';
import {IModalStore, ModalStore} from '../store/modal';
import {filter} from '../utils/tpl';
import {Spinner} from '../components';
import {IServiceStore} from '../store/service';
export interface DrawerProps extends RendererProps {
title?: string; // 标题
@ -536,7 +537,9 @@ export default class Drawer extends React.Component<DrawerProps, object> {
storeType: ModalStore.name,
storeExtendsData: false,
name: 'drawer',
isolateScope: true
isolateScope: true,
shouldSyncSuperStore: (store: IServiceStore, props: any) =>
store.drawerOpen || props.show
})
export class DrawerRenderer extends Drawer {
static contextType = ScopedContext;

View File

@ -227,7 +227,7 @@ export default class ComboControl extends React.Component<ComboProps> {
componentWillUnmount() {
const {formItem} = this.props;
formItem && formItem.setSubStore(null);
formItem && isAlive(formItem) && formItem.setSubStore(null);
this.toDispose.forEach(fn => fn());
this.toDispose = [];

View File

@ -2,13 +2,25 @@ import React from 'react';
import {IFormStore, IFormItemStore} from '../../store/form';
import debouce from 'lodash/debounce';
import {RendererProps, Renderer} from '../../factory';
import {
RendererProps,
Renderer,
RootStoreContext,
withRootStore
} from '../../factory';
import {ComboStore, IComboStore, IUniqueGroup} from '../../store/combo';
import {anyChanged, promisify, isObject, getVariable} from '../../utils/helper';
import {
anyChanged,
promisify,
isObject,
getVariable,
guid
} from '../../utils/helper';
import {Schema} from '../../types';
import {IIRendererStore} from '../../store';
import {IIRendererStore, IRendererStore} from '../../store';
import {ScopedContext, IScopedContext} from '../../Scoped';
import {reaction} from 'mobx';
import {FormItemStore} from '../../store/formItem';
export interface ControlProps extends RendererProps {
control: {
@ -30,6 +42,7 @@ export interface ControlProps extends RendererProps {
pipeOut?: (value: any, originValue: any, data: any) => any;
validate?: (value: any, values: any, name: string) => any;
} & Schema;
rootStore: IRendererStore;
formStore: IFormStore;
store: IIRendererStore;
addHook: (fn: () => any, type?: 'validate' | 'init' | 'flush') => void;
@ -45,7 +58,6 @@ export default class FormControl extends React.PureComponent<
ControlState
> {
static propsList: any = ['control'];
public model: IFormItemStore | undefined;
control: any;
value?: any;
@ -68,6 +80,7 @@ export default class FormControl extends React.PureComponent<
componentWillMount() {
const {
formStore: form,
rootStore,
control: {
name,
id,
@ -98,7 +111,16 @@ export default class FormControl extends React.PureComponent<
return;
}
const model = (this.model = form.registryItem(name, {
const model = rootStore.addStore({
id: guid(),
path: this.props.$path,
storeType: FormItemStore.name,
parentId: form.id,
name
}) as IFormItemStore;
this.model = model;
form.addFormItem(model);
model.config({
id,
type,
required,
@ -112,15 +134,11 @@ export default class FormControl extends React.PureComponent<
labelField,
joinValues,
extractValue
}));
});
if (
this.model.unique &&
form.parentStore &&
form.parentStore.storeType === ComboStore.name
) {
if (this.model.unique && form.parentStore?.storeType === ComboStore.name) {
const combo = form.parentStore as IComboStore;
combo.bindUniuqueItem(this.model);
combo.bindUniuqueItem(model);
}
// 同步 value
@ -269,7 +287,7 @@ export default class FormControl extends React.PureComponent<
combo.unBindUniuqueItem(this.model);
}
this.model && form.unRegistryItem(this.model);
this.model && form.removeFormItem(this.model);
}
controlRef(control: any) {
@ -536,6 +554,8 @@ export default class FormControl extends React.PureComponent<
!/\/control\/control$/i.test(path),
name: 'control'
})
// @ts-ignore
@withRootStore
export class FormControlRenderer extends FormControl {
static displayName = 'Control';
static contextType = ScopedContext;

View File

@ -265,6 +265,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
return (
<div
data-role="form-item"
className={cx(`Form-item Form-item--horizontal`, className, {
[`is-error`]: model && !model.valid,
[`is-required`]: required
@ -384,6 +385,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
return (
<div
data-role="form-item"
className={cx(`Form-item Form-item--${formMode}`, className, {
'is-error': model && !model.valid,
[`is-required`]: required
@ -480,6 +482,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
return (
<div
data-role="form-item"
className={cx(`Form-item Form-item--inline`, className, {
'is-error': model && !model.valid,
[`is-required`]: required
@ -581,6 +584,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
return (
<div
data-role="form-item"
className={cx(`Form-item Form-item--${formMode}`, className, {
'is-error': model && !model.valid,
[`is-required`]: required

View File

@ -34,6 +34,8 @@ import {LazyComponent} from '../../components';
import {isAlive} from 'mobx-state-tree';
import {asFormItem} from './Item';
import {SimpleMap} from '../../utils/SimpleMap';
import {trace} from 'mobx';
export type FormGroup = FormSchema & {
title?: string;
className?: string;
@ -92,6 +94,7 @@ export interface FormProps extends RendererProps, FormSchema {
clearPersistDataAfterSubmit: boolean; // 提交成功后清空本地缓存
trimValues?: boolean;
lazyLoad?: boolean;
simpleMode?: boolean;
onInit?: (values: object, props: any) => any;
onReset?: (values: object) => void;
onSubmit?: (values: object, action: any) => any;
@ -163,7 +166,8 @@ export default class Form extends React.Component<FormProps, object> {
'lazyChange',
'formLazyChange',
'lazyLoad',
'formInited'
'formInited',
'simpleMode'
];
hooks: {
@ -201,11 +205,15 @@ export default class Form extends React.Component<FormProps, object> {
}
componentWillMount() {
const {store, canAccessSuperData, persistData} = this.props;
const {store, canAccessSuperData, persistData, simpleMode} = this.props;
store.setCanAccessSuperData(canAccessSuperData !== false);
persistData && store.getPersistData();
if (simpleMode) {
store.setInited(true);
}
if (
store &&
store.parentStore &&
@ -319,16 +327,6 @@ export default class Form extends React.Component<FormProps, object> {
this.asyncCancel && this.asyncCancel();
this.disposeOnValidate && this.disposeOnValidate();
this.componentCache.dispose();
const store = this.props.store;
if (
store &&
store.parentStore &&
store.parentStore.storeType === 'ComboStore'
) {
const combo = store.parentStore as IComboStore;
isAlive(combo) && combo.removeForm(store);
}
}
async onInit() {
@ -1165,6 +1163,9 @@ export default class Form extends React.Component<FormProps, object> {
translate: __
} = this.props;
// trace(true);
// console.log('Form');
let body: JSX.Element = this.renderBody();
if (wrapWithPanel) {

View File

@ -467,6 +467,7 @@ export const HocQuickEdit = (config: Partial<QuickEditConfig> = {}) => (
wrapperComponent: 'div',
className: cx('Form--quickEdit'),
ref: this.formRef,
simpleMode: true,
onInit: this.handleInit,
onChange: this.handleChange
})}

View File

@ -0,0 +1,250 @@
import React from 'react';
import {Api} from '../../types';
import {RendererProps} from '../../factory';
import {isApiOutdated, isEffectiveApi, normalizeApi} from '../../utils/api';
import {Icon} from '../../components/icons';
import Overlay from '../../components/Overlay';
import PopOver from '../../components/PopOver';
import {findDOMNode} from 'react-dom';
import Checkbox from '../../components/Checkbox';
import xor from 'lodash/xor';
export interface QuickFilterConfig {
options: Array<any>;
source: Api;
multiple: boolean;
[propName: string]: any;
}
export interface HeadCellFilterProps extends RendererProps {
data: any;
name: string;
filterable: QuickFilterConfig;
onQuery: (values: object) => void;
}
export class HeadCellFilterDropDown extends React.Component<
HeadCellFilterProps,
any
> {
state = {
isOpened: false,
filterOptions: []
};
sourceInvalid: boolean = false;
constructor(props: HeadCellFilterProps) {
super(props);
this.open = this.open.bind(this);
this.close = this.close.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleCheck = this.handleCheck.bind(this);
}
componentDidMount() {
const {filterable} = this.props;
if (filterable.source) {
this.fetchOptions();
} else if (filterable.options.length > 0) {
this.setState({
filterOptions: this.alterOptions(filterable.options)
});
}
}
componentWillReceiveProps(nextProps: HeadCellFilterProps) {
const props = this.props;
if (
props.name !== nextProps.name ||
props.filterable !== nextProps.filterable ||
props.data !== nextProps.data
) {
if (nextProps.filterable.source) {
this.sourceInvalid = isApiOutdated(
props.filterable.source,
nextProps.filterable.source,
props.data,
nextProps.data
);
} else if (nextProps.filterable.options) {
this.setState({
filterOptions: this.alterOptions(nextProps.filterable.options || [])
});
}
}
}
componentDidUpdate() {
this.sourceInvalid && this.fetchOptions();
}
fetchOptions() {
const {env, filterable, data} = this.props;
if (!isEffectiveApi(filterable.source, data)) {
return;
}
const api = normalizeApi(filterable.source);
api.cache = 3000; // 开启 3s 缓存因为固顶位置渲染1次会额外多次请求。
env.fetcher(api, data).then(ret => {
let options = (ret.data && ret.data.options) || [];
this.setState({
filterOptions: ret && ret.data && this.alterOptions(options)
});
});
}
alterOptions(options: Array<any>) {
const {data, filterable, name} = this.props;
const filterValue =
data && typeof data[name] !== 'undefined' ? data[name] : '';
if (filterable.multiple) {
options = options.map(option => ({
...option,
selected: filterValue.split(',').indexOf(option.value) > -1
}));
} else {
options = options.map(option => ({
...option,
selected: option.value === filterValue
}));
}
return options;
}
handleClickOutside() {
this.close();
}
open() {
this.setState({
isOpened: true
});
}
close() {
this.setState({
isOpened: false
});
}
handleClick(value: string) {
const {onQuery, name} = this.props;
onQuery({
[name]: value
});
this.close();
}
handleCheck(value: string) {
const {data, name, onQuery} = this.props;
let query: string;
if (data[name] && data[name] === value) {
query = '';
} else {
query =
(data[name] && xor(data[name].split(','), [value]).join(',')) || value;
}
onQuery({
[name]: query
});
}
handleReset() {
const {name, onQuery} = this.props;
onQuery({
[name]: undefined
});
this.close();
}
render() {
const {isOpened, filterOptions} = this.state;
const {
data,
name,
filterable,
popOverContainer,
classPrefix: ns,
classnames: cx,
translate: __
} = this.props;
return (
<span
className={cx(
`${ns}TableCell-filterBtn`,
typeof data[name] !== 'undefined' ? 'is-active' : ''
)}
>
<span onClick={this.open}>
<Icon icon="column-filter" className="icon" />
</span>
{isOpened ? (
<Overlay
container={popOverContainer || (() => findDOMNode(this))}
placement="left-bottom-left-top right-bottom-right-top"
target={
popOverContainer ? () => findDOMNode(this)!.parentNode : null
}
show
>
<PopOver
classPrefix={ns}
onHide={this.close}
className={cx(
`${ns}TableCell-filterPopOver`,
(filterable as any).className
)}
overlay
>
{filterOptions && filterOptions.length > 0 ? (
<ul className={cx('DropDown-menu')}>
{!filterable.multiple
? filterOptions.map((option: any, index) => (
<li
key={index}
className={cx('DropDown-divider', {
'is-selected': option.selected
})}
onClick={this.handleClick.bind(this, option.value)}
>
{option.label}
</li>
))
: filterOptions.map((option: any, index) => (
<li key={index} className={cx('DropDown-divider')}>
<Checkbox
classPrefix={ns}
onChange={this.handleCheck.bind(this, option.value)}
checked={option.selected}
>
{option.label}
</Checkbox>
</li>
))}
<li
key="DropDown-menu-reset"
className={cx('DropDown-divider')}
onClick={this.handleReset.bind(this)}
>
{__('重置')}
</li>
</ul>
) : null}
</PopOver>
</Overlay>
) : null}
</span>
);
}
}

View File

@ -0,0 +1,272 @@
import React from 'react';
import {RendererProps} from '../../factory';
import {Action} from '../../types';
import {Icon} from '../../components/icons';
import Overlay from '../../components/Overlay';
import {findDOMNode} from 'react-dom';
import PopOver from '../../components/PopOver';
import {ITableStore} from '../../store/table';
export interface QuickSearchConfig {
type?: string;
controls?: any;
tabs?: any;
fieldSet?: any;
[propName: string]: any;
}
export interface HeadCellSearchProps extends RendererProps {
name: string;
searchable: boolean | QuickSearchConfig;
classPrefix: string;
onQuery: (values: object) => void;
}
export class HeadCellSearchDropDown extends React.Component<
HeadCellSearchProps,
any
> {
state = {
isOpened: false
};
formItems: Array<string> = [];
constructor(props: HeadCellSearchProps) {
super(props);
this.open = this.open.bind(this);
this.close = this.close.bind(this);
this.close = this.close.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleAction = this.handleAction.bind(this);
}
buildSchema() {
const {searchable, sortable, name, label, translate: __} = this.props;
let schema;
if (searchable === true) {
schema = {
title: '',
controls: [
{
type: 'text',
name,
placeholder: label,
clearable: true
}
]
};
} else if (searchable) {
if (searchable.controls || searchable.tabs || searchable.fieldSet) {
schema = {
title: '',
...searchable
};
} else {
schema = {
title: '',
className: searchable.formClassName,
controls: [
{
type: searchable.type || 'text',
name: searchable.name || name,
placeholder: label,
...searchable
}
]
};
}
}
if (schema && schema.controls && sortable) {
schema.controls.unshift(
{
type: 'hidden',
name: 'orderBy',
value: name
},
{
type: 'button-group',
name: 'orderDir',
label: __('排序'),
options: [
{
label: __('正序'),
value: 'asc'
},
{
label: __('降序'),
value: 'desc'
}
]
}
);
}
if (schema) {
const formItems: Array<string> = [];
schema.controls?.forEach(
(item: any) =>
item.name &&
item.name !== 'orderBy' &&
item.name !== 'orderDir' &&
formItems.push(item.name)
);
this.formItems = formItems;
schema = {
...schema,
type: 'form',
wrapperComponent: 'div',
actions: [
{
type: 'button',
label: __('重置'),
actionType: 'reset'
},
{
type: 'button',
label: __('取消'),
actionType: 'cancel'
},
{
label: __('搜索'),
type: 'submit',
primary: true
}
]
};
}
return schema || 'error';
}
handleClickOutside() {
this.close();
}
open() {
this.setState({
isOpened: true
});
}
close() {
this.setState({
isOpened: false
});
}
handleAction(e: any, action: Action, ctx: object) {
const {onAction} = this.props;
if (action.actionType === 'cancel' || action.actionType === 'close') {
this.close();
return;
}
if (action.actionType === 'reset') {
this.close();
this.handleReset();
return;
}
onAction && onAction(e, action, ctx);
}
handleReset() {
const {onQuery, data, name} = this.props;
const values = {...data};
this.formItems.forEach(key => (values[key] = undefined));
if (values.orderBy === name) {
values.orderBy = '';
values.orderDir = 'asc';
}
onQuery(values);
}
handleSubmit(values: any) {
const {onQuery, name} = this.props;
this.close();
if (values.orderDir) {
values = {
...values,
orderBy: name
};
}
onQuery(values);
}
isActive() {
const {data, name, orderBy} = this.props;
return orderBy === name || this.formItems.some(key => data?.[key]);
}
render() {
const {
render,
name,
data,
searchable,
store,
orderBy,
popOverContainer,
classPrefix: ns,
classnames: cx
} = this.props;
const formSchema = this.buildSchema();
const isActive = this.isActive();
return (
<span
className={cx(`${ns}TableCell-searchBtn`, isActive ? 'is-active' : '')}
>
<span onClick={this.open}>
<Icon icon="search" className="icon" />
</span>
{this.state.isOpened ? (
<Overlay
container={popOverContainer || (() => findDOMNode(this))}
placement="left-bottom-left-top right-bottom-right-top"
target={
popOverContainer ? () => findDOMNode(this)!.parentNode : null
}
show
>
<PopOver
classPrefix={ns}
onHide={this.close}
className={cx(
`${ns}TableCell-searchPopOver`,
(searchable as any).className
)}
overlay
>
{
render('quick-search-form', formSchema, {
data: {
...data,
orderBy: orderBy,
orderDir:
orderBy === name ? (store as ITableStore).orderDir : ''
},
onSubmit: this.handleSubmit,
onAction: this.handleAction
}) as JSX.Element
}
</PopOver>
</Overlay>
) : null}
</span>
);
}
}

View File

@ -0,0 +1,140 @@
import React from 'react';
import {RendererProps, Renderer} from '../../factory';
import QuickEdit from '../QuickEdit';
import Copyable from '../Copyable';
import PopOverable from '../PopOver';
import {observer} from 'mobx-react';
export interface TableCellProps extends RendererProps {
wrapperComponent?: React.ReactType;
column: object;
}
export class TableCell extends React.Component<RendererProps> {
static defaultProps = {
wrapperComponent: 'td'
};
static propsList: Array<string> = [
'type',
'label',
'column',
'body',
'tpl',
'rowSpan',
'remark'
];
render() {
let {
className,
render,
style,
wrapperComponent: Component,
column,
value,
data,
children,
width,
innerClassName,
label,
tabIndex,
onKeyUp,
rowSpan,
body: _body,
tpl,
remark,
prefix,
affix,
isHead,
...rest
} = this.props;
const schema = {
...column,
className: innerClassName,
type: (column && column.type) || 'plain'
};
let body = children
? children
: render('field', schema, {
...rest,
value,
data
});
if (width) {
style = {
...style,
width: (style && style.width) || width
};
if (!/%$/.test(String(style.width))) {
body = (
<div style={{width: style.width}}>
{prefix}
{body}
{affix}
</div>
);
prefix = null;
affix = null;
// delete style.width;
}
}
if (!Component) {
return body as JSX.Element;
}
if (isHead) {
Component = 'th';
}
return (
<Component
rowSpan={rowSpan > 1 ? rowSpan : undefined}
style={style}
className={className}
tabIndex={tabIndex}
onKeyUp={onKeyUp}
>
{prefix}
{body}
{affix}
</Component>
);
}
}
@Renderer({
test: /(^|\/)table\/(?:.*\/)?cell$/,
name: 'table-cell'
})
@QuickEdit()
@PopOverable()
@Copyable()
@observer
export class TableCellRenderer extends TableCell {
static propsList = [
'quickEdit',
'quickEditEnabledOn',
'popOver',
'copyable',
'inline',
...TableCell.propsList
];
}
@Renderer({
test: /(^|\/)field$/,
name: 'field'
})
@PopOverable()
@Copyable()
export class FieldRenderer extends TableCell {
static defaultProps = {
...TableCell.defaultProps,
wrapperComponent: 'div'
};
}

View File

@ -0,0 +1,233 @@
import React from 'react';
import {ClassNamesFn} from '../../theme';
import {IColumn, IRow} from '../../store/table';
import {SchemaNode, Action} from '../../types';
import {TableRow} from './TableRow';
import {filter} from '../../utils/tpl';
import {observer} from 'mobx-react';
import {trace, reaction} from 'mobx';
export interface TableContentProps {
className?: string;
tableClassName?: string;
classnames: ClassNamesFn;
columns: Array<IColumn>;
columnsGroup: Array<{
label: string;
index: number;
colSpan: number;
has: Array<any>;
}>;
rows: Array<IRow>;
placeholder?: string;
render: (region: string, node: SchemaNode, props?: any) => JSX.Element;
onMouseMove: (event: React.MouseEvent) => void;
onScroll: (event: React.UIEvent) => void;
tableRef: (table?: HTMLTableElement | null) => void;
renderHeadCell: (column: IColumn, props?: any) => JSX.Element;
renderCell: (
region: string,
column: IColumn,
item: IRow,
props: any
) => React.ReactNode;
onCheck: (item: IRow) => void;
onQuickChange?: (
item: IRow,
values: object,
saveImmediately?: boolean | any,
savePristine?: boolean
) => void;
footable?: boolean;
footableColumns: Array<IColumn>;
checkOnItemClick?: boolean;
buildItemProps?: (item: IRow, index: number) => any;
onAction?: (e: React.UIEvent<any>, action: Action, ctx: object) => void;
rowClassNameExpr?: string;
rowClassName?: string;
}
export class TableContent extends React.Component<TableContentProps> {
reaction?: () => void;
constructor(props: TableContentProps) {
super(props);
const rows = props.rows;
this.reaction = reaction(
() => `${rows.map(item => item.id).join(',')}`,
() => this.forceUpdate(),
{
onError: () => this.reaction!()
}
);
}
shouldComponentUpdate(nextProps: TableContentProps) {
const props = this.props;
if (props.columns !== nextProps.columns) {
return true;
}
return false;
}
componentwillUnmount() {
this.reaction?.();
}
renderRows(
rows: Array<any>,
columns = this.props.columns,
rowProps: any = {}
): any {
const {
rowClassName,
rowClassNameExpr,
onAction,
buildItemProps,
checkOnItemClick,
classnames: cx,
render,
renderCell,
onCheck,
onQuickChange,
footable,
footableColumns
} = this.props;
return rows.map((item: IRow, rowIndex: number) => {
const itemProps = buildItemProps ? buildItemProps(item, rowIndex) : null;
const doms = [
<TableRow
{...itemProps}
classnames={cx}
checkOnItemClick={checkOnItemClick}
key={item.index}
itemIndex={rowIndex}
item={item}
itemClassName={cx(
rowClassNameExpr
? filter(rowClassNameExpr, item.data)
: rowClassName,
{
'is-last': item.depth > 1 && rowIndex === rows.length - 1
}
)}
columns={columns}
renderCell={renderCell}
render={render}
onAction={onAction}
onCheck={onCheck}
// todo 先注释 quickEditEnabled={item.depth === 1}
onQuickChange={onQuickChange}
{...rowProps}
/>
];
if (footable && footableColumns.length) {
if (item.depth === 1) {
doms.push(
<TableRow
{...itemProps}
classnames={cx}
checkOnItemClick={checkOnItemClick}
key={`foot-${item.index}`}
itemIndex={rowIndex}
item={item}
itemClassName={cx(
rowClassNameExpr
? filter(rowClassNameExpr, item.data)
: rowClassName
)}
columns={footableColumns}
renderCell={renderCell}
render={render}
onAction={onAction}
onCheck={onCheck}
footableMode
footableColSpan={columns.length}
onQuickChange={onQuickChange}
{...rowProps}
/>
);
}
} else if (Array.isArray(item.data.children)) {
// 嵌套表格
doms.push(
...this.renderRows(item.children, columns, {
...rowProps,
parent: item
})
);
}
return doms;
});
}
render() {
const {
placeholder,
classnames: cx,
render,
className,
columns,
columnsGroup,
onMouseMove,
onScroll,
tableRef,
rows,
renderHeadCell
} = this.props;
const tableClassName = cx('Table-table', this.props.tableClassName);
const hideHeader = columns.every(column => !column.label);
return (
<div
onMouseMove={onMouseMove}
className={cx('Table-content', className)}
onScroll={onScroll}
>
<table ref={tableRef} className={tableClassName}>
<thead>
{columnsGroup.length ? (
<tr>
{columnsGroup.map((item, index) => (
<th
key={index}
data-index={item.index}
colSpan={item.colSpan}
>
{item.label ? render('tpl', item.label) : null}
</th>
))}
</tr>
) : null}
<tr className={hideHeader ? 'fake-hide' : ''}>
{columns.map(column =>
renderHeadCell(column, {
'data-index': column.index,
'key': column.index
})
)}
</tr>
</thead>
<tbody>
{rows.length ? (
this.renderRows(rows, columns)
) : (
<tr className={cx('Table-placeholder')}>
<td colSpan={columns.length}>
{render('placeholder', placeholder || '暂无数据')}
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
}

View File

@ -0,0 +1,209 @@
import {observer} from 'mobx-react';
import React from 'react';
import {IRow, IColumn} from '../../store/table';
import {RendererProps} from '../../factory';
import {Action} from '../Action';
import {reaction} from 'mobx';
interface TableRowProps extends Pick<RendererProps, 'render'> {
onCheck: (item: IRow) => void;
classPrefix: string;
renderCell: (
region: string,
column: IColumn,
item: IRow,
props: any
) => React.ReactNode;
columns: Array<IColumn>;
item: IRow;
parent?: IRow;
itemClassName?: string;
itemIndex: number;
regionPrefix?: string;
checkOnItemClick?: boolean;
[propName: string]: any;
}
export class TableRow extends React.Component<TableRowProps> {
reaction?: () => void;
constructor(props: TableRowProps) {
super(props);
this.handleAction = this.handleAction.bind(this);
this.handleQuickChange = this.handleQuickChange.bind(this);
this.handleClick = this.handleClick.bind(this);
const item = props.item;
const parent = props.parent;
const columns = props.columns;
this.reaction = reaction(
() =>
`${item.isHover}${item.checked}${JSON.stringify(item.data)}${
item.moved
}${item.modified}${item.expanded}${parent?.expanded}${columns.length}`,
() => this.forceUpdate(),
{
onError: () => this.reaction!()
}
);
}
shouldComponentUpdate(nextProps: TableRowProps) {
const props = this.props;
if (props.columns !== nextProps.columns) {
return true;
}
// 不需要更新,因为孩子节点已经 observer 了
return false;
}
componentWillUnmount() {
this.reaction?.();
}
handleClick(e: React.MouseEvent<HTMLTableRowElement>) {
const target: HTMLElement = e.target as HTMLElement;
const ns = this.props.classPrefix;
let formItem;
if (
!e.currentTarget.contains(target) ||
~['INPUT', 'TEXTAREA'].indexOf(target.tagName) ||
((formItem = target.closest(`button, a, [data-role="form-item"]`)) &&
e.currentTarget.contains(formItem))
) {
return;
}
this.props.onCheck(this.props.item);
}
handleAction(e: React.UIEvent<any>, action: Action, ctx: any) {
const {onAction, item} = this.props;
onAction && onAction(e, action, ctx || item.data);
}
handleQuickChange(
values: object,
saveImmediately?: boolean,
savePristine?: boolean
) {
const {onQuickChange, item} = this.props;
onQuickChange && onQuickChange(item, values, saveImmediately, savePristine);
}
render() {
const {
itemClassName,
itemIndex,
item,
columns,
renderCell,
children,
footableMode,
footableColSpan,
regionPrefix,
checkOnItemClick,
classPrefix: ns,
render,
classnames: cx,
parent,
...rest
} = this.props;
// console.log('TableRow');
if (footableMode) {
if (!item.expanded) {
return null;
}
return (
<tr
data-id={item.id}
data-index={item.newIndex}
onClick={checkOnItemClick ? this.handleClick : undefined}
className={cx(itemClassName, {
'is-hovered': item.isHover,
'is-checked': item.checked,
'is-modified': item.modified,
'is-moved': item.moved,
[`Table-tr--odd`]: itemIndex % 2 === 0,
[`Table-tr--even`]: itemIndex % 2 === 1
})}
>
<td className={cx(`Table-foot`)} colSpan={footableColSpan}>
<table className={cx(`Table-footTable`)}>
<tbody>
{columns.map(column => (
<tr key={column.index}>
{column.label !== false ? (
<th>
{render(
`${regionPrefix}${itemIndex}/${column.index}/tpl`,
column.label
)}
</th>
) : null}
{renderCell(
`${regionPrefix}${itemIndex}/${column.index}`,
column,
item,
{
...rest,
width: null,
rowIndex: itemIndex,
colIndex: column.rawIndex,
key: column.index,
onAction: this.handleAction,
onQuickChange: this.handleQuickChange
}
)}
</tr>
))}
</tbody>
</table>
</td>
</tr>
);
}
if (parent && !parent.expanded) {
return null;
}
return (
<tr
onClick={checkOnItemClick ? this.handleClick : undefined}
data-index={item.depth === 1 ? item.newIndex : undefined}
data-id={item.id}
className={cx(
itemClassName,
{
'is-hovered': item.isHover,
'is-checked': item.checked,
'is-modified': item.modified,
'is-moved': item.moved,
'is-expanded': item.expanded,
'is-expandable': item.expandable,
[`Table-tr--odd`]: itemIndex % 2 === 0,
[`Table-tr--even`]: itemIndex % 2 === 1
},
`Table-tr--${item.depth}th`
)}
>
{columns.map(column =>
renderCell(`${itemIndex}/${column.index}`, column, item, {
...rest,
rowIndex: itemIndex,
colIndex: column.rawIndex,
key: column.index,
onAction: this.handleAction,
onQuickChange: this.handleQuickChange
})
)}
</tr>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,30 @@
import {types, SnapshotIn, isAlive} from 'mobx-state-tree';
import {types, SnapshotIn, isAlive, onAction} from 'mobx-state-tree';
import {iRendererStore} from './iRenderer';
import {FormItemStore, IFormItemStore} from './formItem';
import {FormStore, IFormStore} from './form';
import {FormItemStore} from './formItem';
import {FormStore, IFormStore, IFormItemStore} from './form';
import {getStoreById} from './index';
export const UniqueGroup = types.model('UniqueGroup', {
name: types.identifier,
items: types.array(types.reference(types.late(() => FormItemStore)))
});
export const UniqueGroup = types
.model('UniqueGroup', {
name: types.identifier,
itemsRef: types.array(types.string)
})
.views(self => ({
get items() {
return self.itemsRef.map(
id => (getStoreById(id) as any) as IFormItemStore
);
}
}))
.actions(self => ({
removeItem(item: IFormItemStore) {
self.itemsRef.replace(self.itemsRef.filter(id => id !== item.id));
},
addItem(item: IFormItemStore) {
self.itemsRef.push(item.id);
}
}));
export type IUniqueGroup = typeof UniqueGroup.Type;
@ -14,13 +32,16 @@ export const ComboStore = iRendererStore
.named('ComboStore')
.props({
uniques: types.map(UniqueGroup),
forms: types.array(types.reference(types.late(() => FormStore))),
formsRef: types.optional(types.array(types.string), []),
minLength: 0,
maxLength: 0,
length: 0,
activeKey: 0
})
.views(self => ({
get forms() {
return self.formsRef.map(item => getStoreById(item) as IFormStore);
},
get addable() {
if (self.maxLength && self.length >= self.maxLength) {
return false;
@ -77,25 +98,38 @@ export const ComboStore = iRendererStore
});
}
let group: IUniqueGroup = self.uniques.get(item.name) as IUniqueGroup;
group.items.push(item);
group.addItem(item);
}
function unBindUniuqueItem(item: IFormItemStore) {
let group: IUniqueGroup = self.uniques.get(item.name) as IUniqueGroup;
group.items.remove(item);
group.removeItem(item);
if (!group.items.length) {
self.uniques.delete(item.name);
}
}
function addForm(form: IFormStore) {
self.forms.push(form);
self.formsRef.push(form.id);
}
function removeForm(form: IFormStore) {
// form 可能再它自己销毁的是已经被移除了。因为调用的是 destroy所以 self.forms 里面也被一起移除。
// 再来尝试移除,会报错。
self.forms.includes(form) && self.forms.remove(form);
function onChildStoreDispose(child: IFormStore) {
if (child.storeType === FormStore.name) {
const idx = self.formsRef.indexOf(child.id);
if (~idx) {
self.formsRef.splice(idx, 1);
child.items.forEach(item => {
if (item.unique) {
unBindUniuqueItem(item);
}
});
self.forms.forEach(item =>
item.items.forEach(item => item.unique && item.syncOptions())
);
}
}
self.removeChildId(child.id);
}
function setActiveKey(key: number) {
@ -108,7 +142,7 @@ export const ComboStore = iRendererStore
bindUniuqueItem,
unBindUniuqueItem,
addForm,
removeForm
onChildStoreDispose
};
});

View File

@ -184,14 +184,10 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
delete ctx[options.perPageField || 'perPage'];
}
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
ctx,
{
...options,
cancelExecutor: (executor: Function) => (fetchCancel = executor)
}
);
const json: Payload = yield getEnv(self).fetcher(api, ctx, {
...options,
cancelExecutor: (executor: Function) => (fetchCancel = executor)
});
fetchCancel = null;
if (!json.ok) {
@ -199,7 +195,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
json.msg || options.errorMessage || self.__('获取失败'),
true
);
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
json.msg,
json.msgTimeout !== undefined
@ -314,27 +310,22 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
// 配置了获取成功提示后提示,默认是空不会提示。
options &&
options.successMessage &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
getEnv(self).notify('success', self.msg);
}
self.markFetching(false);
return json;
} catch (e) {
const root = getRoot(self) as IRendererStore;
if (!isAlive(root) || root.storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
const env = getEnv(self) as IRendererStore;
self.markFetching(false);
if (root.isCancel(e)) {
if (env.isCancel(e)) {
return;
}
console.error(e.stack);
root.notify('error', e.message);
env.notify('error', e.message);
return;
}
});
@ -364,11 +355,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
};
self.markSaving(true);
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
options
);
const json: Payload = yield getEnv(self).fetcher(api, data, options);
self.markSaving(false);
if (!isEmpty(json.data) || json.ok) {
@ -387,7 +374,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
json.msg || options.errorMessage || self.__('保存失败'),
true
);
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
self.msg,
json.msgTimeout !== undefined
@ -400,15 +387,12 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
throw new ServerError(self.msg);
} else {
self.updateMessage(json.msg || options.successMessage);
self.msg &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
self.msg && getEnv(self).notify('success', self.msg);
}
return json.data;
} catch (e) {
self.markSaving(false);
e.type !== 'ServerError' &&
(getRoot(self) as IRendererStore) &&
(getRoot(self) as IRendererStore).notify('error', e.message);
e.type !== 'ServerError' && getEnv(self).notify('error', e.message);
throw e;
}
});

View File

@ -1,4 +1,4 @@
import {types, getEnv, flow, getRoot, detach} from 'mobx-state-tree';
import {types, getEnv, flow, getRoot, detach, destroy} from 'mobx-state-tree';
import debounce from 'lodash/debounce';
import {ServiceStore} from './service';
import {FormItemStore, IFormItemStore, SFormItemStore} from './formItem';
@ -17,7 +17,7 @@ import {
} from '../utils/helper';
import {IComboStore} from './combo';
import isEqual from 'lodash/isEqual';
import {IRendererStore} from '.';
import {IRendererStore, getStoreById, removeStore} from '.';
export const FormStore = ServiceStore.named('FormStore')
.props({
@ -26,59 +26,70 @@ export const FormStore = ServiceStore.named('FormStore')
submited: false,
submiting: false,
validating: false,
items: types.optional(types.array(types.late(() => FormItemStore)), []),
// items: types.optional(types.array(types.late(() => FormItemStore)), []),
itemsRef: types.optional(types.array(types.string), []),
canAccessSuperData: true,
persistData: false
})
.views(self => ({
get loading() {
return self.saving || self.fetching;
},
get errors() {
let errors: {
[propName: string]: Array<string>;
} = {};
self.items.forEach(item => {
if (!item.valid) {
errors[item.name] = Array.isArray(errors[item.name])
? errors[item.name].concat(item.errors)
: item.errors.concat();
}
});
return errors;
},
getValueByName(name: string) {
return getVariable(self.data, name, self.canAccessSuperData);
},
getPristineValueByName(name: string) {
return getVariable(self.pristine, name);
},
getItemById(id: string) {
return self.items.find(item => item.id === id);
},
getItemByName(name: string) {
return self.items.find(item => item.name === name);
},
getItemsByName(name: string) {
return self.items.filter(item => item.name === name);
},
get valid() {
return self.items.every(item => item.valid);
},
get isPristine() {
return isEqual(self.pristine, self.data);
.views(self => {
function getItems() {
return self.itemsRef.map(item => getStoreById(item) as IFormItemStore);
}
}))
return {
get loading() {
return self.saving || self.fetching;
},
get items() {
return getItems();
},
get errors() {
let errors: {
[propName: string]: Array<string>;
} = {};
getItems().forEach(item => {
if (!item.valid) {
errors[item.name] = Array.isArray(errors[item.name])
? errors[item.name].concat(item.errors)
: item.errors.concat();
}
});
return errors;
},
getValueByName(name: string) {
return getVariable(self.data, name, self.canAccessSuperData);
},
getPristineValueByName(name: string) {
return getVariable(self.pristine, name);
},
getItemById(id: string) {
return getItems().find(item => item.itemId === id);
},
getItemByName(name: string) {
return getItems().find(item => item.name === name);
},
getItemsByName(name: string) {
return getItems().filter(item => item.name === name);
},
get valid() {
return getItems().every(item => item.valid);
},
get isPristine() {
return isEqual(self.pristine, self.data);
}
};
})
.actions(self => {
function setValues(values: object, tag?: object, replace?: boolean) {
self.updateData(values, tag, replace);
@ -223,11 +234,7 @@ export const FormStore = ServiceStore.named('FormStore')
}
self.markSaving(true);
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
options
);
const json: Payload = yield getEnv(self).fetcher(api, data, options);
// 失败也同样修改数据,如果有数据的话。
if (!isEmpty(json.data) || json.ok) {
@ -282,22 +289,16 @@ export const FormStore = ServiceStore.named('FormStore')
}
self.markSaving(false);
self.updateMessage(json.msg || (options && options.successMessage));
self.msg &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
self.msg && getEnv(self).notify('success', self.msg);
return json.data;
}
} catch (e) {
if ((getRoot(self) as IRendererStore).storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
self.markSaving(false);
// console.error(e.stack);`
if (e.type === 'ServerError') {
const result = (e as ServerError).response;
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
e.message,
result.msgTimeout !== undefined
@ -308,7 +309,7 @@ export const FormStore = ServiceStore.named('FormStore')
: undefined
);
} else {
(getRoot(self) as IRendererStore).notify('error', e.message);
getEnv(self).notify('error', e.message);
}
throw e;
@ -332,7 +333,7 @@ export const FormStore = ServiceStore.named('FormStore')
if (!valid) {
const msg = failedMessage ?? self.__('表单验证失败,请仔细检查');
msg && (getRoot(self) as IRendererStore).notify('error', msg);
msg && getEnv(self).notify('error', msg);
throw new Error(self.__('验证失败'));
}
@ -420,51 +421,14 @@ export const FormStore = ServiceStore.named('FormStore')
cb && cb(self.data);
}
function registryItem(
name: string,
options?: Partial<SFormItemStore> & {
value?: any;
}
): IFormItemStore {
let item: IFormItemStore;
self.items.push({
identifier: guid(),
name
} as any);
item = self.items[self.items.length - 1] as IFormItemStore;
function addFormItem(item: IFormItemStore) {
self.itemsRef.push(item.id);
// 默认值可能在原型上,把他挪到当前对象上。
setValueByName(item.name, item.value, false, false);
options && item.config(options);
return item;
}
function unRegistryItem(item: IFormItemStore) {
detach(item);
}
function beforeDetach() {
// 本来是想在组件销毁的时候处理,
// 但是 componentWillUnmout 是父级先执行form 都销毁了 formItem 就取不到 父级就不是 combo 了。
if (self.parentStore && self.parentStore.storeType === 'ComboStore') {
const combo = self.parentStore as IComboStore;
self.items.forEach(item => {
if (item.unique) {
combo.unBindUniuqueItem(item);
}
});
combo.removeForm(self as IFormStore);
combo.forms.forEach(item =>
item.items.forEach(item => item.unique && item.syncOptions())
);
}
self.items.forEach(item => detach(item));
function removeFormItem(item: IFormItemStore) {
removeStore(item);
}
function setCanAccessSuperData(value: boolean = true) {
@ -500,6 +464,14 @@ export const FormStore = ServiceStore.named('FormStore')
localStorage.removeItem(location.pathname + self.path);
}
function onChildStoreDispose(child: IFormItemStore) {
if (child.storeType === FormItemStore.name) {
const itemsRef = self.itemsRef.filter(id => id !== child.id);
self.itemsRef.replace(itemsRef);
}
self.removeChildId(child.id);
}
return {
setInited,
setValues,
@ -511,15 +483,15 @@ export const FormStore = ServiceStore.named('FormStore')
clearErrors,
saveRemote,
reset,
registryItem,
unRegistryItem,
beforeDetach,
addFormItem,
removeFormItem,
syncOptions,
setCanAccessSuperData,
deleteValueByName,
getPersistData,
setPersistData,
clearPersistData,
onChildStoreDispose,
beforeDestroy() {
syncOptions.cancel();
setPersistData.cancel();

View File

@ -5,7 +5,8 @@ import {
flow,
getRoot,
hasParent,
isAlive
isAlive,
getEnv
} from 'mobx-state-tree';
import {IFormStore} from './form';
import {str2rules, validate as doValidate} from '../utils/validations';
@ -29,6 +30,7 @@ import find from 'lodash/find';
import {SimpleMap} from '../utils/SimpleMap';
import memoize from 'lodash/memoize';
import {TranslateFn} from '../locale';
import {storeNode} from './node';
interface IOption {
value?: string | number | null;
@ -44,9 +46,9 @@ const ErrorDetail = types.model('ErrorDetail', {
tag: ''
});
export const FormItemStore = types
.model('FormItemStore', {
identifier: types.identifier,
export const FormItemStore = storeNode
.named('FormItemStore')
.props({
isFocused: false,
type: '',
unique: false,
@ -56,7 +58,7 @@ export const FormItemStore = types
messages: types.optional(types.frozen(), {}),
errorData: types.optional(types.array(ErrorDetail), []),
name: types.string,
id: '', // 因为 name 可能会重名,所以加个 id 进来,如果有需要用来定位具体某一个
itemId: '', // 因为 name 可能会重名,所以加个 id 进来,如果有需要用来定位具体某一个
unsetValueOnInvisible: false,
validated: false,
validating: false,
@ -76,11 +78,11 @@ export const FormItemStore = types
})
.views(self => {
function getForm(): any {
return hasParent(self, 2) ? getParent(self, 2) : null;
return self.parentStore;
}
function getValue(): any {
return getForm() ? getForm().getValueByName(self.name) : undefined;
return getForm()?.getValueByName(self.name);
}
function getLastOptionValue(): any {
@ -105,9 +107,7 @@ export const FormItemStore = types
},
get prinstine(): any {
return (getParent(self, 2) as IFormStore).getPristineValueByName(
self.name
);
return (getForm() as IFormStore).getPristineValueByName(self.name);
},
get errors() {
@ -204,11 +204,7 @@ export const FormItemStore = types
},
get __(): TranslateFn {
return isAlive(self) &&
getRoot(self) &&
(getRoot(self) as IRendererStore).storeType === 'RendererStore'
? (getRoot(self) as IRendererStore).__
: (str: string) => str;
return getEnv(self).__;
}
};
})
@ -251,7 +247,7 @@ export const FormItemStore = types
}
typeof type !== 'undefined' && (self.type = type);
typeof id !== 'undefined' && (self.id = id);
typeof id !== 'undefined' && (self.itemId = id);
typeof messages !== 'undefined' && (self.messages = messages);
typeof required !== 'undefined' && (self.required = !!required);
typeof unique !== 'undefined' && (self.unique = !!unique);
@ -399,15 +395,11 @@ export const FormItemStore = types
self.loading = true;
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
{
autoAppend: false,
cancelExecutor: (executor: Function) => (loadCancel = executor),
...config
}
);
const json: Payload = yield getEnv(self).fetcher(api, data, {
autoAppend: false,
cancelExecutor: (executor: Function) => (loadCancel = executor),
...config
});
loadCancel = null;
let result: any = null;
@ -418,7 +410,7 @@ export const FormItemStore = types
reason: json.msg || (config && config.errorMessage)
})
);
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
self.errors.join(''),
json.msgTimeout !== undefined
@ -435,21 +427,16 @@ export const FormItemStore = types
self.loading = false;
return result;
} catch (e) {
const root = getRoot(self) as IRendererStore;
if (root.storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
const env = getEnv(self);
self.loading = false;
if (root.isCancel(e)) {
if (env.isCancel(e)) {
return;
}
console.error(e.stack);
getRoot(self) &&
(getRoot(self) as IRendererStore).notify('error', e.message);
env.notify('error', e.message);
return;
}
} as any);

View File

@ -1,64 +1,26 @@
import {types, getRoot, Instance, destroy, isAlive} from 'mobx-state-tree';
import {types} from 'mobx-state-tree';
import {extendObject, createObject} from '../utils/helper';
import {IRendererStore} from './index';
import {dataMapping} from '../utils/tpl-builtin';
import {SimpleMap} from '../utils/SimpleMap';
import {TranslateFn} from '../locale';
import {storeNode} from './node';
export const iRendererStore = types
.model('iRendererStore', {
id: types.identifier,
path: '',
storeType: types.string,
export const iRendererStore = storeNode
.named('iRendererStore')
.props({
hasRemoteData: types.optional(types.boolean, false),
data: types.optional(types.frozen(), {}),
initedAt: 0, // 初始 init 的时刻
updatedAt: 0, // 从服务端更新时刻
pristine: types.optional(types.frozen(), {}),
disposed: false,
parentId: '',
childrenIds: types.optional(types.array(types.string), []),
action: types.optional(types.frozen(), undefined),
dialogOpen: false,
dialogData: types.optional(types.frozen(), undefined),
drawerOpen: false,
drawerData: types.optional(types.frozen(), undefined)
})
.views(self => {
return {
get parentStore(): any {
return isAlive(self) &&
self.parentId &&
getRoot(self) &&
(getRoot(self) as IRendererStore).storeType === 'RendererStore'
? (getRoot(self) as IRendererStore).stores.get(self.parentId)
: null;
},
get __(): TranslateFn {
return isAlive(self) &&
getRoot(self) &&
(getRoot(self) as IRendererStore).storeType === 'RendererStore'
? (getRoot(self) as IRendererStore).__
: (str: string) => str;
}
};
})
.actions(self => {
const dialogCallbacks = new SimpleMap<(result?: any) => void>();
function dispose() {
// 先标记自己是要销毁的。
self.disposed = true;
const parent = self.parentStore;
if (!self.childrenIds.length) {
const id = self.id;
destroy(self);
parent && parent.onChildDispose(id);
}
}
return {
initData(data: object = {}) {
self.initedAt = Date.now();
@ -177,16 +139,7 @@ export const iRendererStore = types
dialogCallbacks.delete(self.drawerData);
setTimeout(() => callback(result), 200);
}
},
onChildDispose(childId: string) {
const childrenIds = self.childrenIds.filter(item => item !== childId);
self.childrenIds.replace(childrenIds);
self.disposed && dispose();
},
dispose
}
};
});

View File

@ -9,6 +9,9 @@ import {TableStore} from './table';
import {ListStore} from './list';
import {ModalStore} from './modal';
import {TranslateFn} from '../locale';
import find from 'lodash/find';
import {IStoreNode} from './node';
import {FormItemStore} from './formItem';
setLivelynessChecking(
process.env.NODE_ENV === 'production' ? 'ignore' : 'error'
@ -21,30 +24,13 @@ const allowedStoreList = [
CRUDStore,
TableStore,
ListStore,
ModalStore
ModalStore,
FormItemStore
];
export const RendererStore = types
.model('RendererStore', {
storeType: 'RendererStore',
stores: types.map(
types.union(
{
eager: false,
dispatcher: (snapshort: SIRendererStore) => {
for (let storeFactory of allowedStoreList) {
if (storeFactory.name === snapshort.storeType) {
return storeFactory;
}
}
return iRendererStore;
}
},
iRendererStore,
...allowedStoreList
)
)
storeType: 'RendererStore'
})
.views(self => ({
get fetcher() {
@ -61,30 +47,42 @@ export const RendererStore = types
get __(): TranslateFn {
return getEnv(self).translate;
}
}))
.views(self => ({
},
getStoreById(id: string) {
return self.stores.get(id);
return getStoreById(id);
}
}))
.actions(self => ({
addStore(store: SIRendererStore): IIRendererStore {
if (self.stores.has(store.id as string)) {
return self.stores.get(store.id) as IIRendererStore;
}
addStore(store: {
storeType: string;
id: string;
path: string;
parentId?: string;
[propName: string]: any;
}): IStoreNode {
const factory = find(
allowedStoreList,
item => item.name === store.storeType
)!;
if (store.parentId) {
const parent = self.stores.get(store.parentId) as IIRendererStore;
parent.childrenIds.push(store.id);
}
return addStore(factory.create(store, getEnv(self)));
self.stores.put(store);
return self.stores.get(store.id) as IIRendererStore;
// if (self.stores.has(store.id as string)) {
// return self.stores.get(store.id) as IIRendererStore;
// }
// if (store.parentId) {
// const parent = self.stores.get(store.parentId) as IIRendererStore;
// parent.childrenIds.push(store.id);
// }
// self.stores.put(store);
// return self.stores.get(store.id) as IIRendererStore;
},
removeStore(store: IIRendererStore) {
store.dispose();
removeStore(store: IStoreNode) {
// store.dispose();
removeStore(store);
}
}));
@ -93,3 +91,32 @@ export {iRendererStore, IIRendererStore};
export const RegisterStore = function (store: any) {
allowedStoreList.push(store as any);
};
const stores: {
[propName: string]: IStoreNode;
} = {};
export function addStore(store: IStoreNode) {
if (stores[store.id]) {
return stores[store.id];
}
stores[store.id] = store;
// drawer dialog 不加进去,否则有些容器就不会自我销毁 store 了。
if (store.parentId && !/(?:dialog|drawer)$/.test(store.path)) {
const parent = stores[store.parentId] as IIRendererStore;
parent.addChildId(store.id);
}
return store;
}
export function removeStore(store: IStoreNode) {
const id = store.id;
store.dispose(() => delete stores[id]);
}
export function getStoreById(id: string) {
return stores[id];
}

66
src/store/node.ts Normal file
View File

@ -0,0 +1,66 @@
import {types, destroy, isAlive, detach, getEnv} from 'mobx-state-tree';
import {getStoreById} from './index';
export const storeNode = types
.model('storeNode', {
id: types.identifier,
path: '',
storeType: types.string,
disposed: false,
parentId: '',
childrenIds: types.optional(types.array(types.string), [])
})
.views(self => {
return {
get parentStore(): any {
return isAlive(self) && self.parentId
? getStoreById(self.parentId)
: null;
},
get __() {
return getEnv(self).__;
}
};
})
.actions(self => {
function addChildId(id: string) {
self.childrenIds.push(id);
}
function removeChildId(id: string) {
const childrenIds = self.childrenIds.filter(item => item !== id);
self.childrenIds.replace(childrenIds);
self.disposed && dispose();
}
function dispose(callback?: () => void) {
// 先标记自己是要销毁的。
self.disposed = true;
if (/(?:dialog|drawer)$/.test(self.path)) {
destroy(self);
callback?.();
} else if (!self.childrenIds.length) {
const parent = self.parentStore;
parent?.onChildStoreDispose?.(self);
destroy(self);
callback?.();
// destroy(self);
}
}
return {
onChildStoreDispose(child: any) {
removeChildId(child.id);
},
dispose,
addChildId,
removeChildId
};
});
export type IStoreNode = typeof storeNode.Type;
export type SIStoreNode = typeof storeNode.SnapshotType;

View File

@ -81,19 +81,15 @@ export const ServiceStore = iRendererStore
}
(options && options.silent) || markFetching(true);
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
{
...options,
cancelExecutor: (executor: Function) => (fetchCancel = executor)
}
);
const json: Payload = yield getEnv(self).fetcher(api, data, {
...options,
cancelExecutor: (executor: Function) => (fetchCancel = executor)
});
fetchCancel = null;
if (!json.ok) {
updateMessage(json.msg || (options && options.errorMessage), true);
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
json.msg,
json.msgTimeout !== undefined
@ -125,25 +121,21 @@ export const ServiceStore = iRendererStore
// 配置了获取成功提示后提示,默认是空不会提示。
options &&
options.successMessage &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
getEnv(self).notify('success', self.msg);
}
markFetching(false);
return json;
} catch (e) {
const root = getRoot(self) as IRendererStore;
if (!isAlive(root) || root.storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
const env = getEnv(self);
if (root.isCancel(e)) {
if (env.isCancel(e)) {
return;
}
markFetching(false);
e.stack && console.error(e.stack);
root.notify('error', e.message || e);
env.notify('error', e.message || e);
return;
}
});
@ -169,7 +161,7 @@ export const ServiceStore = iRendererStore
}
(options && options.silent) || markFetching(true);
const json: Payload = yield ((getRoot(
const json: Payload = yield ((getEnv(
self
) as IRendererStore) as IRendererStore).fetcher(api, data, {
...options,
@ -192,7 +184,7 @@ export const ServiceStore = iRendererStore
if (!json.ok) {
updateMessage(json.msg || (options && options.errorMessage), true);
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
self.msg,
json.msgTimeout !== undefined
@ -216,25 +208,21 @@ export const ServiceStore = iRendererStore
// 配置了获取成功提示后提示,默认是空不会提示。
options &&
options.successMessage &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
getEnv(self).notify('success', self.msg);
}
markFetching(false);
return json;
} catch (e) {
const root = getRoot(self) as IRendererStore;
if (!isAlive(root) || root.storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
const env = getEnv(self);
if (root.isCancel(e)) {
if (env.isCancel(e)) {
return;
}
markFetching(false);
e.stack && console.error(e.stack);
root.notify('error', e.message || e);
env.notify('error', e.message || e);
return;
}
});
@ -259,11 +247,7 @@ export const ServiceStore = iRendererStore
}
markSaving(true);
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
options
);
const json: Payload = yield getEnv(self).fetcher(api, data, options);
if (!isEmpty(json.data) || json.ok) {
self.updatedAt = Date.now();
@ -294,8 +278,7 @@ export const ServiceStore = iRendererStore
}
updateMessage(json.msg || (options && options.successMessage));
self.msg &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
self.msg && getEnv(self).notify('success', self.msg);
}
markSaving(false);
@ -305,7 +288,7 @@ export const ServiceStore = iRendererStore
// console.log(e.stack);
if (e.type === 'ServerError') {
const result = (e as ServerError).response;
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
e.message,
result.msgTimeout !== undefined
@ -316,7 +299,7 @@ export const ServiceStore = iRendererStore
: undefined
);
} else {
(getRoot(self) as IRendererStore).notify('error', e.message);
getEnv(self).notify('error', e.message);
}
throw e;
@ -363,11 +346,7 @@ export const ServiceStore = iRendererStore
};
}
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
options
);
const json: Payload = yield getEnv(self).fetcher(api, data, options);
fetchSchemaCancel = null;
if (!json.ok) {
@ -377,7 +356,7 @@ export const ServiceStore = iRendererStore
self.__('获取失败,请重试'),
true
);
(getRoot(self) as IRendererStore).notify(
getEnv(self).notify(
'error',
self.msg,
json.msgTimeout !== undefined
@ -403,26 +382,22 @@ export const ServiceStore = iRendererStore
// 配置了获取成功提示后提示,默认是空不会提示。
options &&
options.successMessage &&
(getRoot(self) as IRendererStore).notify('success', self.msg);
getEnv(self).notify('success', self.msg);
}
self.initializing = false;
return json.data;
} catch (e) {
const root = getRoot(self) as IRendererStore;
if (!isAlive(root) || root.storeType !== 'RendererStore') {
// 已经销毁了,不管这些数据了。
return;
}
const env = getEnv(self);
self.initializing = false;
if (root.isCancel(e)) {
if (env.isCancel(e)) {
return;
}
e.stack && console.error(e.stack);
root.notify('error', e.message || e);
env.notify('error', e.message || e);
}
});
@ -441,11 +416,7 @@ export const ServiceStore = iRendererStore
try {
self.checking = true;
const json: Payload = yield (getRoot(self) as IRendererStore).fetcher(
api,
data,
options
);
const json: Payload = yield getEnv(self).fetcher(api, data, options);
json.ok &&
self.updateData(
json.data,

View File

@ -118,9 +118,9 @@ export const Row = types
},
get expanded(): boolean {
return (getParent(self, self.depth * 2) as ITableStore).isExpanded(
self as IRow
);
const table = getParent(self, self.depth * 2) as ITableStore;
return !table.dragging && table.isExpanded(self as IRow);
},
get moved() {
@ -173,6 +173,11 @@ export const Row = types
setIsHover(value: boolean) {
self.isHover = value;
},
replaceWith(data: any) {
delete data.id;
Object.keys(data).forEach(key => ((self as any)[key] = data[key]));
}
}));
@ -680,7 +685,6 @@ export const TableStore = iRendererStore
pristine: item,
data: item,
rowSpans: {},
modified: false,
children:
item && Array.isArray(item.children)
? initChildren(item.children, 1, key, id)
@ -696,7 +700,7 @@ export const TableStore = iRendererStore
arr = autoCombineCell(arr, self.columns, self.combineNum);
}
self.rows.replace(arr as Array<IRow>);
replaceRow(arr);
self.isNested = self.rows.some(item => item.children.length);
const expand = self.footable && self.footable.expand;
@ -717,6 +721,30 @@ export const TableStore = iRendererStore
self.dragging = false;
}
// 尽可能的复用 row
function replaceRow(arr: Array<SRow>) {
const pool = arr.concat();
// 把多的删了先
if (self.rows.length > arr.length) {
self.rows.splice(arr.length, self.rows.length - arr.length);
}
let index = 0;
const len = self.rows.length;
while (pool.length) {
const item = pool.shift()!;
if (index < len) {
self.rows[index].replaceWith(item);
} else {
const row = Row.create(item);
self.rows.push(row);
}
index++;
}
}
function updateSelected(selected: Array<any>, valueField?: string) {
self.selectedRows.clear();
self.rows.forEach(item => {