diff --git a/docs/docs/api/commonUI.md b/docs/docs/api/commonUI.md index f3afe1fc2..975e3caaa 100644 --- a/docs/docs/api/commonUI.md +++ b/docs/docs/api/commonUI.md @@ -48,8 +48,70 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开 | condition | 显示条件函数
Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | | | disabled | 禁用条件函数,可选
Function to determine disabled condition, optional | (nodes: IPublicModelNode[]) => boolean (optional) | | +**ContextMenu 示例** + +```typescript +const App = () => { + const menuItems: IPublicTypeContextMenuAction[] = [ + { + name: 'a', + title: '选项 1', + actions: () => console.log('选项 1 被点击'), + }, + { + name: 'b', + title: '选项 2', + actions: () => console.log('选项 2 被点击'), + }, + ]; + + const ContextMenu = ctx.commonUI.ContextMenu; + + return ( +
+ +
右键点击这里
+
+
+ ); +}; + +export default App; +``` + +**ContextMenu.create 示例** + +```typescript +const App = () => { + const menuItems: IPublicTypeContextMenuAction[] = [ + { + name: 'a', + title: '选项 1', + actions: () => console.log('选项 1 被点击'), + }, + { + name: 'b', + title: '选项 2', + actions: () => console.log('选项 2 被点击'), + }, + ]; + + const ContextMenu = ctx.commonUI.ContextMenu; + + return ( +
+
{ + ContextMenu.create(menuItems, e); + }}>点击这里
+
+ ); +}; + +export default App; +``` ### Balloon + 详细文档: [Balloon Documentation](https://fusion.design/pc/component/balloon) ### Breadcrumb diff --git a/docs/docs/api/material.md b/docs/docs/api/material.md index c83935b97..faf4f1edc 100644 --- a/docs/docs/api/material.md +++ b/docs/docs/api/material.md @@ -250,6 +250,33 @@ material.modifyBuiltinComponentAction('remove', (action) => { addContextMenuOption(action: IPublicTypeContextMenuAction): void; ``` +示例 + +```typescript +import { IPublicEnumContextMenuType } from '@alilc/lowcode-types'; + +material.addContextMenuOption({ + name: 'parentItem', + title: 'Parent Item', + condition: (node) => true, + items: [ + { + name: 'childItem1', + title: 'Child Item 1', + action: (node) => console.log('Child Item 1 clicked', node), + condition: (node) => true + }, + // 分割线 + { + type: IPublicEnumContextMenuType.SEPARATOR + name: 'separator.1' + } + // 更多子菜单项... + ] +}); + +``` + #### removeContextMenuOption 删除特定右键菜单项 @@ -274,7 +301,26 @@ removeContextMenuOption(name: string): void; adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void; ``` +**示例** + +通过 adjustContextMenuLayout 补充分割线 + +```typescript +material.adjustContextMenuLayout((actions: IPublicTypeContextMenuAction) => { + const names = ['a', 'b']; + const newActions = []; + actions.forEach(d => { + newActions.push(d); + if (names.include(d.name)) { + newActions.push({ type: 'separator' }) + } + }); + return newActions +}) +``` + ### 物料元数据 + #### getComponentMeta 获取指定名称的物料元数据 diff --git a/packages/designer/src/context-menu-actions.ts b/packages/designer/src/context-menu-actions.ts index 5b44055b4..c3bad37da 100644 --- a/packages/designer/src/context-menu-actions.ts +++ b/packages/designer/src/context-menu-actions.ts @@ -1,4 +1,4 @@ -import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types'; +import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial, IPublicModelPluginContext } from '@alilc/lowcode-types'; import { IDesigner, INode } from './designer'; import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils'; import { Menu } from '@alifd/next'; @@ -48,6 +48,7 @@ export class GlobalContextMenuActions { event.preventDefault(); const actions: IPublicTypeContextMenuAction[] = []; + let contextMenu: ContextMenuActions = this.contextMenuActionsMap.values().next().value; this.contextMenuActionsMap.forEach((contextMenu) => { actions.push(...contextMenu.actions); }); @@ -57,11 +58,13 @@ export class GlobalContextMenuActions { const destroy = () => { destroyFn?.(); }; + const pluginContext: IPublicModelPluginContext = contextMenu.designer.editor.get('pluginContext') as IPublicModelPluginContext; const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, { nodes: [], destroy, event, + pluginContext, }); if (!menus.length) { @@ -73,6 +76,7 @@ export class GlobalContextMenuActions { const menuNode = parseContextMenuAsReactNode(layoutMenu, { destroy, nodes: [], + pluginContext, }); const target = event.target; @@ -160,10 +164,13 @@ export class ContextMenuActions implements IContextMenuActions { destroyFn?.(); }; + const pluginContext: IPublicModelPluginContext = this.designer.editor.get('pluginContext') as IPublicModelPluginContext; + const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, { nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!), destroy, event, + pluginContext, }); if (!menus.length) { @@ -175,7 +182,7 @@ export class ContextMenuActions implements IContextMenuActions { const menuNode = parseContextMenuAsReactNode(layoutMenu, { destroy, nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!), - designer, + pluginContext, }); destroyFn = createContextMenu(menuNode, { diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index 29b4a7f03..3ccfdf51d 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -115,12 +115,11 @@ const innerSetters = new InnerSetters(); const setters = new Setters(innerSetters); const material = new Material(editor); -const commonUI = new CommonUI(); +const commonUI = new CommonUI(editor); editor.set('project', project); editor.set('setters' as any, setters); editor.set('material', material); editor.set('innerHotkey', innerHotkey); -editor.set('commonUI' as any, commonUI); const config = new Config(engineConfig); const event = new Event(commonEvent, { prefix: 'common' }); const logger = new Logger({ level: 'warn', bizName: 'common' }); @@ -147,6 +146,7 @@ const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { context.commonUI = commonUI; context.registerLevel = IPublicEnumPluginRegisterLevel.Default; context.isPluginRegisteredInWorkspace = false; + editor.set('pluginContext', context); }, }; diff --git a/packages/engine/src/inner-plugins/default-context-menu.ts b/packages/engine/src/inner-plugins/default-context-menu.ts index 9d1336b34..db3d54a0c 100644 --- a/packages/engine/src/inner-plugins/default-context-menu.ts +++ b/packages/engine/src/inner-plugins/default-context-menu.ts @@ -5,12 +5,11 @@ import { IPublicModelNode, IPublicModelPluginContext, IPublicTypeDragNodeDataObject, - IPublicTypeI18nData, IPublicTypeNodeSchema, } from '@alilc/lowcode-types'; -import { isI18nData, isProjectSchema } from '@alilc/lowcode-utils'; +import { isProjectSchema } from '@alilc/lowcode-utils'; import { Notification } from '@alifd/next'; -import { intl, getLocale } from '../locale'; +import { intl } from '../locale'; function getNodesSchema(nodes: IPublicModelNode[]) { const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone)); @@ -18,15 +17,6 @@ function getNodesSchema(nodes: IPublicModelNode[]) { return data; } -function getIntlStr(data: string | IPublicTypeI18nData) { - if (!isI18nData(data)) { - return data; - } - - const locale = getLocale(); - return data[locale] || data['zh-CN'] || data['zh_CN'] || data['en-US'] || data['en_US'] || ''; -} - async function getClipboardText(): Promise { return new Promise((resolve, reject) => { // 使用 Clipboard API 读取剪贴板内容 @@ -61,8 +51,9 @@ async function getClipboardText(): Promise { } export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { - const { material, canvas } = ctx; + const { material, canvas, common } = ctx; const { clipboard } = canvas; + const { intl: utilsIntl } = common.utils; return { init() { @@ -150,7 +141,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { }); if (canAddNodes.length === 0) { Notification.open({ - content: `${nodeSchema.map(d => getIntlStr(d.title || d.componentName)).join(',')}等组件无法放置到${getIntlStr(parent.title || parent.componentName as any)}内`, + content: `${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(parent.title || parent.componentName as any)}内`, type: 'error', }); return; @@ -198,7 +189,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { }); if (canAddNodes.length === 0) { Notification.open({ - content: `${nodeSchema.map(d => getIntlStr(d.title || d.componentName)).join(',')}等组件无法放置到${getIntlStr(node.title || node.componentName as any)}内`, + content: `${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(node.title || node.componentName as any)}内`, type: 'error', }); return; diff --git a/packages/shell/src/api/commonUI.ts b/packages/shell/src/api/commonUI.tsx similarity index 56% rename from packages/shell/src/api/commonUI.ts rename to packages/shell/src/api/commonUI.tsx index 9f40afa11..cb4b7ae38 100644 --- a/packages/shell/src/api/commonUI.ts +++ b/packages/shell/src/api/commonUI.tsx @@ -1,12 +1,16 @@ -import { IPublicApiCommonUI } from '@alilc/lowcode-types'; +import { IPublicApiCommonUI, IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; import { + IEditor, Tip as InnerTip, Title as InnerTitle, } from '@alilc/lowcode-editor-core'; import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next'; import { ContextMenu } from '../components/context-menu'; +import { editorSymbol } from '../symbols'; export class CommonUI implements IPublicApiCommonUI { + [editorSymbol]: IEditor; + Balloon = Balloon; Breadcrumb = Breadcrumb; Button = Button; @@ -35,13 +39,29 @@ export class CommonUI implements IPublicApiCommonUI { Upload = Upload; Divider = Divider; + constructor(editor: IEditor) { + this[editorSymbol] = editor; + } + get Tip() { return InnerTip; } get Title() { return InnerTitle; } + get ContextMenu() { - return ContextMenu; + const editor = this[editorSymbol]; + const innerContextMenu = (props: any) => { + const pluginContext: IPublicModelPluginContext = editor.get('pluginContext') as IPublicModelPluginContext; + return ; + }; + + innerContextMenu.create = (menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + const pluginContext: IPublicModelPluginContext = editor.get('pluginContext') as IPublicModelPluginContext; + return ContextMenu.create(pluginContext, menus, event); + }; + + return innerContextMenu; } } diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx index acefbebd2..d2e10abbf 100644 --- a/packages/shell/src/components/context-menu.tsx +++ b/packages/shell/src/components/context-menu.tsx @@ -1,11 +1,12 @@ import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils'; import { engineConfig } from '@alilc/lowcode-editor-core'; -import { IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; +import { IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; import React from 'react'; -export function ContextMenu({ children, menus }: { +export function ContextMenu({ children, menus, pluginContext }: { menus: IPublicTypeContextMenuAction[]; children: React.ReactElement[] | React.ReactElement; + pluginContext: IPublicModelPluginContext; }): React.ReactElement> { if (!engineConfig.get('enableContextMenu')) { return ( @@ -23,7 +24,10 @@ export function ContextMenu({ children, menus }: { }; const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { destroy, - })); + pluginContext, + }), { + pluginContext, + }); if (!children?.length) { return; @@ -44,4 +48,20 @@ export function ContextMenu({ children, menus }: { return ( <>{childrenWithContextMenu} ); -} \ No newline at end of file +} + +ContextMenu.create = (pluginContext: IPublicModelPluginContext, menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { + pluginContext, + }), { + pluginContext, + }); + + if (!children?.length) { + return; + } + + return createContextMenu(children, { + event, + }); +}; \ No newline at end of file diff --git a/packages/utils/src/context-menu.scss b/packages/utils/src/context-menu.scss index 0bcf39d15..366d03e45 100644 --- a/packages/utils/src/context-menu.scss +++ b/packages/utils/src/context-menu.scss @@ -1,32 +1,42 @@ -.context-menu-tree-wrap { +.engine-context-menu-tree-wrap { position: relative; padding: 4px 10px 4px 32px; } -.context-menu-tree-children { +.engine-context-menu-tree-children { margin-left: 8px; line-height: 24px; } -.context-menu-tree-bg { - position: absolute; - left: 0; - right: 0; - cursor: pointer; +.engine-context-menu-item { + .engine-context-menu-text { + color: var(--color-text); + } + + &:hover { + .engine-context-menu-text { + color: var(--color-title); + } + } + + &.disbale { + .engine-context-menu-text { + color: var(--color-text-disabled); + } + } } -.context-menu-tree-bg-inner { - position: absolute; - height: 24px; - top: -24px; - width: 100%; +.engine-context-menu-title { + color: var(--color-text); + cursor: pointer; &:hover { background-color: var(--color-block-background-light); + color: var(--color-title); } } -.context-menu-tree-selected-icon { +.engine-context-menu-tree-selecte-icon { position: absolute; left: 10px; color: var(--color-icon-active); diff --git a/packages/utils/src/context-menu.tsx b/packages/utils/src/context-menu.tsx index 1816f5b52..0f157e438 100644 --- a/packages/utils/src/context-menu.tsx +++ b/packages/utils/src/context-menu.tsx @@ -1,7 +1,7 @@ import { Menu, Icon } from '@alifd/next'; -import { IDesigner } from '@alilc/lowcode-designer'; -import { IPublicEnumContextMenuType, IPublicModelNode, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '@alilc/lowcode-types'; +import { IPublicEnumContextMenuType, IPublicModelNode, IPublicModelPluginContext, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '@alilc/lowcode-types'; import { Logger } from '@alilc/lowcode-utils'; +import classNames from 'classnames'; import React from 'react'; import './context-menu.scss'; @@ -10,43 +10,51 @@ const { Item, Divider, PopupItem } = Menu; const MAX_LEVEL = 2; +interface IOptions { + nodes?: IPublicModelNode[] | null; + destroy?: Function; + pluginContext: IPublicModelPluginContext; +} + const Tree = (props: { - node?: IPublicModelNode; + node?: IPublicModelNode | null; children?: React.ReactNode; - options: { - nodes?: IPublicModelNode[] | null; - destroy?: Function; - designer?: IDesigner; - }; + options: IOptions; }) => { const { node } = props; if (!node) { return ( -
{ props.children }
+
{ props.children }
); } - const commonUI = props.options.designer?.editor?.get('commonUI'); - - const Title = commonUI?.Title; + const { common } = props.options.pluginContext || {}; + const { intl } = common?.utils || {}; + const indent = node.zLevel * 8 + 32; + const style = { + paddingLeft: indent, + marginLeft: -indent, + marginRight: -10, + paddingRight: 10, + }; return ( - {props.options.nodes?.[0].id === node.id ? () : null} - <div - className="context-menu-tree-children" + className="engine-context-menu-title" + onClick={() => { + props.options.destroy?.(); + node.select(); + }} + style={style} + > + {props.options.nodes?.[0].id === node.id ? (<Icon className="engine-context-menu-tree-selecte-icon" size="small" type="success" />) : null} + {intl(node.title)} + </div> + <div + className="engine-context-menu-tree-children" > - <div - className="context-menu-tree-bg" - onClick={() => { - props.options.destroy?.(); - node.select(); - }} - > - <div className="context-menu-tree-bg-inner" /> - </div> { props.children } </div> </Tree> @@ -55,11 +63,10 @@ const Tree = (props: { let destroyFn: Function | undefined; -export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: { - nodes?: IPublicModelNode[] | null; - destroy?: Function; - designer?: IDesigner; -} = {}): React.ReactNode[] { +export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: IOptions): React.ReactNode[] { + const { common } = options.pluginContext || {}; + const { intl = (title: any) => title } = common?.utils || {}; + const children: React.ReactNode[] = []; menus.forEach((menu, index) => { if (menu.type === IPublicEnumContextMenuType.SEPARATOR) { @@ -70,14 +77,33 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], if (menu.type === IPublicEnumContextMenuType.MENU_ITEM) { if (menu.items && menu.items.length) { children.push(( - <PopupItem key={menu.name} label={menu.title}> + <PopupItem + className={classNames('engine-context-menu-item', { + disbale: menu.disabled, + })} + key={menu.name} + label={<div className="engine-context-menu-text">{intl(menu.title)}</div>} + > <Menu className="next-context engine-context-menu"> { parseContextMenuAsReactNode(menu.items, options) } </Menu> </PopupItem> )); } else { - children.push((<Item disabled={menu.disabled} onClick={menu.action} key={menu.name}>{menu.title}</Item>)); + children.push(( + <Item + className={classNames('engine-context-menu-item', { + disbale: menu.disabled, + })} + disabled={menu.disabled} + onClick={menu.action} + key={menu.name} + > + <div className="engine-context-menu-text"> + {intl(menu.title)} + </div> + </Item> + )); } } @@ -91,9 +117,7 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], return children; } -export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: { - nodes?: IPublicModelNode[] | null; - destroy?: Function; +export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: IOptions & { event?: MouseEvent; }, level = 1): IPublicTypeContextMenuItem[] { destroyFn?.(); @@ -156,7 +180,6 @@ function getMenuItemHeight() { } const root = document.documentElement; const styles = getComputedStyle(root); - // Access the value of the CSS variable const menuItemHeight = styles.getPropertyValue('--context-menu-item-height').trim(); cachedMenuItemHeight = menuItemHeight; diff --git a/packages/workspace/src/context/base-context.ts b/packages/workspace/src/context/base-context.ts index e090d1e37..2f6154788 100644 --- a/packages/workspace/src/context/base-context.ts +++ b/packages/workspace/src/context/base-context.ts @@ -128,13 +128,12 @@ export class BasicContext implements IBasicContext { const logger = getLogger({ level: 'warn', bizName: 'common' }); const skeleton = new Skeleton(innerSkeleton, 'any', true); const canvas = new Canvas(editor, true); - const commonUI = new CommonUI(); + const commonUI = new CommonUI(editor); editor.set('setters', setters); editor.set('project', project); editor.set('material', material); editor.set('hotkey', hotkey); editor.set('innerHotkey', innerHotkey); - editor.set('commonUI' as any, commonUI); this.innerSetters = innerSetters; this.innerSkeleton = innerSkeleton; this.skeleton = skeleton; @@ -175,6 +174,7 @@ export class BasicContext implements IBasicContext { } context.registerLevel = registerLevel; context.isPluginRegisteredInWorkspace = registerLevel === IPublicEnumPluginRegisterLevel.Workspace; + editor.set('pluginContext', context); }, };