编写列表组件

接下来我们来看看列表组件的例子。在system/panel组件里,我们不仅想管理当前System的信息,还想管理属于它的Application对象信息,回忆我们在介绍system/detail组件中的这张图:

systemPanel2

在左侧点击“应用管理”时,我们希望展示(属于这个System)的所有Application的信息列表,这是一个列表组件,其代码见oak-general-business/src/components/system/application目录。

逻辑层

index.ts中,我们要声明这是一个List页面(isList属性为true),给出完整的projection。

export default OakComponent({
    entity: 'application',
    isList: true,
    projection: {
        id: 1,
        name: 1,
        config: 1,
        description: 1,
        type: 1,
        systemId: 1,
        domainId: 1,
    },
    formData({ data }) {
        return {
            applications: data || [],
        };
    },
});

注意,此时在formData中,data对象就是一个数组,其结构可以参见TypeScript的类型定义。

渲染层

渲染层和详情页面也是类似,在这里取得了props.data中的相关属性以后即可进行渲染(注意在上面的页面里,我们把Application的三行数据又渲染成了一行tabs页,因为在每个Application中也有很多设置需要展示)。

import React, { useState } from 'react';
import { Tabs, Modal, Button, Space } from 'antd';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';

export default function render(props: WebComponentProps<EntityDict, 'application', true, {
    applications: EntityDict['application']['OpSchema'][];
    systemId: string;
}>) {
    const { oakFullpath, applications, oakExecutable, oakExecuting, systemId } = props.data;    

    if (oakFullpath && applications?.length > 0) {
        return (
            <Tabs>
                {
                    applications.map(
                        ele => <Tab>....</Tab>
                    )
                }
            </Tabs>
        );
    }

    return null;
}

组件的相对路径

在上面的例子中,你可能会意识到一个问题,详情组件和更新组件在初始props中会自带主键(oakId),列表组件在定义查询数据的时候难道没有约束条件吗?比如上面的system/application组件,所查询的Application对象应当是满足systemId=XXXXX(system的主键)约束才对(可以到这里复习一下这两个数据结构之间的关系)。没错,这个问题非常关键。在list组件中,我们可以通过filters域来定义对数据的查询约束,像下面这样:

export default OakComponent({
    entity: 'application',
    ...,
    properties: {
        systemId: '',           // 定义组件接受一个类型为string的属性systemId
    }
    filters: [
        {
            filter() {
                const { systemId } = this.props;        // 从this.props中可以取到对应的systemId
                return {
                    systemId,
                };          // 返回的filter要满足对应Entity上的定义。
            }
        }
    ],
});

不过在Oak框架里,我们更推荐用另一种方式来处理主外键之间的关系,就是通过设置组件之间的相对路径来实现。我们查看oak-app-domain中编译出来的Application对象声明,其Schema中有这样的定义:

(oak-general-business/src/oak-app-domain/Appliction/Schema.ts)

export type Schema = EntityShape & {
    name: String<32>;
    description: Text;
    type: AppType;
    systemId: ForeignKey<"system">;
    config: WebConfig | WechatMpConfig | WechatPublicConfig | NativeConfig;
    style?: Style | null;
    domainId?: ForeignKey<"domain"> | null;
    system: System.Schema;
    ...
};

同样,在SystemSchema定义中也有:

(oak-general-business/src/oak-app-domain/System/Schema.ts)

export type Schema = EntityShape & {
    name: String<32>;
    description: Text;
    config: Config;
    platformId?: ForeignKey<"platform"> | null;
    folder: String<16>;
    super?: Boolean | null;
    style?: Style | null;
    entity?: String<32> | null;
    entityId?: String<64> | null;
    application$system?: Array<Application.Schema>;
    ...

这表示,在Application中,其system属性是一个System对象,而在System对象中,其application$system属性是Application对象的数组。在Oak框架中,这种对象之间的关系被贯彻在方方面面。在这里,我们在组件之间也可以应用这种关系,来定义组件(关联的对象)之间是什么关系。

在上面这个例子里,我们可以在system/panel组件中像这样来使用system/application组件,用oakPath来标定两者之间的关系:

...
{
    ...
    children: (
        <ApplicationList
            oakPath={`${oakFullpath}.application$system`}
            systemId={id}
        />
    ),
},
...

用了这样的定义,相当于告诉框架“此applicationList组件所查询的Application,是关联在其父结点的System对象上(框架会要求其查询数据时符合systemId = 父system.id的约束)。

当你理解了上面这个例子,就会意识到,绝大多数的页面中的组件关系,和它们所属的对象关系是完全对应的。通过仔细划分这些对象到子组件上,可以让整体代码组织的相当优雅,并且易于重用和维护。在下一章节中,我们将进一步描述这种关系的细节。