Commit ae4f2379fb3a617dc8fbc0cb1767c8710a4c0c4e

Authored by ww
1 parent 3ca62dac

feat: 完成规则链设计器右键菜单功能

1 1 import { RuleChainPaginationItemType } from './model/type';
2 2 import { TBPaginationResult } from '/#/axios';
3 3 import { defHttp } from '/@/utils/http/axios';
4   -import { RuleChainType } from '/@/views/rule/designer/types/ruleNode';
  4 +import { RuleChainDetail, RuleChainType } from '/@/views/rule/designer/types/ruleNode';
5 5
6 6 enum Api {
  7 + GET_RULE_CHAINS_DETAIL = '/ruleChain',
7 8 SAVE = '/ruleChain/metadata',
8 9 GET_RULE_CHAINES = '/ruleChains',
9 10 GET_RULE_NODE_EVENTS = '/events/RULE_NODE',
10 11 }
11 12
  13 +export const getRuleChainDetail = (id: string) => {
  14 + return defHttp.get<RuleChainDetail>(
  15 + {
  16 + url: `${Api.GET_RULE_CHAINS_DETAIL}/${id}`,
  17 + },
  18 + { joinPrefix: false }
  19 + );
  20 +};
  21 +
12 22 export const getRuleChainData = (id: string) => {
13 23 return defHttp.get<RuleChainType>(
14 24 {
... ...
... ... @@ -38,6 +38,11 @@ export const PLATFORM = 'PLATFORM';
38 38 export const PLATFORM_INFO_CACHE_KEY = 'PLATFORM_INFO';
39 39
40 40 export const MENU_LIST = 'MENU_LIST';
  41 +
  42 +export const RULE_NODE_LOCAL_CACHE_KEY = 'RULE__NODE__KEY__';
  43 +
  44 +export const RULE_NODE_KEY = 'RULE_NODE';
  45 +
41 46 export enum CacheTypeEnum {
42 47 SESSION,
43 48 LOCAL,
... ...
  1 +export enum RuleChainEntityType {
  2 + RULE_NODE = 'RULE_NODE',
  3 + RULE_CHAIN = 'RULE_CHAIN',
  4 +}
... ...
1 1 import { Node } from '@vue-flow/core';
2 2 import { NodeTypeEnum } from '../enum';
3 3 import { buildUUID } from '/@/utils/uuid';
  4 +import { NodeData } from '../types/node';
4 5
5 6 export const useAddNodes = () => {
6 7 const getAddNodesParams = (
7 8 position: Node['position'],
8   - data: object,
  9 + data: NodeData,
9 10 options?: Partial<Node>
10 11 ): Node => {
11 12 return {
... ...
... ... @@ -122,6 +122,7 @@ export function useBasicDataTransform() {
122 122 description,
123 123 name,
124 124 },
  125 + created: !!id?.id,
125 126 },
126 127 {
127 128 id: id?.id || buildUUID(),
... ...
  1 +import { GraphEdge, GraphNode, VueFlowStore } from '@vue-flow/core';
  2 +import { getRuleNodeCache, setRuleNodeCache } from './useRuleCopyPaste';
  3 +import { RuleContextMenuEnum } from './useRuleChainContextMenu';
  4 +import { useAddNodes } from './useAddNodes';
  5 +import { buildUUID } from '/@/utils/uuid';
  6 +import { toRaw, unref } from 'vue';
  7 +import { useSaveAndRedo } from './useSaveAndRedo';
  8 +import { isUnDef } from '/@/utils/is';
  9 +import { useAddEdges } from './useAddEdges';
  10 +import { EdgeData } from '../types/node';
  11 +
  12 +interface HandleContextMenuActionParamsType {
  13 + menuType: RuleContextMenuEnum;
  14 + event?: Event;
  15 + flowActionType?: VueFlowStore;
  16 + node?: GraphNode;
  17 + edge?: GraphEdge;
  18 + useSaveAndRedoActionType?: ReturnType<typeof useSaveAndRedo>;
  19 +}
  20 +
  21 +export const NODE_WIDTH = 176;
  22 +export const NODE_HEIGHT = 48;
  23 +
  24 +function getElementsCenter(nodes: GraphNode[]) {
  25 + let leftTopX: number | undefined;
  26 + let leftTopY: number | undefined;
  27 + let rightBottomX: number | undefined;
  28 + let rightBottomY: number | undefined;
  29 +
  30 + for (const node of nodes) {
  31 + const { position } = node;
  32 + const { x, y } = position;
  33 + if (isUnDef(leftTopX)) {
  34 + leftTopX = x;
  35 + leftTopY = y;
  36 + rightBottomX = x + NODE_WIDTH;
  37 + rightBottomY = y + NODE_HEIGHT;
  38 +
  39 + continue;
  40 + }
  41 +
  42 + if (x < leftTopX!) {
  43 + leftTopX = x;
  44 + if (y < leftTopY!) {
  45 + leftTopY = y;
  46 + }
  47 + continue;
  48 + }
  49 +
  50 + if (x + NODE_WIDTH > rightBottomX!) {
  51 + rightBottomX = x + NODE_WIDTH;
  52 + if (y + NODE_HEIGHT > rightBottomY!) {
  53 + rightBottomY = y + NODE_HEIGHT;
  54 + }
  55 + }
  56 + }
  57 +
  58 + return {
  59 + originX: (rightBottomX! - leftTopX!) / 2 + leftTopX!,
  60 + originY: (rightBottomY! - leftTopY!) / 2 + leftTopY!,
  61 + };
  62 +}
  63 +
  64 +export function useContextMenuAction() {
  65 + const copy = (params: HandleContextMenuActionParamsType) => {
  66 + const { node } = params;
  67 + if (!node) return;
  68 + const { position, data } = node;
  69 + const { getAddNodesParams } = useAddNodes();
  70 + const { x, y } = position;
  71 +
  72 + const value = getAddNodesParams(position, data, { id: buildUUID() });
  73 +
  74 + setRuleNodeCache({
  75 + nodes: [value],
  76 + originX: x + NODE_WIDTH / 2 + x,
  77 + originY: y + NODE_HEIGHT / 2,
  78 + });
  79 + };
  80 +
  81 + const paste = (params: HandleContextMenuActionParamsType) => {
  82 + const { event, flowActionType, useSaveAndRedoActionType } = params;
  83 + const { triggerChange } = useSaveAndRedoActionType || {};
  84 + const { getAddNodesParams } = useAddNodes();
  85 + const { getAddedgesParams } = useAddEdges();
  86 + const clientX = (event as MouseEvent).offsetX;
  87 + const clientY = (event as MouseEvent).offsetY;
  88 +
  89 + const { edges = [], nodes = [], originX, originY } = getRuleNodeCache();
  90 +
  91 + const newNode = nodes.map((node) => {
  92 + const { position, data, id } = node;
  93 + const { x, y } = position;
  94 +
  95 + const newX = clientX - originX! + x + NODE_WIDTH / 2;
  96 + const newY = clientY - originY! + y + NODE_HEIGHT / 2;
  97 +
  98 + return getAddNodesParams({ x: newX, y: newY }, { ...data, created: false }, { id });
  99 + });
  100 +
  101 + const newEdges = edges.map((edge) => getAddedgesParams(edge, edge.data));
  102 +
  103 + flowActionType?.addNodes(newNode);
  104 + flowActionType?.addEdges(newEdges);
  105 +
  106 + triggerChange?.();
  107 +
  108 + flowActionType?.removeSelectedElements();
  109 + };
  110 +
  111 + const selectAll = (params: HandleContextMenuActionParamsType) => {
  112 + const { flowActionType } = params;
  113 + flowActionType?.addSelectedElements(unref(flowActionType.getElements));
  114 + };
  115 +
  116 + const unselect = (params: HandleContextMenuActionParamsType) => {
  117 + const { flowActionType } = params;
  118 + flowActionType?.removeSelectedElements();
  119 + };
  120 +
  121 + const deleteElement = (parmas: HandleContextMenuActionParamsType) => {
  122 + const { useSaveAndRedoActionType, flowActionType, node, edge } = parmas;
  123 + const { triggerChange } = useSaveAndRedoActionType || {};
  124 +
  125 + node && flowActionType?.removeNodes(node);
  126 + edge && flowActionType?.removeEdges(edge);
  127 +
  128 + triggerChange?.();
  129 + };
  130 +
  131 + const deleteElements = (params: HandleContextMenuActionParamsType) => {
  132 + const { flowActionType, useSaveAndRedoActionType } = params;
  133 + flowActionType?.removeNodes(unref(flowActionType.getSelectedNodes));
  134 +
  135 + useSaveAndRedoActionType?.triggerChange?.();
  136 + };
  137 +
  138 + const selectCopy = (params: HandleContextMenuActionParamsType) => {
  139 + const { flowActionType } = params;
  140 + const { getAddNodesParams } = useAddNodes();
  141 + const { getAddedgesParams } = useAddEdges();
  142 +
  143 + const edges = unref(flowActionType?.getSelectedEdges)?.map((edge) =>
  144 + getAddedgesParams(
  145 + {
  146 + source: edge.source,
  147 + target: edge.target,
  148 + sourceHandle: edge.sourceHandle,
  149 + targetHandle: edge.targetHandle,
  150 + },
  151 + toRaw(unref(edge.data as EdgeData)?.data)
  152 + )
  153 + );
  154 +
  155 + const nodes = unref(flowActionType?.getSelectedNodes)?.map((node) => {
  156 + const { id: oldId } = node;
  157 + const newId = buildUUID();
  158 +
  159 + for (const connection of edges || []) {
  160 + if (connection.source.includes(oldId)) {
  161 + connection.source = newId;
  162 + connection.sourceHandle = connection.sourceHandle?.replaceAll(oldId, newId);
  163 + continue;
  164 + }
  165 +
  166 + if (connection.target.includes(oldId)) {
  167 + connection.target = newId;
  168 + connection.targetHandle = connection.targetHandle?.replaceAll(oldId, newId);
  169 + }
  170 + }
  171 +
  172 + return getAddNodesParams(node.position, toRaw(unref(node.data)), { id: newId });
  173 + });
  174 +
  175 + const originRect = getElementsCenter(unref(flowActionType?.getSelectedNodes) || []);
  176 +
  177 + setRuleNodeCache({ nodes, edges, ...originRect });
  178 + };
  179 +
  180 + const applyChange = (params: HandleContextMenuActionParamsType) => {
  181 + const { useSaveAndRedoActionType, flowActionType } = params;
  182 +
  183 + useSaveAndRedoActionType?.handleApplyChange(flowActionType!);
  184 + };
  185 +
  186 + const undoChange = (params: HandleContextMenuActionParamsType) => {
  187 + const { useSaveAndRedoActionType, flowActionType } = params;
  188 +
  189 + useSaveAndRedoActionType?.handleRedoChange(flowActionType!);
  190 + };
  191 +
  192 + const handleContextMenuAction = (params: HandleContextMenuActionParamsType) => {
  193 + const { menuType } = params;
  194 +
  195 + const handlerMapping = {
  196 + [RuleContextMenuEnum.COPY]: copy,
  197 + [RuleContextMenuEnum.PASTE]: paste,
  198 + [RuleContextMenuEnum.SELECT_ALL]: selectAll,
  199 + [RuleContextMenuEnum.UNSELECTED]: unselect,
  200 + [RuleContextMenuEnum.DELETE]: deleteElement,
  201 + [RuleContextMenuEnum.DELETE_SELECT]: deleteElements,
  202 + [RuleContextMenuEnum.SELECT_COPY]: selectCopy,
  203 + [RuleContextMenuEnum.APPLY_CHANGE]: applyChange,
  204 + [RuleContextMenuEnum.UNDO_CHANGE]: undoChange,
  205 + };
  206 +
  207 + if (handlerMapping[menuType]) {
  208 + handlerMapping[menuType]?.(params);
  209 + }
  210 + };
  211 +
  212 + return {
  213 + handleContextMenuAction,
  214 + };
  215 +}
... ...
  1 +import { NodeMouseEvent } from '@vue-flow/core';
  2 +import { useContextMenu } from '../src/components/RuleChainContextMenu';
  3 +import { RuleChainContextMenuItemType } from '../src/components/RuleChainContextMenu/index.type';
  4 +import { ElementsTypeEnum } from '../enum';
  5 +import { checkHasCacheRuleNode } from './useRuleCopyPaste';
  6 +
  7 +export enum RuleContextMenuEnum {
  8 + DETAIL = 'DETAIL',
  9 + COPY = 'COPY',
  10 + DELETE = 'DELETE',
  11 +
  12 + SELECT_COPY = 'SELECT_COPY',
  13 + PASTE = 'PASTE',
  14 + UNSELECTED = 'UNSELECTED',
  15 + DELETE_SELECT = 'DELETE_SELECT',
  16 + APPLY_CHANGE = 'APPLY_CHANGE',
  17 + UNDO_CHANGE = 'UNDO_CHANGE',
  18 +
  19 + SELECT_ALL = 'SELECT_ALL',
  20 +}
  21 +
  22 +export enum RuleContextMenuNameEnum {
  23 + DETAIL = '详情',
  24 + COPY = '复制',
  25 + DELETE = '删除',
  26 +
  27 + SELECT_COPY = '选择副本',
  28 + PASTE = '粘贴',
  29 + UNSELECTED = '取消选择',
  30 + DELETE_SELECT = '删除选定',
  31 + APPLY_CHANGE = '应用更改',
  32 + UNDO_CHANGE = '撤销更改',
  33 +
  34 + SELECT_ALL = '选择全部',
  35 +}
  36 +
  37 +export enum RuleChainContextMenuIconEnum {
  38 + DETAIL = 'material-symbols:menu',
  39 + COPY = 'material-symbols:content-copy',
  40 + DELETE = 'material-symbols:delete',
  41 +
  42 + SELECT_COPY = 'material-symbols:content-copy',
  43 + PASTE = 'material-symbols:content-paste',
  44 + UNSELECTED = 'material-symbols:tab-unselected',
  45 + DELETE_SELECT = 'material-symbols:close',
  46 + APPLY_CHANGE = 'material-symbols:done',
  47 + UNDO_CHANGE = 'material-symbols:close',
  48 +
  49 + SELECT_ALL = 'material-symbols:select-all',
  50 +
  51 + // LINK = 'material-symbols:trending-flat',
  52 +}
  53 +
  54 +export enum RuleChainContextMenuShortcutKeyEnum {
  55 + DELETE = 'Ctrl(⌘) X',
  56 +
  57 + SELECT_COPY = 'Ctrl(⌘) C',
  58 + PASTE = 'Ctrl(⌘) V',
  59 + UNSELECTED = 'Esc',
  60 + DELETE_SELECT = 'Del',
  61 + APPLY_CHANGE = 'Ctrl(⌘) S',
  62 + UNDO_CHANGE = 'Ctrl(⌘) Z',
  63 +
  64 + SELECT_ALL = 'Ctrl(⌘) A',
  65 +}
  66 +
  67 +const getMenuItem = (key: RuleContextMenuEnum, handler: Fn, disabled = false) => {
  68 + return {
  69 + key,
  70 + label: RuleContextMenuNameEnum[key],
  71 + icon: RuleChainContextMenuIconEnum[key],
  72 + shortcutKey: RuleChainContextMenuShortcutKeyEnum[key],
  73 + handler: () => handler?.(key),
  74 + disabled,
  75 + } as RuleChainContextMenuItemType;
  76 +};
  77 +
  78 +const getDivider = (): RuleChainContextMenuItemType => ({ divider: true });
  79 +
  80 +export function useCreateRuleChainContextMenu() {
  81 + const [createContextMenu] = useContextMenu();
  82 +
  83 + const createNodeContextMenu = (params: NodeMouseEvent): Promise<RuleContextMenuEnum | ''> => {
  84 + return new Promise(async (resolve) => {
  85 + await createContextMenu(params, {
  86 + items: [
  87 + getMenuItem(RuleContextMenuEnum.DETAIL, resolve),
  88 + getMenuItem(RuleContextMenuEnum.COPY, resolve),
  89 + getMenuItem(RuleContextMenuEnum.DELETE, resolve),
  90 + ],
  91 + });
  92 + resolve('');
  93 + });
  94 + };
  95 +
  96 + const createElementsSelectedContextMenu = (
  97 + params: NodeMouseEvent,
  98 + changeMarker: boolean,
  99 + elementsType: ElementsTypeEnum.NODE = ElementsTypeEnum.NODE
  100 + ): Promise<RuleContextMenuEnum | ''> => {
  101 + return new Promise(async (resolve) => {
  102 + await createContextMenu(params, {
  103 + items: [
  104 + ...(elementsType === ElementsTypeEnum.NODE
  105 + ? [getMenuItem(RuleContextMenuEnum.SELECT_COPY, resolve)]
  106 + : []),
  107 + getMenuItem(RuleContextMenuEnum.PASTE, resolve, !checkHasCacheRuleNode()),
  108 + getDivider(),
  109 + getMenuItem(RuleContextMenuEnum.UNSELECTED, resolve),
  110 + getMenuItem(RuleContextMenuEnum.DELETE_SELECT, resolve),
  111 + getDivider(),
  112 + getMenuItem(RuleContextMenuEnum.APPLY_CHANGE, resolve, !changeMarker),
  113 + getMenuItem(RuleContextMenuEnum.UNDO_CHANGE, resolve, !changeMarker),
  114 + ],
  115 + });
  116 + resolve('');
  117 + });
  118 + };
  119 +
  120 + const createPanelContextMenu = (
  121 + params: NodeMouseEvent,
  122 + changeMarker: boolean
  123 + ): Promise<RuleContextMenuEnum | ''> => {
  124 + return new Promise(async (resolve) => {
  125 + await createContextMenu(params, {
  126 + items: [
  127 + getMenuItem(RuleContextMenuEnum.PASTE, resolve, !checkHasCacheRuleNode()),
  128 + getMenuItem(RuleContextMenuEnum.SELECT_ALL, resolve),
  129 + getMenuItem(RuleContextMenuEnum.APPLY_CHANGE, resolve, !changeMarker),
  130 + getMenuItem(RuleContextMenuEnum.UNDO_CHANGE, resolve, !changeMarker),
  131 + ],
  132 + });
  133 + resolve('');
  134 + });
  135 + };
  136 +
  137 + const createEdgeContextMenu = (
  138 + params: NodeMouseEvent,
  139 + isInput = false
  140 + ): Promise<RuleContextMenuEnum | ''> => {
  141 + return new Promise(async (resolve) => {
  142 + await createContextMenu(params, {
  143 + items: [
  144 + ...(isInput ? [] : [getMenuItem(RuleContextMenuEnum.DETAIL, resolve)]),
  145 + getMenuItem(RuleContextMenuEnum.DELETE, resolve),
  146 + ],
  147 + });
  148 + resolve('');
  149 + });
  150 + };
  151 + return {
  152 + createNodeContextMenu,
  153 + createElementsSelectedContextMenu,
  154 + createPanelContextMenu,
  155 + createEdgeContextMenu,
  156 + };
  157 +}
... ...
  1 +import { Edge, Node } from '@vue-flow/core';
  2 +import { RULE_NODE_KEY, RULE_NODE_LOCAL_CACHE_KEY } from '/@/enums/cacheEnum';
  3 +import { createLocalStorage } from '/@/utils/cache';
  4 +
  5 +const ruleNodeStorage = createLocalStorage({ prefixKey: RULE_NODE_LOCAL_CACHE_KEY });
  6 +
  7 +interface RuleNodeCacheType {
  8 + nodes?: Node[];
  9 + edges?: Edge[];
  10 + originX?: number;
  11 + originY?: number;
  12 +}
  13 +
  14 +export const setRuleNodeCache = ({
  15 + nodes = [],
  16 + edges = [],
  17 + originX,
  18 + originY,
  19 +}: RuleNodeCacheType) => {
  20 + ruleNodeStorage.set(RULE_NODE_KEY, {
  21 + nodes,
  22 + edges,
  23 + originX,
  24 + originY,
  25 + });
  26 +};
  27 +
  28 +export const getRuleNodeCache = (): RuleNodeCacheType => ruleNodeStorage.get(RULE_NODE_KEY);
  29 +
  30 +export const checkHasCacheRuleNode = () => !!getRuleNodeCache();
  31 +
  32 +function initRuleNodeStorage() {
  33 + const value = ruleNodeStorage.get(RULE_NODE_KEY);
  34 + value && ruleNodeStorage.set(RULE_NODE_KEY, value);
  35 +}
  36 +
  37 +initRuleNodeStorage();
... ...
... ... @@ -4,29 +4,38 @@ import type {
4 4 NodeComponent,
5 5 ValidConnectionFunc,
6 6 GraphNode,
  7 + NodeMouseEvent,
  8 + GraphEdge,
  9 + EdgeMouseEvent,
7 10 } from '@vue-flow/core';
8   -import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core';
9 11 import type { Ref } from 'vue';
10   -import { markRaw, toRaw, unref } from 'vue';
11   -import { isFunction } from 'lodash-es';
12 12 import type { CreateNodeModal } from '../src/components/CreateNodeModal';
13   -import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum';
14   -import { BasicEdge, BasicNode } from '../src/components';
15 13 import type { EdgeData, NodeData } from '../types/node';
16 14 import type { CreateEdgeModal } from '../src/components/CreateEdgeModal';
  15 +import { BasicEdge, BasicNode } from '../src/components';
  16 +import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum';
  17 +import { markRaw, toRaw, unref } from 'vue';
  18 +import { isFunction } from 'lodash-es';
  19 +import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core';
17 20 import { isInputHandle, isOutputHandle } from '../utils';
18 21 import { useAddEdges } from './useAddEdges';
19 22 import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer';
20 23 import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer';
21 24 import { isNumber } from '/@/utils/is';
  25 +import { RuleContextMenuEnum, useCreateRuleChainContextMenu } from './useRuleChainContextMenu';
  26 +import { RuleChainDetail } from '../types/ruleNode';
  27 +import { useContextMenuAction } from './useContextMenuAction';
  28 +import { useSaveAndRedo } from './useSaveAndRedo';
  29 +import { EntryCategoryComponentEnum } from '../enum/category';
22 30
23 31 interface UseRuleFlowOptionsType {
24 32 id: string;
  33 + ruleChainDetail: Ref<RuleChainDetail | undefined>;
25 34 createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
26 35 createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>;
27 36 updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>;
28 37 updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>;
29   - triggerChange: () => void;
  38 + useSaveAndRedoActionType: ReturnType<typeof useSaveAndRedo>;
30 39 }
31 40
32 41 const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => {
... ... @@ -40,12 +49,15 @@ const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) =>
40 49 export function useRuleFlow(options: UseRuleFlowOptionsType) {
41 50 const {
42 51 id,
  52 + ruleChainDetail,
43 53 createEdgeModalActionType,
44 54 updateEdgeDrawerActionType,
45 55 updateNodeDrawerActionType,
46   - triggerChange,
  56 + useSaveAndRedoActionType,
47 57 } = options;
48 58
  59 + const { triggerChange } = useSaveAndRedoActionType;
  60 +
49 61 const flowActionType = useVueFlow({
50 62 id,
51 63 maxZoom: 1,
... ... @@ -96,6 +108,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
96 108 onNodeDoubleClick,
97 109 onEdgeDoubleClick,
98 110 onNodeDragStop,
  111 + onNodeContextMenu,
  112 + onEdgeContextMenu,
  113 + onPaneContextMenu,
99 114 } = flowActionType;
100 115
101 116 const { getAddedgesParams } = useAddEdges();
... ... @@ -125,6 +140,119 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
125 140 });
126 141
127 142 onNodeDoubleClick(async ({ node }) => {
  143 + handleUpdateNode(node);
  144 + });
  145 +
  146 + onEdgeDoubleClick(async ({ edge }) => {
  147 + handleUpdateEdge(edge);
  148 + });
  149 +
  150 + onNodeDragStop(() => {
  151 + triggerChange();
  152 + });
  153 +
  154 + const {
  155 + createNodeContextMenu,
  156 + createElementsSelectedContextMenu,
  157 + createEdgeContextMenu,
  158 + createPanelContextMenu,
  159 + } = useCreateRuleChainContextMenu();
  160 +
  161 + const { handleContextMenuAction } = useContextMenuAction();
  162 +
  163 + onNodeContextMenu(async (params) => {
  164 + const menuType = params.node.selected
  165 + ? await createElementsSelectedContextMenu(
  166 + params,
  167 + unref(useSaveAndRedoActionType.changeMarker)
  168 + )
  169 + : await createNodeContextMenu(params);
  170 +
  171 + if (menuType) {
  172 + if (menuType === RuleContextMenuEnum.DETAIL) {
  173 + handleUpdateNode(params.node);
  174 + return;
  175 + }
  176 +
  177 + handleContextMenuAction({
  178 + menuType,
  179 + flowActionType,
  180 + event: params.event,
  181 + node: params.node,
  182 + useSaveAndRedoActionType,
  183 + });
  184 + }
  185 + });
  186 +
  187 + onEdgeContextMenu(async (params) => {
  188 + const isInputNode =
  189 + (params.edge.sourceNode.data as NodeData).config?.key === EntryCategoryComponentEnum.INPUT;
  190 + const menuType = await createEdgeContextMenu(
  191 + getCreateEdgeContextMenuParams(params),
  192 + isInputNode
  193 + );
  194 +
  195 + if (menuType) {
  196 + if (menuType === RuleContextMenuEnum.DETAIL) {
  197 + handleUpdateEdge(params.edge);
  198 + return;
  199 + }
  200 +
  201 + handleContextMenuAction({
  202 + menuType,
  203 + flowActionType,
  204 + event: params.event,
  205 + useSaveAndRedoActionType,
  206 + edge: params.edge,
  207 + });
  208 + }
  209 + });
  210 +
  211 + onPaneContextMenu(async (params) => {
  212 + const menuType = unref(flowActionType.getSelectedElements).length
  213 + ? await createElementsSelectedContextMenu(
  214 + getCreatePanelContextMenuParams(params),
  215 + unref(useSaveAndRedoActionType.changeMarker)
  216 + )
  217 + : await createPanelContextMenu(
  218 + getCreatePanelContextMenuParams(params),
  219 + unref(useSaveAndRedoActionType.changeMarker)
  220 + );
  221 +
  222 + if (menuType) {
  223 + handleContextMenuAction({
  224 + menuType,
  225 + flowActionType,
  226 + event: params,
  227 + useSaveAndRedoActionType,
  228 + });
  229 + }
  230 + });
  231 +
  232 + /**
  233 + * @description 验证是否有连接label
  234 + * @param sourceData
  235 + * @returns
  236 + */
  237 + function validateHasLabelConnection(sourceData: NodeData) {
  238 + return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length;
  239 + }
  240 +
  241 + function handleMaxConnectionPoint(sourceNode?: GraphNode) {
  242 + if (!sourceNode) return;
  243 +
  244 + const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint;
  245 +
  246 + if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return;
  247 +
  248 + const sourceId = sourceNode.id;
  249 + const connectionPool = unref(getEdges).filter((item) => item.source === sourceId);
  250 + if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) {
  251 + removeEdges(connectionPool[0].id);
  252 + }
  253 + }
  254 +
  255 + async function handleUpdateNode(node: GraphNode) {
128 256 if ((node.data as NodeData).config?.disableAction) return;
129 257 const { flag, data } =
130 258 (await unref(updateNodeDrawerActionType)?.open(
... ... @@ -137,9 +265,10 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
137 265
138 266 const currentNode = findNode(node.id);
139 267 (currentNode!.data as NodeData).data = data;
140   - });
  268 + triggerChange();
  269 + }
141 270
142   - onEdgeDoubleClick(async ({ edge }) => {
  271 + async function handleUpdateEdge(edge: GraphEdge) {
143 272 if (!validateHasLabelConnection(edge.sourceNode.data)) return;
144 273
145 274 if ((edge.sourceNode.data as NodeData).config?.disableAction) return;
... ... @@ -156,33 +285,48 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
156 285 const currentEdge = findEdge(edge.id);
157 286
158 287 (currentEdge!.data as EdgeData).data = toRaw(unref(data));
159   - });
160   -
161   - onNodeDragStop(() => {
162   - triggerChange();
163   - });
164 288
165   - /**
166   - * @description 验证是否有连接label
167   - * @param sourceData
168   - * @returns
169   - */
170   - function validateHasLabelConnection(sourceData: NodeData) {
171   - return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length;
  289 + triggerChange?.();
172 290 }
173 291
174   - function handleMaxConnectionPoint(sourceNode?: GraphNode) {
175   - if (!sourceNode) return;
176   -
177   - const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint;
178   -
179   - if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return;
  292 + function getCreatePanelContextMenuParams(params: Event) {
  293 + return {
  294 + event: params as MouseEvent,
  295 + node: {
  296 + data: {
  297 + data: { name: '规则链' },
  298 + config: {
  299 + name: unref(ruleChainDetail)?.name,
  300 + backgroundColor: '#aac7e4',
  301 + configurationDescriptor: {
  302 + nodeDefinition: {
  303 + icon: 'material-symbols:settings-ethernet',
  304 + },
  305 + },
  306 + },
  307 + },
  308 + },
  309 + } as NodeMouseEvent;
  310 + }
180 311
181   - const sourceId = sourceNode.id;
182   - const connectionPool = unref(getEdges).filter((item) => item.source === sourceId);
183   - if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) {
184   - removeEdges(connectionPool[0].id);
185   - }
  312 + function getCreateEdgeContextMenuParams(params: EdgeMouseEvent) {
  313 + return {
  314 + event: params.event as MouseEvent,
  315 + node: {
  316 + data: {
  317 + data: { name: '链接' },
  318 + config: {
  319 + name: unref(params.edge.data as EdgeData)?.data?.type?.join(' / '),
  320 + backgroundColor: '#aac7e4',
  321 + configurationDescriptor: {
  322 + nodeDefinition: {
  323 + icon: 'material-symbols:trending-flat',
  324 + },
  325 + },
  326 + },
  327 + },
  328 + },
  329 + } as NodeMouseEvent;
186 330 }
187 331
188 332 return { flowActionType };
... ...
... ... @@ -3,11 +3,12 @@ import { ComputedRef, computed, ref, unref } from 'vue';
3 3 import { BasicNodeBindData, EdgeData, NodeData } from '../types/node';
4 4 import { EntryCategoryComponentEnum } from '../enum/category';
5 5 import { useBasicDataTransform } from './useBasicDataTransform';
6   -import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner';
7   -import { ConnectionItemType, RuleChainType } from '../types/ruleNode';
  6 +import { getRuleChainData, getRuleChainDetail, saveRuleChainData } from '/@/api/ruleDesigner';
  7 +import { ConnectionItemType, RuleChainDetail, RuleChainType } from '../types/ruleNode';
8 8 import { useInputNode } from './useInputNode';
9 9 import { buildUUID } from '/@/utils/uuid';
10 10 import { useRoute } from 'vue-router';
  11 +import { RuleChainEntityType } from '../enum/entity';
11 12
12 13 const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT];
13 14
... ... @@ -24,6 +25,8 @@ export function useSaveAndRedo() {
24 25
25 26 const getRuleChainId = computed(() => (route.params as Record<'id', string>).id);
26 27
  28 + const ruleChainDetail = ref<RuleChainDetail>();
  29 +
27 30 const { mergeData, deconstructionData } = useBasicDataTransform();
28 31
29 32 const triggerChange = () => {
... ... @@ -73,7 +76,16 @@ export function useSaveAndRedo() {
73 76
74 77 const data = nodeData.data;
75 78
76   - nodes.push(mergeData(data, nodeData, node));
  79 + nodes.push(
  80 + Object.assign(
  81 + mergeData(data, nodeData, node),
  82 + nodeData.created
  83 + ? ({
  84 + id: { id: node.id, entityType: RuleChainEntityType.RULE_NODE },
  85 + } as BasicNodeBindData)
  86 + : {}
  87 + )
  88 + );
77 89 }
78 90
79 91 return nodes;
... ... @@ -118,6 +130,10 @@ export function useSaveAndRedo() {
118 130 resetChange();
119 131 };
120 132
  133 + async function getCurrentRuleChainDetail() {
  134 + ruleChainDetail.value = await getRuleChainDetail(unref(getRuleChainId));
  135 + }
  136 +
121 137 async function handleSaveRuleChain(
122 138 connections: ConnectionItemType[],
123 139 nodes: BasicNodeBindData[],
... ... @@ -125,12 +141,13 @@ export function useSaveAndRedo() {
125 141 ) {
126 142 try {
127 143 loading.value = true;
  144 +
128 145 const data = await saveRuleChainData({
129 146 connections,
130 147 nodes,
131 148 firstNodeIndex,
132 149 ruleChainId: {
133   - entityType: 'RULE_CHAIN',
  150 + entityType: RuleChainEntityType.RULE_CHAIN,
134 151 id: unref(getRuleChainId),
135 152 },
136 153 });
... ... @@ -188,10 +205,12 @@ export function useSaveAndRedo() {
188 205 loading,
189 206 debugMarker,
190 207 changeMarker,
  208 + ruleChainDetail,
191 209 triggerChange,
192 210 handleApplyChange,
193 211 handleRedoChange,
194 212 handleRemoveDebug,
195 213 getCurrentPageMetaData,
  214 + getCurrentRuleChainDetail,
196 215 };
197 216 }
... ...
... ... @@ -40,23 +40,28 @@
40 40
41 41 const elements = ref([]);
42 42
  43 + const useSaveAndRedoActionType = useSaveAndRedo();
  44 +
43 45 const {
44 46 loading,
45 47 changeMarker,
  48 + ruleChainDetail,
46 49 getCurrentPageMetaData,
47 50 triggerChange,
48 51 handleApplyChange,
49 52 handleRedoChange,
50 53 handleRemoveDebug,
51   - } = useSaveAndRedo();
  54 + getCurrentRuleChainDetail,
  55 + } = useSaveAndRedoActionType;
52 56
53 57 const { flowActionType } = useRuleFlow({
54 58 id: getId,
  59 + ruleChainDetail,
55 60 createNodeModalActionType,
56 61 createEdgeModalActionType,
57 62 updateEdgeDrawerActionType,
58 63 updateNodeDrawerActionType,
59   - triggerChange,
  64 + useSaveAndRedoActionType,
60 65 });
61 66
62 67 const { handleOnDragOver, handleOnDrop } = useDragCreate({
... ... @@ -79,12 +84,12 @@
79 84 );
80 85
81 86 const handleDeleteSelectionElements = () => {
82   - flowActionType.removeEdges(unref(flowActionType.getSelectedEdges));
83 87 flowActionType.removeNodes(unref(flowActionType.getSelectedNodes));
84 88 };
85 89
86 90 onMounted(() => {
87 91 getCurrentPageMetaData(flowActionType);
  92 + getCurrentRuleChainDetail();
88 93 });
89 94
90 95 createFlowContext({
... ...
... ... @@ -12,7 +12,6 @@
12 12 const ROUTER = useRouter();
13 13
14 14 const handleClick = () => {
15   - console.log(props);
16 15 const { data } = props.nodeProps?.data || ({} as NodeData);
17 16 const { configuration } = (data || {}) as { configuration: Record<'ruleChainId', string> };
18 17 if (configuration.ruleChainId) {
... ... @@ -22,12 +21,13 @@
22 21 </script>
23 22
24 23 <template>
25   - <div class="w-full h-6 flex justify-end" @click="handleClick">
  24 + <div class="w-full h-6 flex justify-end">
26 25 <Tooltip color="#fff">
27 26 <template #title>
28 27 <span class="text-slate-500 italic">打开规则链</span>
29 28 </template>
30 29 <Icon
  30 + @click="handleClick"
31 31 icon="material-symbols:login"
32 32 class="cursor-pointer svg:text-lg svg:text-light-50 border-1 border-light-50 bg-purple-400 hover:bg-purple-500 rounded"
33 33 />
... ...
... ... @@ -29,6 +29,11 @@
29 29 return config?.name;
30 30 });
31 31
  32 + const getIconUrl = computed(() => {
  33 + const { iconUrl } = unref(getNodeDefinition);
  34 + return iconUrl;
  35 + });
  36 +
32 37 const getIcon = computed(() => {
33 38 const { icon } = unref(getNodeDefinition);
34 39 const { category } = unref(getData);
... ... @@ -64,10 +69,11 @@
64 69 </template>
65 70 <main
66 71 class="basic-node-hover flex items-center w-44 h-12 rounded border px-4 py-2 border-gray-700 dark:text-light-50"
67   - :style="{ backgroundColor: getBackgroundColor }"
  72 + :style="{ backgroundColor: getBackgroundColor, outline: selected ? '3px solid red' : 'none' }"
68 73 >
69 74 <div>
70   - <Icon class="text-2xl dark:text-light-50" :icon="getIcon" />
  75 + <Icon v-if="!getIconUrl" class="text-2xl dark:text-light-50" :icon="getIcon" />
  76 + <img v-if="getIconUrl" :src="getIconUrl" class="w-4 h-4" />
71 77 </div>
72 78 <BasicToolbar v-if="!getData.config?.disableAction" v-bind="$props" />
73 79 <div class="flex text-xs flex-col ml-2 text-left truncate">
... ...
  1 +import { NodeMouseEvent } from '@vue-flow/core';
  2 +import ContextMenuVue from './index.vue';
  3 +import { isClient } from '/@/utils/is';
  4 +import { createVNode, render, getCurrentInstance, onUnmounted, toRaw, unref } from 'vue';
  5 +
  6 +const menuManager: {
  7 + domList: Element[];
  8 + resolve: Fn;
  9 +} = {
  10 + domList: [],
  11 + resolve: () => {},
  12 +};
  13 +
  14 +const createContextMenu = function (
  15 + options: NodeMouseEvent,
  16 + params?: InstanceType<typeof ContextMenuVue>['$props']
  17 +) {
  18 + const { event } = options || {};
  19 +
  20 + if (!(params?.items && params?.items.length)) return;
  21 +
  22 + event && event?.preventDefault();
  23 +
  24 + if (!isClient) {
  25 + return;
  26 + }
  27 +
  28 + return new Promise((resolve) => {
  29 + const body = document.body;
  30 +
  31 + const container = document.createElement('div');
  32 + const propsData: Partial<InstanceType<typeof ContextMenuVue>['$props']> = {
  33 + ...params,
  34 + };
  35 +
  36 + if (options.event) {
  37 + const { clientX, clientY } = options.event as MouseEvent;
  38 + propsData.axis = { x: clientX, y: clientY };
  39 + }
  40 +
  41 + if (options.node) {
  42 + propsData.nodeData = toRaw(unref(options.node.data));
  43 + }
  44 +
  45 + const vm = createVNode(ContextMenuVue, propsData);
  46 + render(vm, container);
  47 +
  48 + const handleClick = function () {
  49 + menuManager.resolve('');
  50 + };
  51 +
  52 + menuManager.domList.push(container);
  53 +
  54 + const remove = function () {
  55 + menuManager.domList.forEach((dom: Element) => {
  56 + try {
  57 + dom && body.removeChild(dom);
  58 + } catch (error) {}
  59 + });
  60 + body.removeEventListener('click', handleClick);
  61 + body.removeEventListener('scroll', handleClick);
  62 + };
  63 +
  64 + menuManager.resolve = function (arg) {
  65 + remove();
  66 + resolve(arg);
  67 + };
  68 + remove();
  69 + body.appendChild(container);
  70 + body.addEventListener('click', handleClick);
  71 + body.addEventListener('scroll', handleClick);
  72 + });
  73 +};
  74 +
  75 +const destroyContextMenu = function () {
  76 + if (menuManager) {
  77 + menuManager.resolve('');
  78 + menuManager.domList = [];
  79 + }
  80 +};
  81 +
  82 +export function useContextMenu(authRemove = true) {
  83 + if (getCurrentInstance() && authRemove) {
  84 + onUnmounted(() => {
  85 + destroyContextMenu();
  86 + });
  87 + }
  88 + return [createContextMenu, destroyContextMenu];
  89 +}
... ...
  1 +export { default as RuleChainContextMenu } from './index.vue';
  2 +export { useContextMenu } from './createContextMenu';
... ...
  1 +export interface RuleChainContextMenuItemType {
  2 + icon?: string;
  3 + key?: string;
  4 + label?: string;
  5 + shortcutKey?: string;
  6 + divider?: boolean;
  7 + disabled?: boolean;
  8 + handler?: Fn;
  9 +}
... ...
  1 +<script setup lang="ts">
  2 + import { computed, CSSProperties, unref } from 'vue';
  3 + import { NodeData } from '../../../types/node';
  4 + import { RuleChainContextMenuItemType } from './index.type';
  5 + import { Icon } from '/@/components/Icon';
  6 + import { Divider } from 'ant-design-vue';
  7 +
  8 + const props = withDefaults(
  9 + defineProps<{
  10 + width?: number;
  11 + itemHeight?: number;
  12 + styles?: CSSProperties;
  13 + axis?: Record<'x' | 'y', number>;
  14 + items?: RuleChainContextMenuItemType[];
  15 + nodeData?: NodeData;
  16 + }>(),
  17 + {
  18 + width: 320,
  19 + itemHeight: 48,
  20 + axis: () => ({ x: 0, y: 0 }),
  21 + items: () => [],
  22 + nodeData: () => ({}),
  23 + }
  24 + );
  25 +
  26 + const getMenuHeight = computed(() => {
  27 + const { items, itemHeight } = props;
  28 + let dividerNumber = 0;
  29 + let menuNumber = 1;
  30 + for (const item of items) {
  31 + if (item.divider) dividerNumber++;
  32 + else menuNumber++;
  33 + }
  34 +
  35 + return itemHeight * menuNumber + dividerNumber + 8;
  36 + });
  37 +
  38 + const getStyle = computed((): CSSProperties => {
  39 + const { axis, styles, width } = props;
  40 + const { x, y } = axis;
  41 + const menuHeight = unref(getMenuHeight);
  42 + const menuWidth = width;
  43 + const body = document.body;
  44 +
  45 + const left = body.clientWidth < x + menuWidth ? x - menuWidth : x;
  46 + const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
  47 +
  48 + return {
  49 + ...styles,
  50 + position: 'absolute',
  51 + width: `${width}px`,
  52 + left: `${left + 1}px`,
  53 + top: `${top + 1}px`,
  54 + };
  55 + });
  56 +
  57 + const getNodeIconUrl = computed(() => {
  58 + const { config } = props.nodeData;
  59 + const { configurationDescriptor } = config || {};
  60 + const { nodeDefinition } = configurationDescriptor || {};
  61 + const { iconUrl } = nodeDefinition || {};
  62 + return iconUrl;
  63 + });
  64 +
  65 + const getNodeIcon = computed(() => {
  66 + const { nodeData } = props;
  67 + const { category, config } = nodeData;
  68 + const { icon: categoryIcon } = category || {};
  69 + const { configurationDescriptor } = config || {};
  70 + const { nodeDefinition } = configurationDescriptor || {};
  71 + const { icon } = nodeDefinition || {};
  72 +
  73 + return categoryIcon || icon;
  74 + });
  75 +
  76 + const getTitleBackgroundColor = computed(() => {
  77 + const { category, config } = props.nodeData;
  78 + const { backgroundColor: categoryBackgroundColor } = category || {};
  79 + const { backgroundColor } = config || {};
  80 + return categoryBackgroundColor || backgroundColor;
  81 + });
  82 +</script>
  83 +
  84 +<template>
  85 + <div
  86 + :style="getStyle"
  87 + class="bg-light-50 shadow-lg shadow-dark-50 z-50 rounded-md overflow-hidden pb-2"
  88 + >
  89 + <div
  90 + v-if="nodeData"
  91 + :style="{ backgroundColor: getTitleBackgroundColor, height: `${itemHeight}px` }"
  92 + class="flex items-center p-2"
  93 + >
  94 + <Icon v-if="!getNodeIconUrl" :icon="getNodeIcon" class="svg:text-2xl" />
  95 + <img v-if="getNodeIconUrl" :src="getNodeIconUrl" class="w-6 h-6" />
  96 + <div class="ml-4">
  97 + <div class="font-medium">{{ nodeData.config?.name }}</div>
  98 + <div class="text-xs">{{ nodeData.data?.name }}</div>
  99 + </div>
  100 + </div>
  101 + <div>
  102 + <template v-for="item in items" :key="item.key">
  103 + <div
  104 + v-if="!item.divider"
  105 + :style="{ height: `${itemHeight}px` }"
  106 + class="px-4 flex items-center cursor-pointer hover:bg-neutral-100"
  107 + :class="item.disabled && 'disables'"
  108 + @click="(event) => !item.disabled && item?.handler?.(event)"
  109 + >
  110 + <Icon :icon="item.icon" class="svg:text-2xl" />
  111 + <div class="flex-auto px-4">{{ item.label }}</div>
  112 + <div class="flex items-center"> {{ item.shortcutKey }} </div>
  113 + </div>
  114 + <Divider v-if="item.divider" class="!m-0" />
  115 + </template>
  116 + </div>
  117 + </div>
  118 +</template>
  119 +
  120 +<style lang="less" scoped>
  121 + .disables {
  122 + @apply pointer-events-none text-gray-300;
  123 + }
  124 +</style>
... ...
... ... @@ -113,7 +113,11 @@
113 113 <Empty v-if="!shadowComponent" description="未找到链接组件" />
114 114 </Spin>
115 115 </Tabs.TabPane>
116   - <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.EVENT]" :key="TabsPanelEnum.EVENT">
  116 + <Tabs.TabPane
  117 + v-if="nodeData?.created"
  118 + :tab="TabsPanelNameEnum[TabsPanelEnum.EVENT]"
  119 + :key="TabsPanelEnum.EVENT"
  120 + >
117 121 <BasicEvents :elementInfo="elementInfo" />
118 122 </Tabs.TabPane>
119 123 <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.HELP]" :key="TabsPanelEnum.HELP">
... ...
... ... @@ -58,3 +58,7 @@
58 58 .vue-flow__nodesselection-rect, .vue-flow__selection{
59 59 border-width: 3px;
60 60 }
  61 +
  62 +.vue-flow__nodesselection-rect {
  63 + pointer-events: none;
  64 +}
... ...
... ... @@ -137,6 +137,7 @@ export interface NodeData<T = BasicNodeFormData> {
137 137 category?: CategoryConfigType;
138 138 config?: NodeItemConfigType;
139 139 data?: T;
  140 + created?: boolean;
140 141 }
141 142
142 143 export interface EdgeData {
... ...
... ... @@ -18,3 +18,20 @@ export interface ConnectionItemType {
18 18 toIndex: number;
19 19 type: string;
20 20 }
  21 +
  22 +export interface RuleChainDetail {
  23 + id: Id;
  24 + createdTime: number;
  25 + additionalInfo: AdditionalInfo;
  26 + tenantId: Id;
  27 + name: Id;
  28 + type: string;
  29 + firstRuleNodeId: Id;
  30 + root: boolean;
  31 + debugMode: boolean;
  32 + configuration: any;
  33 +}
  34 +
  35 +export interface AdditionalInfo {
  36 + description: string;
  37 +}
... ...