• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节我们实现了事件绑定,并内置了两个动作:

我们没用弹窗展示动作:

这样当动作多了就不好展示了。

我们改一下:

新建 Setting/ActionModal.tsx

import { Modal, Segmented } from "antd";
import { useState } from "react";
import { GoToLink } from "./actions/GoToLink";
import { ComponentEvent } from "../../stores/component-config";
import { ShowMessage } from "./actions/ShowMessage";

interface ActionModalProps {
    visible: boolean;
    eventConfig: ComponentEvent;
    handleOk: () => void;
    handleCancel: () => void;
}

export function ActionModal(props: ActionModalProps) {
    const { visible, handleOk, eventConfig, handleCancel } = props;

    const [key, setKey] = useState<string>("访问链接");

    return (
        <Modal
            title="事件动作配置"
            width={800}
            open={visible}
            okText="添加"
            cancelText="取消"
            onOk={handleOk}
            onCancel={handleCancel}>
            <div className="h-[500px]">
                <Segmented
                    value={key}
                    onChange={setKey}
                    block
                    options={["访问链接", "消息提示", "自定义 JS"]}
                />
                {key === "访问链接" && <GoToLink event={eventConfig} />}
                {key === "消息提示" && <ShowMessage event={eventConfig} />}
            </div>
        </Modal>
    );
}

就是展示所有的动作,当选择某个动作,输入内容后,修改对应的 event 配置。

在 ComponentEvent 里调用下:

加一个 state 来控制弹窗打开关闭。

再加一个 state 来记录当前的 event 配置,当点击 label 的添加动作按钮的时候,打开弹窗,记录当前 event

import { Collapse, Input, Select, CollapseProps, Button } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";
import type { ComponentEvent } from "../../stores/component-config";
import { ActionModal } from "./ActionModal";
import { useState } from "react";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();
    const [actionModalOpen, setActionModalOpen] = useState(false);
    const [curEvent, setCurEvent] = useState<ComponentEvent>();

    if (!curComponent) return null;

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: (
                <div className="flex justify-between leading-[30px]">
                    {event.label}
                    <Button
                        type="primary"
                        onClick={() => {
                            setCurEvent(event);
                            setActionModalOpen(true);
                        }}>
                        添加动作
                    </Button>
                </div>
            ),
            children: <div></div>,
        };
    });

    return (
        <div className="px-[10px]">
            <Collapse className="mb-[10px]" items={items} />
            <ActionModal
                visible={actionModalOpen}
                eventConfig={curEvent!}
                handleOk={() => {
                    setActionModalOpen(false);
                }}
                handleCancel={() => {
                    setActionModalOpen(false);
                }}
            />
        </div>
    );
}

试一下:

展示出来了,就是有点小。

我们把表单改大一点:

import { Input } from "antd";
import { ComponentEvent } from "../../../stores/component-config";
import { useComponetsStore } from "../../../stores/components";
import TextArea from "antd/es/input/TextArea";

export function GoToLink(props: { event: ComponentEvent }) {
    const { event } = props;

    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();

    function urlChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                url: value,
            },
        });
    }

    return (
        <div className="mt-[40px]">
            <div className="flex items-center gap-[10px]">
                <div>跳转链接</div>
                <div>
                    <TextArea
                        style={{
                            height: 200,
                            width: 500,
                            border: "1px solid #000",
                        }}
                        onChange={(e) => {
                            urlChange(event.name, e.target.value);
                        }}
                        value={curComponent?.props?.[event.name]?.url}
                    />
                </div>
            </div>
        </div>
    );
}
import { Input, Select } from "antd";
import { ComponentEvent } from "../../../stores/component-config";
import { useComponetsStore } from "../../../stores/components";

