diff --git a/apps/playground/src/helpers/prototypes.ts b/apps/playground/src/helpers/prototypes.ts
index 3a85e6dc..d8bf7c68 100644
--- a/apps/playground/src/helpers/prototypes.ts
+++ b/apps/playground/src/helpers/prototypes.ts
@@ -35,15 +35,11 @@ const Snippet2ColumnLayout: IComponentPrototype = {
package: '@music163/antd',
initChildren: `
-
-
-
-
-
-
+
+
`,
- relatedImports: ['Columns', 'Column', 'Box'],
+ relatedImports: ['Columns', 'Column'],
};
const Snippet3ColumnLayout: IComponentPrototype = {
@@ -56,17 +52,14 @@ const Snippet3ColumnLayout: IComponentPrototype = {
initChildren: `
-
-
-
`,
- relatedImports: ['Columns', 'Column', 'Box'],
+ relatedImports: ['Columns', 'Column'],
};
const SnippetButtonGroup: IComponentPrototype = {
diff --git a/packages/core/src/models/designer.ts b/packages/core/src/models/designer.ts
index e472891b..03f76453 100644
--- a/packages/core/src/models/designer.ts
+++ b/packages/core/src/models/designer.ts
@@ -20,6 +20,10 @@ interface IDesignerOptions {
workspace: IWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
activeSidebarPanel?: string;
+ /**
+ * 默认激活的视图模式
+ */
+ activeView?: DesignerViewType;
}
const ISimulatorTypes: Record = {
@@ -64,6 +68,16 @@ export class Designer {
*/
_showSmartWizard = false;
+ /**
+ * 是否显示添加组件面板
+ */
+ _showAddComponentPopover = false;
+
+ /**
+ * 添加组件面板的位置
+ */
+ _addComponentPopoverPosition = { clientX: 0, clientY: 0 };
+
/**
* 是否显示右侧面板
*/
@@ -74,11 +88,6 @@ export class Designer {
*/
_isPreview = false;
- /**
- * 默认展开的侧边栏
- */
- defaultActiveSidebarPanel?: string;
-
private readonly workspace: IWorkspace;
get simulator(): ISimulatorType {
@@ -109,10 +118,22 @@ export class Designer {
return this._showRightPanel;
}
+ get showAddComponentPopover() {
+ return this._showAddComponentPopover;
+ }
+
+ get addComponentPopoverPosition() {
+ return this._addComponentPopoverPosition;
+ }
+
constructor(options: IDesignerOptions) {
this.workspace = options.workspace;
- const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
+ const {
+ simulator,
+ activeSidebarPanel: defaultActiveSidebarPanel,
+ activeView: defaultActiveView,
+ } = options;
// 默认设计器模式
if (simulator) {
@@ -124,6 +145,11 @@ export class Designer {
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
}
+ // 默认激活的视图
+ if (defaultActiveView) {
+ this.setActiveView(defaultActiveView);
+ }
+
makeObservable(this, {
_simulator: observable,
_viewport: observable,
@@ -131,6 +157,8 @@ export class Designer {
_activeSidebarPanel: observable,
_showSmartWizard: observable,
_showRightPanel: observable,
+ _showAddComponentPopover: observable,
+ _addComponentPopoverPosition: observable,
_isPreview: observable,
simulator: computed,
viewport: computed,
@@ -139,6 +167,8 @@ export class Designer {
isPreview: computed,
showRightPanel: computed,
showSmartWizard: computed,
+ showAddComponentPopover: computed,
+ addComponentPopoverPosition: computed,
setSimulator: action,
setViewport: action,
setActiveView: action,
@@ -147,6 +177,7 @@ export class Designer {
toggleRightPanel: action,
toggleSmartWizard: action,
toggleIsPreview: action,
+ toggleAddComponentPopover: action,
});
}
@@ -185,6 +216,22 @@ export class Designer {
this._showRightPanel = value ?? !this._showRightPanel;
}
+ /**
+ * 显示添加组件面板
+ * @param value 是否显示
+ * @param position 坐标
+ */
+ toggleAddComponentPopover(
+ value: boolean,
+ position: {
+ clientX: number;
+ clientY: number;
+ } = this.addComponentPopoverPosition,
+ ) {
+ this._showAddComponentPopover = value;
+ this._addComponentPopoverPosition = position;
+ }
+
toggleIsPreview(value: boolean) {
this._isPreview = value ?? !this._isPreview;
if (value) {
diff --git a/packages/designer/src/components/components-popover.tsx b/packages/designer/src/components/components-popover.tsx
new file mode 100644
index 00000000..edf5a9f1
--- /dev/null
+++ b/packages/designer/src/components/components-popover.tsx
@@ -0,0 +1,143 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import { Box } from 'coral-system';
+import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
+import { IconFont, DragPanel } from '@music163/tango-ui';
+import { ComponentsPanel, ComponentsPanelProps } from '../sidebar';
+import { IComponentPrototype } from '@music163/tango-helpers';
+
+interface ComponentsPopoverProps {
+ // 添加组件位置
+ type?: 'inner' | 'before' | 'after';
+ // 弹出方式 手动触发/DOM 触发
+ isControlled?: boolean;
+ title?: string;
+ prototype?: IComponentPrototype;
+ children?: React.ReactNode;
+}
+
+export const ComponentsPopover = observer(
+ ({
+ type = 'inner',
+ title = '添加组件',
+ isControlled = false,
+ prototype,
+ children,
+ ...popoverProps
+ }: ComponentsPopoverProps) => {
+ const [layout, setLayout] = useState('grid');
+ const workspace = useWorkspace();
+ const designer = useDesigner();
+
+ const { addComponentPopoverPosition, showAddComponentPopover } = designer;
+ const selectedNodeName = workspace.selectSource.selected?.[0]?.codeId ?? '未选中';
+
+ // 推荐使用的子组件
+ const insertedList = useMemo(
+ () =>
+ Array.isArray(prototype?.childrenName)
+ ? prototype.childrenName
+ : [prototype.childrenName].filter(Boolean),
+ [prototype.childrenName],
+ );
+
+ // 推荐使用的代码片段
+ const siblingList = useMemo(() => prototype.siblingNames ?? [], [prototype.siblingNames]);
+
+ const tipsTextMap = useMemo(
+ () => ({
+ before: `点击,在 ${selectedNodeName} 的前方添加节点`,
+ after: `点击,在 ${selectedNodeName} 的后方添加节点`,
+ inner: `点击,在 ${selectedNodeName} 内部添加节点`,
+ }),
+ [selectedNodeName],
+ );
+
+ const handleSelect = useCallback(
+ (name: string) => {
+ switch (type) {
+ case 'before':
+ workspace.insertBeforeSelectedNode(name);
+ break;
+ case 'after':
+ workspace.insertAfterSelectedNode(name);
+ break;
+ case 'inner':
+ workspace.insertToSelectedNode(name);
+ break;
+ default:
+ break;
+ }
+ },
+ [type, workspace],
+ );
+
+ const changeLayout = useCallback(() => {
+ setLayout(layout === 'grid' ? 'line' : 'grid');
+ }, [layout]);
+
+ const menuData = useMemo(() => {
+ const menuList = JSON.parse(JSON.stringify(workspace.menuData));
+ const commonList = menuList['common'];
+ if (siblingList?.length) {
+ commonList.unshift({
+ title: '代码片段',
+ items: siblingList,
+ });
+ }
+
+ if (insertedList?.length) {
+ commonList.unshift({
+ title: '推荐使用',
+ items: insertedList,
+ });
+ }
+
+ return menuList;
+ }, [insertedList, siblingList, workspace.menuData]);
+
+ const innerTypeProps =
+ // 手动触发 适用于 点击添加组件
+ type === 'inner' && isControlled
+ ? {
+ open: showAddComponentPopover,
+ onOpenChange: (open: boolean) => designer.toggleAddComponentPopover(open),
+ left: addComponentPopoverPosition.clientX,
+ top: addComponentPopoverPosition.clientY,
+ }
+ : {};
+
+ return (
+
+ {layout === 'grid' ? (
+
+ ) : (
+
+ )}
+
+ }
+ footer={tipsTextMap[type]}
+ width="330px"
+ body={
+
+ }
+ {...popoverProps}
+ >
+ {children}
+
+ );
+ },
+);
diff --git a/packages/designer/src/components/index.ts b/packages/designer/src/components/index.ts
index 80ec298b..c598c258 100644
--- a/packages/designer/src/components/index.ts
+++ b/packages/designer/src/components/index.ts
@@ -2,3 +2,4 @@ export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';
+export * from './components-popover';
diff --git a/packages/designer/src/dnd/use-dnd.ts b/packages/designer/src/dnd/use-dnd.ts
index 91135d5b..0a7ee674 100644
--- a/packages/designer/src/dnd/use-dnd.ts
+++ b/packages/designer/src/dnd/use-dnd.ts
@@ -412,6 +412,13 @@ export function useDnd({
// 打开智能向导弹窗
designer.toggleSmartWizard(true);
break;
+ case 'addComponent':
+ // 打开添加组件面板
+ designer.toggleAddComponentPopover(true, {
+ clientX: (e.detail.meta as any).clientX + 40,
+ clientY: (e.detail.meta as any).clientY + 110,
+ });
+ break;
default:
break;
}
diff --git a/packages/designer/src/simulator/selection.tsx b/packages/designer/src/simulator/selection.tsx
index ed68bcac..d2dbfb4f 100644
--- a/packages/designer/src/simulator/selection.tsx
+++ b/packages/designer/src/simulator/selection.tsx
@@ -1,14 +1,13 @@
-import React, { useMemo, useState } from 'react';
+import React from 'react';
import styled, { css, keyframes } from 'styled-components';
import { Box, Button, Group, HTMLCoralProps } from 'coral-system';
-import { DropdownProps, Tooltip } from 'antd';
+import { Tooltip } from 'antd';
import { HolderOutlined, InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
-import { DragPanel, IconFont } from '@music163/tango-ui';
import { getDragGhostElement } from '../helpers';
import { getWidget } from '../widgets';
-import { ComponentsPanel, ComponentsPanelProps } from '../sidebar';
+import { ComponentsPopover } from '../components';
/**
* 选择辅助工具的对齐方式
@@ -111,14 +110,6 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
const prototype = workspace.componentPrototypes.get(data.name);
const isPage = prototype?.type === 'page';
- // 推荐使用的子组件
- const insertedList = Array.isArray(prototype?.childrenName)
- ? prototype.childrenName
- : [prototype.childrenName].filter(Boolean);
-
- // 推荐使用的代码片段
- const siblingList = prototype.siblingNames ?? [];
-
let selectionHelpersAlign: SelectionHelperAlignType = 'top-right';
if (data.bounding) {
if (data.bounding.left + data.bounding.width + boundingOffset < designer.viewport.width) {
@@ -157,30 +148,16 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
style={style}
>
<>
- {
- workspace.insertBeforeSelectedNode(name);
- }}
- >
+
} css={topAddSiblingBtnStyle} />
-
- {
- workspace.insertAfterSelectedNode(name);
- }}
- >
+
+
} css={bottomAddSiblingBtnStyle} />
-
+
>
{showActions && (
@@ -220,19 +197,11 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
/>
{!isPage && actions}
{prototype.hasChildren !== false && (
- {
- workspace.insertToSelectedNode(name);
- }}
- >
+
} />
-
+
)}
)}
@@ -405,83 +374,3 @@ const NameSelector = ({ label, parents = [], onSelect = noop }: NameSelectorProp
);
};
-
-interface InsertedDropdownProps extends DropdownProps {
- title?: string;
- footer?: string;
- insertedList?: string[];
- siblingList?: string[];
- onSelect?: (name: string) => void;
-}
-
-function InsertedDropdown({
- title = '添加组件',
- insertedList = [],
- siblingList = [],
- onSelect,
- footer,
- children,
- ...props
-}: InsertedDropdownProps) {
- const workspace = useWorkspace();
-
- const [layout, setLayout] = useState('grid');
-
- const changeLayout = () => {
- setLayout(layout === 'grid' ? 'line' : 'grid');
- };
-
- const menuData = useMemo(() => {
- const menuList = JSON.parse(JSON.stringify(workspace.menuData));
- const commonList = menuList['common'];
- if (siblingList?.length) {
- commonList.unshift({
- title: '代码片段',
- items: siblingList,
- });
- }
-
- if (insertedList?.length) {
- commonList.unshift({
- title: '推荐使用',
- items: insertedList,
- });
- }
-
- return menuList;
- }, [insertedList, siblingList, workspace.menuData]);
-
- return (
-
- {layout === 'grid' ? (
-
- ) : (
-
- )}
-
- }
- footer={footer}
- width="330px"
- placement="bottomCenter"
- body={
-
- }
- {...props}
- >
- {children}
-
- );
-}
diff --git a/packages/designer/src/workspace-view.tsx b/packages/designer/src/workspace-view.tsx
index dc6f5bf0..0218d0f8 100644
--- a/packages/designer/src/workspace-view.tsx
+++ b/packages/designer/src/workspace-view.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { Box } from 'coral-system';
import { observer, useDesigner } from '@music163/tango-context';
import { DesignerViewType } from '@music163/tango-core';
+import { ComponentsPopover } from './components';
export interface WorkspaceViewProps {
/**
@@ -27,6 +28,8 @@ export const WorkspaceView = observer((props: WorkspaceViewProps) => {
position="relative"
>
{children}
+ {/* 添加组件弹层 */}
+ {display === 'block' && }
);
});
diff --git a/packages/ui/package.json b/packages/ui/package.json
index b0367d20..64f18906 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -45,7 +45,7 @@
"coral-system": "^1.0.5",
"eslint-linter-browserify": "^8.51.0",
"react-json-view": "^1.21.3",
- "react-monaco-editor-lite": "^1.3.9",
+ "react-monaco-editor-lite": "^1.3.11",
"react-draggable": "^4.4.5"
},
"publishConfig": {
diff --git a/packages/ui/src/drag-panel.tsx b/packages/ui/src/drag-panel.tsx
index b36d4a1f..f8e6d403 100644
--- a/packages/ui/src/drag-panel.tsx
+++ b/packages/ui/src/drag-panel.tsx
@@ -1,8 +1,7 @@
import React, { useState } from 'react';
import { Box, Text, styled } from 'coral-system';
-import { Dropdown, DropdownProps } from 'antd';
+import { Popover, PopoverProps, IconFont } from '@music163/tango-ui';
import Draggable from 'react-draggable';
-import { IconFont } from '@music163/tango-ui';
import { CloseOutlined } from '@ant-design/icons';
const CloseIcon = styled(CloseOutlined)`
@@ -17,7 +16,7 @@ const CloseIcon = styled(CloseOutlined)`
}
`;
-interface DragPanelProps extends DropdownProps {
+interface DragPanelProps extends Omit {
// 标题
title?: React.ReactNode | string;
// 内容
@@ -28,6 +27,7 @@ interface DragPanelProps extends DropdownProps {
width?: number | string;
// 右上角区域
extra?: React.ReactNode | string;
+ children?: React.ReactNode;
}
export function DragPanel({
@@ -44,71 +44,68 @@ export function DragPanel({
const footerNode = typeof footer === 'function' ? footer(() => setOpen(false)) : footer;
return (
- {
- return (
-
+ onOpenChange={setOpen}
+ overlay={
+
+
+ {/* 头部区域 */}
- {/* 头部区域 */}
+
+
+ {title}
+
+
+ {extra}
+ {
+ setOpen(false);
+ props?.onOpenChange?.(false);
+ }}
+ />
+
+
+ {/* 主体区域 */}
+ {body}
+ {/* 底部 */}
+ {footer && (
-
-
- {title}
-
-
- {extra}
- setOpen(false)} />
-
+ {footerNode}
- {/* 主体区域 */}
- {body}
- {/* 底部 */}
- {footer && (
-
- {footerNode}
-
- )}
-
-
- );
- }}
+ )}
+
+
+ }
{...props}
>
- {React.cloneElement(children as any, {
- onClick: (e: Event) => {
- e.stopPropagation();
- setOpen(!open);
- (children as any).props?.onClick?.(e);
- },
- })}
-
+ {children}
+
);
}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index fce9ced7..eaa4efbe 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -22,4 +22,5 @@ export * from './tabs';
export * from './select-action';
export * from './copy-clipboard';
export * from './tag-select';
+export * from './popover';
export * from './drag-panel';
diff --git a/packages/ui/src/popover.tsx b/packages/ui/src/popover.tsx
new file mode 100644
index 00000000..e76771d8
--- /dev/null
+++ b/packages/ui/src/popover.tsx
@@ -0,0 +1,114 @@
+import React, { useState, useRef, useEffect, useMemo } from 'react';
+import ReactDOM from 'react-dom';
+
+export interface PopoverProps {
+ open?: boolean;
+ // 浮层内容
+ overlay: React.ReactNode;
+ onOpenChange?: (open: boolean) => void;
+ children?: React.ReactNode;
+ /**
+ * 浮层被遮挡时自动调整位置
+ */
+ autoAdjustOverflow?: boolean;
+ left?: number;
+ top?: number;
+ zIndex?: number;
+}
+
+export const Popover: React.FC = ({
+ open,
+ overlay,
+ onOpenChange,
+ autoAdjustOverflow = true,
+ left: controlledLeft,
+ top: controlledTop,
+ children,
+ zIndex = 9999,
+}) => {
+ const [visible, setVisible] = useState(false);
+ const [left, setLeft] = useState(0);
+ const [top, setTop] = useState(0);
+ const popoverRef = useRef(null);
+
+ // 唤起位置受控
+ const isControlledPostion = useMemo(
+ () => controlledLeft !== undefined || controlledTop !== undefined,
+ [controlledLeft, controlledTop],
+ );
+
+ useEffect(() => {
+ if (typeof controlledLeft === 'number') {
+ setLeft(controlledLeft);
+ }
+ }, [controlledLeft]);
+
+ useEffect(() => {
+ if (typeof controlledTop === 'number') {
+ setTop(controlledTop);
+ }
+ }, [controlledTop]);
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ const x = e.clientX;
+ const y = e.clientY;
+ setLeft(x);
+ setTop(y + 10);
+ setVisible(true);
+ onOpenChange(true);
+ };
+
+ useEffect(() => {
+ setVisible(open);
+ }, [open]);
+
+ const getAdjustedPosition = () => {
+ const popoverElement = popoverRef.current;
+ if (popoverElement) {
+ const popoverRect = popoverElement.getBoundingClientRect();
+ if (popoverRect.right > window.innerWidth) {
+ setLeft(window.innerWidth - popoverRect.width);
+ }
+ if (popoverRect.bottom > window.innerHeight) {
+ setTop(window.innerHeight - popoverRect.height);
+ }
+ }
+ };
+
+ useEffect(() => {
+ if (visible && autoAdjustOverflow) {
+ getAdjustedPosition();
+ }
+ }, [visible, autoAdjustOverflow]);
+
+ const popoverStyle: React.CSSProperties = {
+ display: visible ? 'block' : 'none',
+ position: 'fixed',
+ left,
+ top,
+ zIndex,
+ };
+
+ const overlayDom = (
+
+ );
+
+ return (
+ <>
+ {!isControlledPostion &&
+ React.cloneElement(children as any, {
+ onClick: (e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleClick(e);
+ (children as any).props?.onClick?.(e);
+ },
+ })}
+ {visible ? ReactDOM.createPortal(overlayDom, document.body) : null}
+ >
+ );
+};
diff --git a/yarn.lock b/yarn.lock
index 0da725e0..0feeb4c1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15878,10 +15878,10 @@ react-merge-refs@^1.1.0:
resolved "https://registry.npmmirror.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz"
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
-react-monaco-editor-lite@^1.3.9:
- version "1.3.9"
- resolved "https://registry.npmmirror.com/react-monaco-editor-lite/-/react-monaco-editor-lite-1.3.9.tgz#377a2126f2a26de7e478323c70b13a50f0c5c760"
- integrity sha512-pE6CydX/kkisHArbV8GE+TvhukIiT9YYUYBq/cnDL7tfwhX/h1tKQKMJwItnkJvjozaB5uS+oK77GWV874figQ==
+react-monaco-editor-lite@^1.3.11:
+ version "1.3.11"
+ resolved "https://registry.npmmirror.com/react-monaco-editor-lite/-/react-monaco-editor-lite-1.3.11.tgz#b81b681d23f3ca46f54d4e01f13781bcff6a4a03"
+ integrity sha512-+9xlIn98Yp2hD8OFjRNpQiBx+wLFzsvvT5EgNTxReFrDhFAPPjivdLUCiex1ktzpkGTdo3fxDFUokckvO7hVvQ==
dependencies:
monaco-editor "^0.38.0"
monaco-editor-textmate "^4.0.0"