Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

编写详情组件

本节示例代码可参看 oak-general-business/src/components/system/detail,其父组件是 oak-general-business/src/components/system/panel

现在我们需要做一个用于查看 System 信息的详情组件。这里要先说明一个真实的公共组件写法:system/detail 并不是自己单独负责首轮取数,而是复用 system/panel 已经建立好的结点和 projection。

也就是说:

  • system/panel 负责声明 entity: 'system'、完整 projection 和根路径;
  • system/detail 通过复用同一条 oakPath,在这个结点上继续做展示和提交入口;
  • system/upsert 也会挂到同一条路径上,负责编辑。

这个模式在 Oak 组件里很常见:一个父容器组件负责稳定取数,多个子组件共享同一条对象上下文。

逻辑层(index.ts)

system/detail/index.ts 的真实代码大致如下:

export default OakComponent({
    isList: false,
    entity: 'system',
    formData({ data }) {
        return {
            ...data,
            oakExecutable: this.tryExecute(),
        };
    },
});

这里有两个关键信息:

  1. 这是一个单行组件,所以 isListfalse
  2. 它没有自己声明 projection,因为当前组件通常挂在 system/panel 的同一路径上,父组件已经把需要的字段取好了。

同时它在 formData 中额外返回了:

oakExecutable: this.tryExecute()

这也是 Oak 项目里非常常见的写法。它表示:

  • 当前结点上如果存在待提交修改;
  • 并且这些修改通过了 checker / 权限检查;
  • 那么渲染层就可以据此决定“确认按钮是否可点”。

这也再次说明:oakExecutable 并不是所有组件自动拥有的内置数据项,很多时候是组件作者自己在 formData 中计算并返回的。

渲染层(web.pc.tsx)

system/detail/web.pc.tsx 中,组件一方面展示详情,一方面提供一个“打开编辑弹窗并提交”的入口。核心代码大致如下:

export default function Render(props) {
    const {
        oakId,
        name,
        description,
        oakFullpath,
        oakExecutable,
        oakExecuting,
    } = props.data;
    const { t, execute, clean } = props.methods;

    return (
        <>
            <Modal
                open={open}
                onCancel={() => {
                    clean();
                    setOpen(false);
                }}
                footer={
                    <Space>
                        <Button
                            onClick={() => {
                                clean();
                                setOpen(false);
                            }}
                            disabled={oakExecuting}
                        >
                            {t('common::action.cancel')}
                        </Button>
                        <Button
                            type="primary"
                            onClick={async () => {
                                await execute();
                                setOpen(false);
                            }}
                            disabled={oakExecutable !== true || oakExecuting}
                        >
                            {t('common::action.confirm')}
                        </Button>
                    </Space>
                }
            >
                <SystemUpsert oakId={oakId} oakPath={oakFullpath} />
            </Modal>
            ...
        </>
    );
}

当前 Oak 编译器会根据同目录 index.ts 中的 entityisListpropertiesformDatamethods 为传统 TSX render 注入 props.data / props.methods 的精确类型。因此这里不再手写 WebComponentProps,也不应保留仅为旧签名服务的 WebComponentPropsEntityDict 类型导入。业务输入仍必须在 index.tsproperties 中声明,不能因为 render 能取到某个字段就省略组件契约。

这里最值得学的不是具体 UI,而是这三个运行时动作:

  • clean():取消时清理当前路径上的未提交修改;
  • execute():提交当前路径上的修改;
  • <SystemUpsert oakId={oakId} oakPath={oakFullpath} />:把编辑组件挂到和详情组件相同的结点上。

也就是说,在这个例子里:

  • 详情组件负责打开弹窗、显示按钮状态、发起提交;
  • 更新组件负责写入待提交的数据;
  • 两者通过相同的 oakPath 共享同一个对象结点。

这个组件是如何被使用的

真正负责首轮取数的是 system/panel。在它的 web.pc.tsx 中,可以看到类似下面的写法:

<SystemDetail
    oakId={id}
    oakPath={oakFullpath}
/>

这里:

  • oakId 指向当前 System 的主键;
  • oakPath 复用 system/panel 已建立好的那条路径。

这也是为什么 system/detail 自己可以不再声明 projection

这个例子应该记住什么

  1. 详情组件不一定要自己负责 projection,完全可以复用父组件的对象结点。
  2. 详情组件在 Oak 中经常不只是“展示”,还会兼任“提交入口”。
  3. 如果详情组件和更新组件共享同一条 oakPath,就可以把编辑结果直接写到同一个 runningTree 结点上,再由详情组件统一 execute()

这一模式在后台管理类页面里非常常见。下一节的更新组件,就是这个例子里被弹窗打开的 system/upsert