export function ShowMessage(props: { event: ComponentEvent }) {
    const { event } = props;

    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();

    function messageTypeChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                config: {
                    ...curComponent?.props?.[eventName]?.config,
                    type: value,
                },
            },
        });
    }

    function messageTextChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                config: {
                    ...curComponent?.props?.[eventName]?.config,
                    text: value,
                },
            },
        });
    }

    return (
        <div className="mt-[30px]">
            <div className="flex items-center gap-[20px]">
                <div>类型:</div>
                <div>
                    <Select
                        style={{ width: 500, height: 50 }}
                        options={[
                            { label: "成功", value: "success" },
                            { label: "失败", value: "error" },
                        ]}
                        onChange={(value) => {
                            messageTypeChange(event.name, value);
                        }}
                        value={curComponent?.props?.[event.name]?.config?.type}
                    />
                </div>
            </div>
            <div className="flex items-center gap-[20px] mt-[50px]">
                <div>文本:</div>
                <div>
                    <Input
                        style={{ width: 500, height: 50 }}
                        onChange={(e) => {
                            messageTextChange(event.name, e.target.value);
                        }}
                        value={curComponent?.props?.[event.name]?.config?.text}
                    />
                </div>
            </div>
        </div>
    );
}

看下效果:

好多了。

之前我们是在 action 组件里直接修改 json,

现在改为通过 onChange 暴露出来,然后后面在点添加按钮的时候再改 json:

import { useState } from "react";
import { useComponetsStore } from "../../../stores/components";
import TextArea from "antd/es/input/TextArea";

export interface GoToLinkConfig {
    type: "goToLink";
    url: string;
}

export interface GoToLinkProps {
    defaultValue?: string;
    onChange?: (config: GoToLinkConfig) => void;
}

export function GoToLink(props: GoToLinkProps) {
    const { defaultValue, onChange } = props;

    const { curComponentId } = useComponetsStore();
    const [value, setValue] = useState(defaultValue);

    function urlChange(value: string) {
        if (!curComponentId) return;

        setValue(value);

        onChange?.({
            type: "goToLink",
            url: value,
        });
    }

    return (
        <div className="mt-[40px]">
            <div className="flex items-center gap-[10px]">
                <div>跳转链接</div>
                <div>
                    <TextArea
                        style={{
                            height: 200,
                            width: 500,
                            border: "1px solid #000",
                        }}
                        onChange={(e) => {
                            urlChange(e.target.value);
                        }}
                        value={value || ""}
                    />
                </div>
            </div>
        </div>
    );
}

现在不用传入 event 配置了,传入回显的 value 就行。

ShowMessage 组件也是这样改:

import { Input, Select } from "antd";
import { useComponetsStore } from "../../../stores/components";
import { useState } from "react";

export interface ShowMessageConfig {
    type: "showMessage";
    config: {
        type: "success" | "error";
        text: string;
    };
}

export interface ShowMessageProps {
    value?: ShowMessageConfig["config"];
    onChange?: (config: ShowMessageConfig) => void;
}

export function ShowMessage(props: ShowMessageProps) {
    const { value, onChange } = props;

    const { curComponentId } = useComponetsStore();

    const [type, setType] = useState<"success" | "error">(
        value?.type || "success"
    );
    const [text, setText] = useState<string>(value?.text || "");

    function messageTypeChange(value: "success" | "error") {
        if (!curComponentId) return;

        setType(value);

        onChange?.({
            type: "showMessage",
            config: {
                type: value,
                text,
            },
        });
    }

    function messageTextChange(value: string) {
        if (!curComponentId) return;

        setText(value);

        onChange?.({
            type: "showMessage",
            config: {
                type,
                text: value,
            },
        });
    }

    return (
        <div className="mt-[30px]">
            <div className="flex items-center gap-[20px]">
                <div>类型:</div>
                <div>
                    <Select
                        style={{ width: 500, height: 50 }}
                        options={[
                            { label: "成功", value: "success" },
                            { label: "失败", value: "error" },
                        ]}
                        onChange={(value) => {
                            messageTypeChange(value);
                        }}
                        value={type}
                    />
                </div>
            </div>
            <div className="flex items-center gap-[20px] mt-[50px]">
                <div>文本:</div>
                <div>
                    <Input
                        style={{ width: 500, height: 50 }}
                        onChange={(e) => {
                            messageTextChange(e.target.value);
                        }}
                        value={text}
                    />
                </div>
            </div>
        </div>
    );
}

试一下:

{
    key === "访问链接" && (
        <GoToLink
            onChange={(config) => {
                console.log(config);
            }}
        />
    );
}
{
    key === "消息提示" && (
        <ShowMessage
            onChange={(config) => {
                console.log(config);
            }}
        />
    );
}

