feat: support param value input

This commit is contained in:
Jian Lu 2025-01-31 22:38:46 +08:00
parent f959a1aa34
commit 70a1f6b05d
19 changed files with 380 additions and 246 deletions

View File

@ -11,7 +11,7 @@ import { useEvent } from '../../event-flow';
import { useCollection } from '../../data-source/collection/CollectionProvider';
export function useFormEvents({ form }) {
const { define, emit } = useEvent();
const { define, emit, removeDefinition } = useEvent();
const collection = useCollection();
const fields = collection?.fields || [];
const fieldsMap = fields.reduce((acc, field) => {
@ -122,5 +122,7 @@ export function useFormEvents({ form }) {
}
});
// TODO remove define
define(inter);
}

View File

@ -39,8 +39,13 @@ export class EventFlowPlugin extends Plugin {
// this.app.addProviders()
// this.app.router.add()
}
static isEqual(a: EventDefinition, b: EventDefinition) {
return a.name === b.name && a.uid === b.uid;
}
// 定义事件
define(definition: EventDefinition[] | EventDefinition) {
define(definition: EventDefinition | EventDefinition[]) {
if (!definition) {
return;
}
@ -51,6 +56,17 @@ export class EventFlowPlugin extends Plugin {
}
this.definitions = uniqBy(this.definitions, (item) => item.name + item.uid);
}
// 移除定义事件
removeDefinition(definition: EventDefinition | EventDefinition[]) {
if (!definition) {
return;
}
if (Array.isArray(definition)) {
this.definitions = this.definitions.filter((item) => !definition.some((d) => EventFlowPlugin.isEqual(item, d)));
} else {
this.definitions = this.definitions.filter((item) => !EventFlowPlugin.isEqual(item, definition));
}
}
// 运行时注册事件
register(events: EventSetting[]) {
console.log('todo register', events);

View File

@ -12,7 +12,7 @@ import { Button, Input, Space } from 'antd';
import React, { useCallback } from 'react';
import { ArrayField as ArrayFieldModel } from '@formily/core';
import { CloseCircleOutlined } from '@ant-design/icons';
import { ActionParamSelect } from './ActionParamSelect';
import { ActionParamSelect } from '../components/ActionParamSelect';
import { VariableInput, getShouldChange } from '../../../schema-settings';
import { CollectionField } from '../../../data-source/collection-field';

View File

@ -17,7 +17,7 @@ import { ArrayBase } from '@formily/antd-v5';
import { useActionOptions } from './hooks/useActionOptions';
import { CloseCircleOutlined } from '@ant-design/icons';
import { ActionParamsField } from './ActionParams';
import { ActionSelect } from './ActionSelect';
import { ActionSelect } from '../components/ActionSelect';
interface LinkageRuleActionGroupProps {
type: 'button' | 'field' | 'style';

View File

@ -1,104 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useDataSourceManager } from '../../../../data-source/data-source';
import { useEvent } from '../../../hooks/useEvent';
import { EventParam, EventSetting, EventDefinition } from '../../../types';
const useCurrentEventParams: (event?: EventSetting['event']) => {
[key: string]: EventParam;
} = (event) => {
const { definitions } = useEvent();
const definition = definitions?.find((d) => d.name === event?.definition && d.uid === event.uid);
const eventParams = definition?.events?.find((e) => e.name === event?.event)?.params;
return eventParams;
};
const useStateDefine = ({ definitions }: { definitions: EventDefinition[] }) => {
const stateDefine = definitions.filter((item) => item.uid && item.states);
const stateDefineOptions = {};
stateDefine.forEach((definition) => {
stateDefineOptions[definition.uid] = {
name: definition.uid,
title: `${definition.title}-${definition.uid}`,
type: 'object',
properties: definition.states,
};
});
return { stateDefineOptions };
};
export const useFilterOptions = (recordValues: EventSetting) => {
const selectedEvent: EventSetting['event'] = recordValues?.['event'];
const currentEventParamsDefine = useCurrentEventParams(selectedEvent);
const { definitions } = useEvent();
const { stateDefineOptions } = useStateDefine({ definitions });
const options: EventParam[] = [
{
name: '_eventParams',
title: '事件参数',
type: 'object',
properties: currentEventParamsDefine,
},
{
name: '_state',
title: '组件数据',
type: 'object',
properties: stateDefineOptions,
},
// {
// name: '_system',
// title: '系统参数',
// type: 'object',
// properties: {},
// },
];
const dm = useDataSourceManager();
const getOption = (opt: EventParam) => {
if (opt.type === 'object' && opt.properties) {
return {
name: opt.name,
title: opt.title,
children: Object.keys(opt.properties).map((key) =>
getOption({
...opt.properties[key],
name: key,
}),
),
};
}
if (opt.type === 'array' && opt.items) {
//TODO: 处理数组
return {
name: opt.name,
title: opt.title,
children: [],
};
}
const fieldInterface = dm?.collectionFieldInterfaceManager.getFieldInterface(opt?.interface || opt.type);
const { nested, children, operators } = fieldInterface?.filterable || {};
const res = {
name: opt.name,
type: opt.type,
// target: opt.target,
title: opt?.uiSchema?.title || opt.title || opt.name,
schema: opt?.uiSchema,
interface: opt.interface,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(opt);
}) || [],
};
return res;
};
return options.map((opt) => getOption(opt));
};

