上节我们实现了事件绑定,并内置了两个动作:
我们没用弹窗展示动作:
这样当动作多了就不好展示了。
我们改一下:
新建 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 里。
在预览的时候会同时执行多个动作。
主流的低代码编辑器的添加动作的交互都是这么做的。
