diff --git a/apps/playground/src/helpers/mock-files.ts b/apps/playground/src/helpers/mock-files.ts index 3f36dc65..9d6ecc4a 100644 --- a/apps/playground/src/helpers/mock-files.ts +++ b/apps/playground/src/helpers/mock-files.ts @@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot"; import { Page, Section, + Box, Button, Input, FormilyForm, @@ -167,7 +168,9 @@ class App extends React.Component { render() { return ( }> -
+
+ +
your input: copy input: diff --git a/apps/playground/src/helpers/prototypes.ts b/apps/playground/src/helpers/prototypes.ts index a3a0793a..485afc9f 100644 --- a/apps/playground/src/helpers/prototypes.ts +++ b/apps/playground/src/helpers/prototypes.ts @@ -200,6 +200,13 @@ const prototypes: Dict = { title: 'd', setter: 'textSetter', }, + { + name: 'onClick', + title: '点击事件', + setter: 'eventSetter', + template: '(e) => {\n {{content}}\n}', + tip: '回调参数说明:e 为事件对象', + }, ], }, Columns: { diff --git a/apps/storybook/.storybook/preview.js b/apps/storybook/.storybook/preview.js index 3c648495..64c7244b 100644 --- a/apps/storybook/.storybook/preview.js +++ b/apps/storybook/.storybook/preview.js @@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system'; import 'antd/dist/antd.css'; export const parameters = { - actions: { argTypesRegex: '^on[A-Z].*' }, + actions: { argTypesRegex: '^on.*' }, controls: { matchers: { color: /(background|color)$/i, diff --git a/apps/storybook/src/ui/action-select.stories.tsx b/apps/storybook/src/ui/action-select.stories.tsx new file mode 100644 index 00000000..2e4c758d --- /dev/null +++ b/apps/storybook/src/ui/action-select.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { ActionSelect } from '@music163/tango-ui'; + +export default { + title: 'UI/ActionSelect', + component: ActionSelect, +}; + +const options = [ + { label: 'action1', value: 'action1' }, + { label: 'action2', value: 'action2' }, + { label: 'action3', value: 'action3' }, +]; + +export const Basic = { + args: { + defaultText: '选择动作', + options, + onSelect: console.log, + }, +}; + +export const showInput = { + args: { + text: '选择动作', + options, + showInput: true, + }, +}; diff --git a/packages/designer/src/components/components-popover.tsx b/packages/designer/src/components/components-popover.tsx index cb7866b2..a631935a 100644 --- a/packages/designer/src/components/components-popover.tsx +++ b/packages/designer/src/components/components-popover.tsx @@ -34,17 +34,17 @@ export const ComponentsPopover = observer( // eslint-disable-next-line @typescript-eslint/consistent-type-assertions workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype); - // 推荐使用的子组件 - const insertedList = useMemo( - () => - Array.isArray(prototype?.childrenName) - ? prototype?.childrenName - : [prototype?.childrenName].filter(Boolean), - [prototype?.childrenName], - ); - - // 推荐使用的代码片段 - const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]); + const recommendedList = useMemo(() => { + if (type === 'inner') { + return prototype?.childrenName + ? Array.isArray(prototype?.childrenName) + ? prototype.childrenName + : [prototype.childrenName] + : []; + } + // 默认推荐使用相同类型的组件作为兄弟节点 + return prototype.siblingNames || [prototype.name]; + }, [prototype.childrenName, prototype.siblingNames, prototype.name, type]); const tipsTextMap = useMemo( () => ({ @@ -82,22 +82,15 @@ export const ComponentsPopover = observer( const menuList = JSON.parse(JSON.stringify(designer.menuData)); const commonList = menuList['common'] ?? []; - if (commonList?.length && siblingList?.length) { - commonList.unshift({ - title: '代码片段', - items: siblingList, - }); - } - - if (commonList?.length && insertedList?.length) { + if (commonList?.length && recommendedList.length) { commonList.unshift({ title: '推荐使用', - items: insertedList, + items: recommendedList, }); } return menuList; - }, [insertedList, siblingList, designer.menuData]); + }, [recommendedList, designer.menuData]); const innerTypeProps = // 手动触发 适用于 点击添加组件 diff --git a/packages/designer/src/setters/event-setter.tsx b/packages/designer/src/setters/event-setter.tsx index 793eaac0..b332364d 100644 --- a/packages/designer/src/setters/event-setter.tsx +++ b/packages/designer/src/setters/event-setter.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useMemo, useState } from 'react'; import { css, Box, Text } from 'coral-system'; -import { AutoComplete } from 'antd'; +import { AutoComplete, Input } from 'antd'; import { ActionSelect } from '@music163/tango-ui'; import { FormItemComponentProps } from '@music163/tango-setting-form'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { Dict, wrapCode } from '@music163/tango-helpers'; -import { ExpressionPopover } from './expression-setter'; +import { ExpressionPopover, getCallbackValue } from './expression-setter'; import { value2code } from '@music163/tango-core'; enum EventAction { @@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps; * 事件监听函数绑定器 */ export function EventSetter(props: EventSetterProps) { - const { value, onChange, modalTitle } = props; + const { value, onChange, modalTitle, modalTip, template } = props; const [type, setType] = useState(); // 事件类型 const [temp, setTemp] = useState(''); // 二级暂存值 const { actionVariables, routeOptions } = useWorkspaceData(); @@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) { label: ( { handleChange(nextValue); }} @@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) { { label: '打开弹窗', value: EventAction.OpenModal }, { label: '关闭弹窗', value: EventAction.CloseModal }, ], - [modalTitle, value, actionVariables, handleChange], + [modalTitle, value, actionVariables, template, handleChange], ); const onAction = (key: string) => { setType(key as EventAction); // 记录事件类型 setTemp(''); // 重置二级选项值 - switch (key) { - case EventAction.ConsoleLog: - handleChange('(...args) => console.log(...args)'); - break; - case EventAction.NoAction: - handleChange(undefined); - break; - default: - break; + if (key === EventAction.NoAction) { + handleChange(undefined); + return; } }; - const actionText = getActionText(type, temp, code); - return ( - + + {type === EventAction.ConsoleLog && ( + setTemp(e.target.value)} + onBlur={() => { + if (temp) { + handleChange(getExpressionValue(type, temp)); + } + }} + /> + )} {type === EventAction.NavigateTo && ( tango.${handler}("${value}")`; + return getCallbackValue(`${handler}("${value}");`); } } diff --git a/packages/designer/src/setters/expression-setter.tsx b/packages/designer/src/setters/expression-setter.tsx index 5c6aeadd..26b06061 100644 --- a/packages/designer/src/setters/expression-setter.tsx +++ b/packages/designer/src/setters/expression-setter.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Box, Text, css } from 'coral-system'; import { Dropdown, Button } from 'antd'; import { isValidExpressionCode } from '@music163/tango-core'; -import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers'; +import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers'; import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons'; import { Panel, @@ -34,6 +34,19 @@ export const jsonValueValidate = (value: string) => { } }; +/** + * 拼装回调函数 + * @param value 回调函数体 + * @param template 回调函数模板 + * @returns + */ +export function getCallbackValue(value: string, template?: string) { + if (!value) { + return; + } + return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`; +} + const suffixStyle = css` display: flex; align-items: center; @@ -63,6 +76,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) { modalTitle, modalTip, autoCompleteOptions, + template, placeholder = '在这里输入代码', value: valueProp, status, @@ -105,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) { { - change(''); + change(undefined); }} /> )} @@ -134,6 +148,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) { subTitle={modalTip} placeholder={placeholder} autoCompleteOptions={autoCompleteOptions} + template={template} newStoreTemplate={newStoreTemplate} value={inputValue} expressionType={expressionType} @@ -166,6 +181,10 @@ export interface ExpressionPopoverProps extends InputCodeProps { onOk?: (value: string) => void; dataSource?: IVariableTreeNode[]; autoCompleteOptions?: string[]; + /** + * 值的模板,一般用于定义函数模板 + */ + template?: string; /** * 新建 store 的模板代码 */ @@ -186,6 +205,7 @@ export function ExpressionPopover({ value, dataSource, autoCompleteOptions, + template, newStoreTemplate = CODE_TEMPLATES.newStoreTemplate, children, expressionType, @@ -245,19 +265,18 @@ export function ExpressionPopover({ autoCompleteOptions={autoCompleteOptions} /> {error ? ( - + 出错了!输入的表达式存在语法错误,请修改后再提交! ) : null} - {subTitle && ( - - {subTitle} + + 说明: + {subTitle && {subTitle}} + + 你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx + 代码,但需要符合该属性的接受值定义。 - )} - - 说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx - 代码,但需要符合该属性的接受值定义。 - + ) : ( - {data.name} + {data.name} ); diff --git a/packages/helpers/src/helpers/string.ts b/packages/helpers/src/helpers/string.ts index ece586de..73d5885a 100644 --- a/packages/helpers/src/helpers/string.ts +++ b/packages/helpers/src/helpers/string.ts @@ -125,3 +125,17 @@ export function parseDndId(str: string): DndIdParsedType { id: str, }; } + +/** + * 替换模板中的变量 + * @example interpolate('hello {{name}}', { name: 'world' }) -> 'hello world' + * + * @param template 带模板变量的字符串 + * @param props 变量字典 + * @returns 返回替换后的字符串 + */ +export function interpolate(template: string, props: Record) { + return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => { + return props[key]; + }); +} diff --git a/packages/helpers/src/types/prototype.ts b/packages/helpers/src/types/prototype.ts index e6b1faca..716a39b2 100644 --- a/packages/helpers/src/types/prototype.ts +++ b/packages/helpers/src/types/prototype.ts @@ -88,6 +88,11 @@ export interface IComponentProp { * 自动补全的提示值,仅对 ExpressionSetter 有效 */ autoCompleteOptions?: string[]; + /** + * value 的模板,一般用于函数类型的属性,便于 setter 用来拼装返回值 + * @example "(arg1, arg2, arg3) => { {{content}}}" + */ + template?: string; /** * 设置器 */ diff --git a/packages/setting-form/src/form-item.tsx b/packages/setting-form/src/form-item.tsx index dccf57df..16ab066f 100644 --- a/packages/setting-form/src/form-item.tsx +++ b/packages/setting-form/src/form-item.tsx @@ -182,6 +182,7 @@ export function createFormItem(options: IFormItemCreateOptions) { placeholder, docs, autoCompleteOptions, + template, setter: setterProp, setterProps, defaultValue, @@ -238,6 +239,7 @@ export function createFormItem(options: IFormItemCreateOptions) { modalTitle: title, modalTip: tip, autoCompleteOptions, + template, }; } diff --git a/packages/ui/src/action-select.tsx b/packages/ui/src/action-select.tsx index bf0564d1..fa911d21 100644 --- a/packages/ui/src/action-select.tsx +++ b/packages/ui/src/action-select.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box, css, Text } from 'coral-system'; import { Button, Dropdown, Input, Menu } from 'antd'; import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons'; @@ -23,9 +23,13 @@ interface ActionSelectProps { */ onInputChange?: (value: string) => void; /** - * 提示文本 + * 受控的提示文本 */ text?: string; + /** + * 默认的提示文本 + */ + defaultText?: string; /** * 选择菜单时的回调 */ @@ -50,11 +54,6 @@ const actionInputStyle = css` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: var(--tango-colors-text3); - - &:hover { - color: var(--tango-colors-text2); - } } .anticon-down { @@ -71,15 +70,25 @@ const inputStyle = { width: 'calc(100% - 82px)' }; export function ActionSelect({ showInput = false, defaultInputValue, - text, + text: textProp, + defaultText, options = [], onSelect, onInputChange, }: ActionSelectProps) { + const [text, setText] = useState(defaultText); const menu = ( - onSelect(key)}> + {options.map((item) => ( - + { + onSelect?.(item.value); + if (!textProp) { + setText(item.label as string); + } + }} + > {item.label} @@ -122,8 +131,8 @@ export function ActionSelect({ mb="m" css={actionInputStyle} > - - {text} + + {textProp ?? text}