View File

@ -25,17 +25,17 @@ import { FormProvider, createSchemaField } from '@formily/react';
import { ArrayCollapse } from '../components/LinkageHeader';
import { Filter } from '../Filter';
import { ArrayBase, Select, DatePicker, Editable, Input, ArrayItems, FormItem } from '@formily/antd-v5';
import { useFilterOptions } from './hooks/useFilterOptions';
import { useFilterOptions } from '../hooks/useFilterOptions';
import { EventDefinition, EventSetting } from '../../types';
import { useVariableOptions } from './hooks/useVariableOptions';
import { useVariableOptions } from '../hooks/useVariableOptions';
import { uniqBy } from 'lodash';
import { AddBtn, DeleteBtn } from './AddBtn';
import { ActionSelect } from './ActionSelect';
import { ActionParamSelect } from './ActionParamSelect';
import { ActionSelect } from '../components/ActionSelect';
import { ActionParamSelect } from '../components/ActionParamSelect';
import { Space } from 'antd';
import { useFormBlockContext } from '../../../block-provider';
import ConditionSelect from './ConditionSelect';
import { actionsSchema } from './schemas/actions';
import ConditionSelect from '../components/ConditionSelect';
import { actionsSchema } from '../schemas/actions';
const SchemaField = createSchemaField({
components: {
@ -71,12 +71,10 @@ export const ActionsSetting = withDynamicSchemaProps(
const array = ArrayBase.useArray();
const recordValues = ArrayBase.useRecord();
const index = ArrayBase.useIndex();
const { definitions, options, defaultValues, collectionName, variables, localVariables, record, dynamicComponent } =
props;
const { options, defaultValues, collectionName, variables, localVariables, record, dynamicComponent } = props;
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const parentRecordData = useCollectionParentRecordData();
const { form } = useFormBlockContext();
const filterOptions = useFilterOptions(recordValues);
const variableOptions = useVariableOptions();
console.log('variableOptions', variableOptions);
const components = useMemo(
@ -131,6 +129,14 @@ export const ActionsSetting = withDynamicSchemaProps(
},
condition: {
'x-component': 'ConditionSelect',
'x-reactions': {
dependencies: ['...event'],
fulfill: {
state: {
'componentProps.options3': '{{$deps[0]}}',
},
},
},
},
actionsTitle: {
'x-component': 'h4',
@ -173,7 +179,6 @@ export const ActionsSetting = withDynamicSchemaProps(
props,
record,
variables,
filterOptions,
],
);
const value = useMemo(

View File

@ -22,5 +22,6 @@ export function ActionParamSelect(props: { action: EventActionSetting }) {
value: params[key]?.name || key,
...params[key],
}));
console.log('ActionParamSelect', props);
return <Select options={options} {...rest} style={{ minWidth: 150 }}></Select>;
}

View File

@ -0,0 +1,52 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Input, Select } from 'antd';
import { EventActionSetting } from '../../types';
import { useEvent } from '../../hooks/useEvent';
import React, { useCallback } from 'react';
import { VariableInput } from '../../../schema-settings/VariableInput';
import { getShouldChange } from '../../../schema-settings/VariableInput';
import { CollectionField } from '../../../data-source/collection-field';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { SchemaComponent } from '../../../schema-component';
export const ActionParamValueInput = (props) => {
const renderSchemaComponent = useCallback(({ value, onChange }): React.JSX.Element => {
return <Input value={value} onChange={onChange} style={{ minWidth: 100 }} />;
}, []);
// const renderSchemaComponent = useCallback(({ value, onChange }): React.JSX.Element => {
// console.log('renderSchemaComponent', value, onChange);
// return <CollectionField value={value} onChange={onChange} />;
// }, []);
console.log('ActionParamValueInput props', props);
return (
<div>
<VariableInput
{...props}
// form={form}
// record={record}
shouldChange={getShouldChange({
// collectionField,
// variables,
// localVariables,
// getAllCollectionsInheritChain,
})}
renderSchemaComponent={renderSchemaComponent}
// returnScope={(scope) => {
// return uniqBy([...scope, ...variableOptions], 'key');
// }}
style={{ minWidth: 200 }}
/>
</div>
);
};

View File

@ -10,7 +10,7 @@
import { observer } from '@formily/react';
import { Cascader } from 'antd';
import React from 'react';
import { useActionOptions } from './hooks/useActionOptions';
import { useActionOptions } from '../hooks/useActionOptions';
import { EventActionSetting } from '../../types';
export const ActionSelect = observer((props: any) => {

View File

@ -7,29 +7,35 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { Filter, useFilterOptions } from '../Filter';
import React, { useEffect } from 'react';
import { Filter } from '../Filter';
import { useFilterOptions } from '../hooks/useFilterOptions';
import { ArrayBase } from '@formily/antd-v5';
import { css } from '@emotion/css';
import { getShouldChange } from '../../../schema-settings/VariableInput';
import { DynamicComponentProps } from '../../../schema-component/antd/filter/DynamicComponent';
import { VariableInput } from '../../../schema-settings/VariableInput';
import { uniqBy } from 'lodash';
import { useVariableOptions } from './hooks/useVariableOptions';
import { useVariableOptions } from '../hooks/useVariableOptions';
import { EventSetting } from '../../types';
import { useField } from '@formily/react';
import { ObjectField } from '@formily/core';
import { useUpdateEffect } from 'ahooks';
export default function ConditionSelect(props: any) {
const recordValues = ArrayBase.useRecord();
const filterOptions = useFilterOptions(recordValues);
export default function ConditionSelect(props: { event?: EventSetting['event']; onChange?: any }) {
const filterOptions = useFilterOptions(props.event);
const variableOptions = useVariableOptions();
console.log('filterOptions', filterOptions, props);
const field = useField<ObjectField>();
useUpdateEffect(() => {
// 当 event 变化时,清空 condition
// TODO: 条件判定进行清空
field.value = {};
}, [JSON.stringify(props.event)]);
return (
<Filter
className={css`
position: relative;
width: 100%;
margin-left: 10px;
`}
options={filterOptions}
dynamicComponent={(props: DynamicComponentProps) => {
const { collectionField } = props;
return (

View File

@ -1,44 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ArrayBase } from '@formily/antd-v5';
import { Switch } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
export const EnableLinkage = React.forwardRef((props: any, ref) => {
const array = ArrayBase.useArray();
const index = ArrayBase.useIndex(props.index);
const { t } = useTranslation();
return (
<Switch
{...props}
checkedChildren={t('On')}
unCheckedChildren={t('Off')}
checked={!array?.field?.value[index].disabled}
size={'small'}
style={{
transition: 'all 0.25s ease-in-out',
color: 'rgba(0, 0, 0, 0.8)',
fontSize: 16,
marginLeft: 6,
marginBottom: 3,
}}
onChange={(checked, e) => {
e.stopPropagation();
array.field.value.splice(index, 1, { ...array?.field?.value[index], disabled: !checked });
}}
onClick={(checked, e) => {
e.stopPropagation();
}}
/>
);
});
EnableLinkage.displayName = 'EnableLinkage';

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useEvent } from '../../../hooks/useEvent';
import { useEvent } from '../../hooks/useEvent';
export function useActionOptions() {
const { definitions } = useEvent();

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useEvent } from '../../../hooks/useEvent';
import { useEvent } from '../../hooks/useEvent';
export function useActionOptions() {
const { definitions } = useEvent();

View File

@ -7,6 +7,97 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export const useFilterOptions = () => {
return { filterOptions: [] };
import { useDataSourceManager } from '../../../data-source/data-source';
import { useEvent } from '../../hooks/useEvent';
import { EventParam, EventSetting, EventDefinition } from '../../types';
const useCurrentEventParams: (event?: EventSetting['event']) => {
[key: string]: EventParam;
} = (event) => {
const { definitions } = useEvent();
const definition = definitions?.find((d) => d.name === event?.definition && d.uid === event.uid);
const eventParams = definition?.events?.find((e) => e.name === event?.event)?.params;
return eventParams;
};
const useStateDefine = ({ definitions }: { definitions: EventDefinition[] }) => {
const stateDefine = definitions.filter((item) => item.uid && item.states);
const stateDefineOptions = {};
stateDefine.forEach((definition) => {
stateDefineOptions[definition.uid] = {
name: definition.uid,
title: `${definition.title}-${definition.uid}`,
type: 'object',
properties: definition.states,
};
});
return { stateDefineOptions };
};
export const useFilterOptions = (event?: EventSetting['event']) => {
const currentEventParamsDefine = useCurrentEventParams(event);
const { definitions } = useEvent();
const { stateDefineOptions } = useStateDefine({ definitions });
const options: EventParam[] = [
{
name: '_eventParams',
title: '事件参数',
type: 'object',
properties: currentEventParamsDefine,
},
{
name: '_state',
title: '组件数据',
type: 'object',
properties: stateDefineOptions,
},
// {
// name: '_system',
// title: '系统参数',
// type: 'object',
// properties: {},
// },
];
const dm = useDataSourceManager();
const getOption = (opt: EventParam) => {
if (opt.type === 'object' && opt.properties) {
return {
name: opt.name,
title: opt.title,
children: Object.keys(opt.properties).map((key) =>
getOption({
...opt.properties[key],
name: key,
}),
),
};
}
if (opt.type === 'array' && opt.items) {
//TODO: 处理数组
return {
name: opt.name,
title: opt.title,
children: [],
};
}
const fieldInterface = dm?.collectionFieldInterfaceManager.getFieldInterface(opt?.interface || opt.type);
const { nested, children, operators } = fieldInterface?.filterable || {};
const res = {
name: opt.name,
type: opt.type,
// target: opt.target,
title: opt?.uiSchema?.title || opt.title || opt.name,
schema: opt?.uiSchema,
interface: opt.interface,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(opt);
}) || [],
};
return res;
};
return options.map((opt) => getOption(opt));
};

View File

@ -9,8 +9,8 @@
import { useCompile } from '@nocobase/client';
import { useMemo } from 'react';
import { useEvent } from '../../../hooks/useEvent';
import { EventParam } from '../../../types';
import { useEvent } from '../../hooks/useEvent';
import { EventParam } from '../../types';
export const useVariableOptions = () => {
const { definitions } = useEvent();

View File

@ -20,8 +20,9 @@ import {
useLocalVariables,
useFormBlockType,
useRecord,
Filter,
} from '@nocobase/client';
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { ISchema, useField } from '@formily/react';
import { SchemaSettingsKey, useEvent } from '..';
import { useFieldSchema } from '@formily/react';
@ -30,8 +31,13 @@ import { ActionsSetting } from './ActionsSetting';
import EventSelect from './EventSelect';
import { ArrayCollapse } from './components/LinkageHeader';
import { css } from '@emotion/css';
import { useFilterOptions } from './hooks/useFilterOptions';
import { FormItem, FormLayout } from '@formily/antd-v5';
import { rulesSchema } from './schemas/rules';
import { ActionSelect } from './components/ActionSelect';
import { ActionParamSelect } from './components/ActionParamSelect';
import ConditionSelect from './components/ConditionSelect';
import { Space } from 'antd';
import { ActionParamValueInput } from './components/ActionParamValueInput';
// packages/core/client/src/schema-settings/SchemaSettings.tsx
export const EventSettingItem = (props) => {
@ -51,13 +57,29 @@ export const EventSettingItem = (props) => {
const variables = useVariables();
const localVariables = useLocalVariables();
const { type: formBlockType } = useFormBlockType();
const record = useRecord();
return (
<SchemaSettingsModalItem
title={'Event Setting'}
components={{ ArrayCollapse, ActionsSetting, EventSelect, FormLayout }}
initialValues={{}}
scope={{}}
components={{
ArrayCollapse,
// ActionsSetting,
EventSelect,
// FormLayout,
// Filter,
Space,
ActionParamSelect,
ActionSelect,
ConditionSelect,
ActionParamValueInput,
}}
initialValues={{ events: schema[SchemaSettingsKey] }}
scope={{
emptyParams: (field, target) => {
const params = field.query('.params').take(1);
params.value = [];
},
}}
width={1000}
schema={
{
@ -78,59 +100,42 @@ export const EventSettingItem = (props) => {
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
// extra: <EnableLinkage />,
},
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
eventTitle: {
'x-component': 'h4',
'x-content': '{{ t("触发事件") }}',
},
event: {
'x-component': EventSelect,
'x-component-props': {
labelStyle: {
marginTop: '4px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
eventTitle: {
'x-component': 'h4',
'x-content': '{{ t("触发事件") }}',
},
event: {
'x-component': EventSelect,
'x-component-props': {
definitions,
className: css`
margin-bottom: 12px;
`,
},
},
actionTitle: {
'x-component': 'h4',
'x-content': '{{ t("执行动作") }}',
},
actions: {
type: 'void',
'x-component': ActionsSetting,
'x-use-component-props': () => {
return {
definitions,
options,
linkageOptions,
category: 'default',
elementType: 'field',
collectionName,
// form,
variables,
localVariables,
record,
formBlockType,
};
},
},
definitions,
className: css`
margin-bottom: 12px;
`,
},
},
actionTitle: {
'x-component': 'h4',
'x-content': '{{ t("执行规则") }}',
},
actionsBlock: rulesSchema,
// actionsBlock: {
// type: 'void',
// 'x-component': ActionsSetting,
// 'x-use-component-props': () => {
// return {
// options,
// linkageOptions,
// category: 'default',
// elementType: 'field',
// collectionName,
// // form,
// variables,
// localVariables,
// formBlockType,
// };
// },
// },
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
@ -140,7 +145,7 @@ export const EventSettingItem = (props) => {
properties: {
add: {
type: 'void',
title: '{{ t("Add events") }}',
title: '{{ t("添加事件") }}',
'x-component': 'ArrayCollapse.Addition',
},
},
@ -150,15 +155,15 @@ export const EventSettingItem = (props) => {
}
onSubmit={({ events }) => {
console.log('todo onSubmit', JSON.parse(JSON.stringify(events)));
// fieldSchema[SchemaSettingsKey] = events;
// dn.emit('patch', {
// schema: {
// 'x-uid': fieldSchema['x-uid'],
// [SchemaSettingsKey]: events,
// },
// });
fieldSchema[SchemaSettingsKey] = events;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
[SchemaSettingsKey]: events,
},
});
// register(events);
// dn.refresh();
dn.refresh();
}}
/>
);

View File

@ -7,6 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ActionParamValueInput } from '../components/ActionParamValueInput';
const actionParamsSchema = {
type: 'void',
properties: {
@ -22,7 +24,7 @@ const actionParamsSchema = {
},
},
properties: {
key: {
name: {
type: 'string',
'x-component': 'ActionParamSelect',
'x-reactions': {
@ -36,7 +38,21 @@ const actionParamsSchema = {
},
},
},
value: { type: 'string', 'x-component': 'Input' },
value: {
type: 'string',
'x-component': 'ActionParamValueInput',
'x-reactions': {
dependencies: ['...action', '.name'],
fulfill: {
schema: {
'x-component-props': {
action: '{{$deps[0]}}',
paramName: '{{$deps[1]}}',
},
},
},
},
},
removeBtn: {
type: 'void',
'x-component': 'ArrayItems.Remove',
@ -62,6 +78,11 @@ export const actionsSchema = {
actions: {
type: 'array',
'x-component': 'ArrayItems',
'x-component-props': {
style: {
marginLeft: 12,
},
},
items: {
type: 'object',
'x-component': 'Space',

View File

@ -0,0 +1,81 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { css } from '@emotion/css';
import { actionsSchema } from './actions';
export const rulesSchema = {
type: 'void',
properties: {
rules: {
type: 'array',
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
titleRender: (item: any, index: number) => {
return `规则 ${index + 1}`;
},
showEmpty: false,
},
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
properties: {
conditionsTitle: {
'x-component': 'h4',
'x-content': '{{ t("Condition") }}',
},
condition: {
'x-component': 'ConditionSelect',
'x-component-props': {
className: css`
position: relative;
width: 100%;
margin-left: 12px;
`,
},
'x-reactions': {
dependencies: ['...event'],
fulfill: {
state: {
'componentProps.event': '{{$deps[0]}}',
},
},
},
},
actionsTitle: {
'x-component': 'h4',
'x-content': '{{ t("动作") }}',
},
actionsBlock: actionsSchema,
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("添加规则") }}',
'x-component': 'ArrayCollapse.Addition',
// 'x-reactions': {
// dependencies: ['rules'],
// fulfill: {
// state: {
// disabled: '{{$deps[0].length >= 3}}',
// },
// },
// },
},
},
},
},
};

View File

@ -43,6 +43,8 @@ export const useEvent = () => {
definitions: eventFlowPlugin?.definitions,
// 定义事件
define,
// 移除事件
removeDefinition: eventFlowPlugin?.removeDefinition,
// 运行时事件注册
register,
// 触发事件