现在选择某个动作,填入配置的时候,在 ActionModal 里就能拿到。

那接下来只要在 handleOk 里传出去,然后父组件里加到 store 就可以了。

import { Modal, Segmented } from "antd";
import { useState } from "react";
import { GoToLink, GoToLinkConfig } from "./actions/GoToLink";
import { ComponentEvent } from "../../stores/component-config";
import { ShowMessage, ShowMessageConfig } from "./actions/ShowMessage";

interface ActionModalProps {
    visible: boolean;
    handleOk: (config?: GoToLinkConfig | ShowMessageConfig) => void;
    handleCancel: () => void;
}

export function ActionModal(props: ActionModalProps) {
    const { visible, handleOk, handleCancel } = props;

    const [key, setKey] = useState<string>("访问链接");
    const [curConfig, setCurConfig] = useState<
        GoToLinkConfig | ShowMessageConfig
    >();

    return (
        <Modal
            title="事件动作配置"
            width={800}
            open={visible}
            okText="确认"
            cancelText="取消"
            onOk={() => handleOk(curConfig)}
            onCancel={handleCancel}>
            <div className="h-[500px]">
                <Segmented
                    value={key}
                    onChange={setKey}
                    block
                    options={["访问链接", "消息提示", "自定义 JS"]}
                />
                {key === "访问链接" && (
                    <GoToLink
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
                {key === "消息提示" && (
                    <ShowMessage
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
            </div>
        </Modal>
    );
}

在父组件里添加到 store 里:

import { Collapse, Input, Select, CollapseProps, Button } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";
import type { ComponentEvent } from "../../stores/component-config";
import { ActionModal } from "./ActionModal";
import { useState } from "react";
import { GoToLinkConfig } from "./actions/GoToLink";
import { ShowMessageConfig } from "./actions/ShowMessage";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();
    const [actionModalOpen, setActionModalOpen] = useState(false);
    const [curEvent, setCurEvent] = useState<ComponentEvent>();

    if (!curComponent) return null;

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: (
                <div className="flex justify-between leading-[30px]">
                    {event.label}
                    <Button
                        type="primary"
                        onClick={() => {
                            setCurEvent(event);
                            setActionModalOpen(true);
                        }}>
                        添加动作
                    </Button>
                </div>
            ),
            children: <div></div>,
        };
    });

    function handleModalOk(config?: GoToLinkConfig | ShowMessageConfig) {
        if (!config || !curEvent || !curComponent) {
            return;
        }

        updateComponentProps(curComponent.id, {
            [curEvent.name]: {
                actions: [
                    ...(curComponent.props[curEvent.name]?.actions || []),
                    config,
                ],
            },
        });

        setActionModalOpen(false);
    }

    return (
        <div className="px-[10px]">
            <Collapse className="mb-[10px]" items={items} />
            <ActionModal
                visible={actionModalOpen}
                handleOk={handleModalOk}
                handleCancel={() => {
                    setActionModalOpen(false);
                }}
            />
        </div>
    );
}

试一下:

现在的 json 结构就支持多个动作了:

和 amis 的一样:

然后我们也做下这个列表展示:

children: <div>
    {(curComponent.props[event.name]?.actions || []).map(
        (item: GoToLinkConfig | ShowMessageConfig) => {
            return (
                <div>
                    {item.type === "goToLink" ? (
                        <div className="border border-[#aaa] m-[10px] p-[10px]">
                            <div className="text-[blue]">跳转链接</div>
                            <div>{item.url}</div>
                        </div>
                    ) : null}
                    {item.type === "showMessage" ? (
                        <div className="border border-[#aaa] m-[10px] p-[10px]">
                            <div className="text-[blue]">消息弹窗</div>
                            <div>{item.config.type}</div>
                            <div>{item.config.text}</div>
                        </div>
                    ) : null}
                </div>
            );
        }
    )}
</div>;

列表展示没问题。

只是每次都会触发展开收起。

我们加一个 defaultActiveKey 让所有的都展开:

defaultActiveKey={componentConfig[curComponent.name].events?.map(item =>item.name)}

然后禁止点击事件冒泡,这样点击按钮就不会收起 Collapse 了:

然后在 Preview 组件里处理下事件绑定:

function handleEvent(component: Component) {
    const props: Record<string, any> = {};

    componentConfig[component.name].events?.forEach((event) => {
        const eventConfig = component.props[event.name];

        if (eventConfig) {
            props[event.name] = () => {
                eventConfig?.actions?.forEach(
                    (action: GoToLinkConfig | ShowMessageConfig) => {
                        if (action.type === "goToLink") {
                            window.location.href = action.url;
                        } else if (action.type === "showMessage") {
                            if (action.config.type === "success") {
                                message.success(action.config.text);
                            } else if (action.config.type === "error") {
                                message.error(action.config.text);
                            }
                        }
                    }
                );
            };
        }
    });
    return props;
}

相比之前,就是多了个遍历的过程。

测试下:

添加两个消息提示的动作,可以看到,两个动作都执行了。

最后我们再做下动作的删除就好了:

通过绝对定位在右上角显示一个删除按钮,点击按钮删除对应 index 的 action。

import { Collapse, Input, Select, CollapseProps, Button } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";
import type { ComponentEvent } from "../../stores/component-config";
import { ActionModal } from "./ActionModal";
import { useState } from "react";
import { GoToLinkConfig } from "./actions/GoToLink";
import { ShowMessageConfig } from "./actions/ShowMessage";
import { DeleteOutlined } from "@ant-design/icons";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();
    const [actionModalOpen, setActionModalOpen] = useState(false);
    const [curEvent, setCurEvent] = useState<ComponentEvent>();

    if (!curComponent) return null;

    function deleteAction(event: ComponentEvent, index: number) {
        if (!curComponent) {
            return;
        }

        const actions = curComponent.props[event.name]?.actions;

        actions.splice(index, 1);

        updateComponentProps(curComponent.id, {
            [event.name]: {
                actions: actions,
            },
        });
    }

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: (
                <div className="flex justify-between leading-[30px]">
                    {event.label}
                    <Button
                        type="primary"
                        onClick={(e) => {
                            e.stopPropagation();

                            setCurEvent(event);
                            setActionModalOpen(true);
                        }}>
                        添加动作
                    </Button>
                </div>
            ),
            children: (
                <div>
                    {(curComponent.props[event.name]?.actions || []).map(
                        (
                            item: GoToLinkConfig | ShowMessageConfig,
                            index: number
                        ) => {
                            return (
                                <div>
                                    {item.type === "goToLink" ? (
                                        <div className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                跳转链接
                                            </div>
                                            <div>{item.url}</div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                    {item.type === "showMessage" ? (
                                        <div className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                消息弹窗
                                            </div>
                                            <div>{item.config.type}</div>
                                            <div>{item.config.text}</div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                </div>
                            );
                        }
                    )}
                </div>
            ),
        };
    });

    function handleModalOk(config?: GoToLinkConfig | ShowMessageConfig) {
        if (!config || !curEvent || !curComponent) {
            return;
        }

        updateComponentProps(curComponent.id, {
            [curEvent.name]: {
                actions: [
                    ...(curComponent.props[curEvent.name]?.actions || []),
                    config,
                ],
            },
        });

        setActionModalOpen(false);
    }

    return (
        <div className="px-[10px]">
            <Collapse
                className="mb-[10px]"
                items={items}
                defaultActiveKey={componentConfig[
                    curComponent.name
                ].events?.map((item) => item.name)}
            />
            <ActionModal
                visible={actionModalOpen}
                handleOk={handleModalOk}
                handleCancel={() => {
                    setActionModalOpen(false);
                }}
            />
        </div>
    );
}

删除成功,json 也修改了。

案例代码上传了小册仓库,可以切换到这个 commit 查看:

git reset --hard c85c9913270242f216ec28d18f03cb64887475b4

总结

之前我们是直接在 Setting 区域展示的动作表单,动作多了以后不好展示,这节我们实现了动作选择弹窗。

选择一个动作,填入信息之后,点击添加就可以添加到 actions 里。

在预览的时候会同时执行多个动作。

主流的低代码编辑器的添加动作的交互都是这么做的。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
73.低代码编辑器:事件绑定
Next
75.低代码编辑器:自定义JS