Showing
21 changed files
with
900 additions
and
46 deletions
1 | import { RuleChainPaginationItemType } from './model/type'; | 1 | import { RuleChainPaginationItemType } from './model/type'; |
2 | import { TBPaginationResult } from '/#/axios'; | 2 | import { TBPaginationResult } from '/#/axios'; |
3 | import { defHttp } from '/@/utils/http/axios'; | 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 | enum Api { | 6 | enum Api { |
7 | + GET_RULE_CHAINS_DETAIL = '/ruleChain', | ||
7 | SAVE = '/ruleChain/metadata', | 8 | SAVE = '/ruleChain/metadata', |
8 | GET_RULE_CHAINES = '/ruleChains', | 9 | GET_RULE_CHAINES = '/ruleChains', |
9 | GET_RULE_NODE_EVENTS = '/events/RULE_NODE', | 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 | export const getRuleChainData = (id: string) => { | 22 | export const getRuleChainData = (id: string) => { |
13 | return defHttp.get<RuleChainType>( | 23 | return defHttp.get<RuleChainType>( |
14 | { | 24 | { |
@@ -38,6 +38,11 @@ export const PLATFORM = 'PLATFORM'; | @@ -38,6 +38,11 @@ export const PLATFORM = 'PLATFORM'; | ||
38 | export const PLATFORM_INFO_CACHE_KEY = 'PLATFORM_INFO'; | 38 | export const PLATFORM_INFO_CACHE_KEY = 'PLATFORM_INFO'; |
39 | 39 | ||
40 | export const MENU_LIST = 'MENU_LIST'; | 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 | export enum CacheTypeEnum { | 46 | export enum CacheTypeEnum { |
42 | SESSION, | 47 | SESSION, |
43 | LOCAL, | 48 | LOCAL, |
src/views/rule/designer/enum/entity.ts
0 → 100644
1 | import { Node } from '@vue-flow/core'; | 1 | import { Node } from '@vue-flow/core'; |
2 | import { NodeTypeEnum } from '../enum'; | 2 | import { NodeTypeEnum } from '../enum'; |
3 | import { buildUUID } from '/@/utils/uuid'; | 3 | import { buildUUID } from '/@/utils/uuid'; |
4 | +import { NodeData } from '../types/node'; | ||
4 | 5 | ||
5 | export const useAddNodes = () => { | 6 | export const useAddNodes = () => { |
6 | const getAddNodesParams = ( | 7 | const getAddNodesParams = ( |
7 | position: Node['position'], | 8 | position: Node['position'], |
8 | - data: object, | 9 | + data: NodeData, |
9 | options?: Partial<Node> | 10 | options?: Partial<Node> |
10 | ): Node => { | 11 | ): Node => { |
11 | return { | 12 | return { |
@@ -122,6 +122,7 @@ export function useBasicDataTransform() { | @@ -122,6 +122,7 @@ export function useBasicDataTransform() { | ||
122 | description, | 122 | description, |
123 | name, | 123 | name, |
124 | }, | 124 | }, |
125 | + created: !!id?.id, | ||
125 | }, | 126 | }, |
126 | { | 127 | { |
127 | id: id?.id || buildUUID(), | 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,29 +4,38 @@ import type { | ||
4 | NodeComponent, | 4 | NodeComponent, |
5 | ValidConnectionFunc, | 5 | ValidConnectionFunc, |
6 | GraphNode, | 6 | GraphNode, |
7 | + NodeMouseEvent, | ||
8 | + GraphEdge, | ||
9 | + EdgeMouseEvent, | ||
7 | } from '@vue-flow/core'; | 10 | } from '@vue-flow/core'; |
8 | -import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core'; | ||
9 | import type { Ref } from 'vue'; | 11 | import type { Ref } from 'vue'; |
10 | -import { markRaw, toRaw, unref } from 'vue'; | ||
11 | -import { isFunction } from 'lodash-es'; | ||
12 | import type { CreateNodeModal } from '../src/components/CreateNodeModal'; | 12 | import type { CreateNodeModal } from '../src/components/CreateNodeModal'; |
13 | -import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum'; | ||
14 | -import { BasicEdge, BasicNode } from '../src/components'; | ||
15 | import type { EdgeData, NodeData } from '../types/node'; | 13 | import type { EdgeData, NodeData } from '../types/node'; |
16 | import type { CreateEdgeModal } from '../src/components/CreateEdgeModal'; | 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 | import { isInputHandle, isOutputHandle } from '../utils'; | 20 | import { isInputHandle, isOutputHandle } from '../utils'; |
18 | import { useAddEdges } from './useAddEdges'; | 21 | import { useAddEdges } from './useAddEdges'; |
19 | import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer'; | 22 | import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer'; |
20 | import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer'; | 23 | import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer'; |
21 | import { isNumber } from '/@/utils/is'; | 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 | interface UseRuleFlowOptionsType { | 31 | interface UseRuleFlowOptionsType { |
24 | id: string; | 32 | id: string; |
33 | + ruleChainDetail: Ref<RuleChainDetail | undefined>; | ||
25 | createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>; | 34 | createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>; |
26 | createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>; | 35 | createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>; |
27 | updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>; | 36 | updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>; |
28 | updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>; | 37 | updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>; |
29 | - triggerChange: () => void; | 38 | + useSaveAndRedoActionType: ReturnType<typeof useSaveAndRedo>; |
30 | } | 39 | } |
31 | 40 | ||
32 | const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => { | 41 | const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => { |
@@ -40,12 +49,15 @@ const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => | @@ -40,12 +49,15 @@ const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => | ||
40 | export function useRuleFlow(options: UseRuleFlowOptionsType) { | 49 | export function useRuleFlow(options: UseRuleFlowOptionsType) { |
41 | const { | 50 | const { |
42 | id, | 51 | id, |
52 | + ruleChainDetail, | ||
43 | createEdgeModalActionType, | 53 | createEdgeModalActionType, |
44 | updateEdgeDrawerActionType, | 54 | updateEdgeDrawerActionType, |
45 | updateNodeDrawerActionType, | 55 | updateNodeDrawerActionType, |
46 | - triggerChange, | 56 | + useSaveAndRedoActionType, |
47 | } = options; | 57 | } = options; |
48 | 58 | ||
59 | + const { triggerChange } = useSaveAndRedoActionType; | ||
60 | + | ||
49 | const flowActionType = useVueFlow({ | 61 | const flowActionType = useVueFlow({ |
50 | id, | 62 | id, |
51 | maxZoom: 1, | 63 | maxZoom: 1, |
@@ -96,6 +108,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | @@ -96,6 +108,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | ||
96 | onNodeDoubleClick, | 108 | onNodeDoubleClick, |
97 | onEdgeDoubleClick, | 109 | onEdgeDoubleClick, |
98 | onNodeDragStop, | 110 | onNodeDragStop, |
111 | + onNodeContextMenu, | ||
112 | + onEdgeContextMenu, | ||
113 | + onPaneContextMenu, | ||
99 | } = flowActionType; | 114 | } = flowActionType; |
100 | 115 | ||
101 | const { getAddedgesParams } = useAddEdges(); | 116 | const { getAddedgesParams } = useAddEdges(); |
@@ -125,6 +140,119 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | @@ -125,6 +140,119 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | ||
125 | }); | 140 | }); |
126 | 141 | ||
127 | onNodeDoubleClick(async ({ node }) => { | 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 | if ((node.data as NodeData).config?.disableAction) return; | 256 | if ((node.data as NodeData).config?.disableAction) return; |
129 | const { flag, data } = | 257 | const { flag, data } = |
130 | (await unref(updateNodeDrawerActionType)?.open( | 258 | (await unref(updateNodeDrawerActionType)?.open( |
@@ -137,9 +265,10 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | @@ -137,9 +265,10 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | ||
137 | 265 | ||
138 | const currentNode = findNode(node.id); | 266 | const currentNode = findNode(node.id); |
139 | (currentNode!.data as NodeData).data = data; | 267 | (currentNode!.data as NodeData).data = data; |
140 | - }); | 268 | + triggerChange(); |
269 | + } | ||
141 | 270 | ||
142 | - onEdgeDoubleClick(async ({ edge }) => { | 271 | + async function handleUpdateEdge(edge: GraphEdge) { |
143 | if (!validateHasLabelConnection(edge.sourceNode.data)) return; | 272 | if (!validateHasLabelConnection(edge.sourceNode.data)) return; |
144 | 273 | ||
145 | if ((edge.sourceNode.data as NodeData).config?.disableAction) return; | 274 | if ((edge.sourceNode.data as NodeData).config?.disableAction) return; |
@@ -156,33 +285,48 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | @@ -156,33 +285,48 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { | ||
156 | const currentEdge = findEdge(edge.id); | 285 | const currentEdge = findEdge(edge.id); |
157 | 286 | ||
158 | (currentEdge!.data as EdgeData).data = toRaw(unref(data)); | 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 | return { flowActionType }; | 332 | return { flowActionType }; |
@@ -3,11 +3,12 @@ import { ComputedRef, computed, ref, unref } from 'vue'; | @@ -3,11 +3,12 @@ import { ComputedRef, computed, ref, unref } from 'vue'; | ||
3 | import { BasicNodeBindData, EdgeData, NodeData } from '../types/node'; | 3 | import { BasicNodeBindData, EdgeData, NodeData } from '../types/node'; |
4 | import { EntryCategoryComponentEnum } from '../enum/category'; | 4 | import { EntryCategoryComponentEnum } from '../enum/category'; |
5 | import { useBasicDataTransform } from './useBasicDataTransform'; | 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 | import { useInputNode } from './useInputNode'; | 8 | import { useInputNode } from './useInputNode'; |
9 | import { buildUUID } from '/@/utils/uuid'; | 9 | import { buildUUID } from '/@/utils/uuid'; |
10 | import { useRoute } from 'vue-router'; | 10 | import { useRoute } from 'vue-router'; |
11 | +import { RuleChainEntityType } from '../enum/entity'; | ||
11 | 12 | ||
12 | const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT]; | 13 | const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT]; |
13 | 14 | ||
@@ -24,6 +25,8 @@ export function useSaveAndRedo() { | @@ -24,6 +25,8 @@ export function useSaveAndRedo() { | ||
24 | 25 | ||
25 | const getRuleChainId = computed(() => (route.params as Record<'id', string>).id); | 26 | const getRuleChainId = computed(() => (route.params as Record<'id', string>).id); |
26 | 27 | ||
28 | + const ruleChainDetail = ref<RuleChainDetail>(); | ||
29 | + | ||
27 | const { mergeData, deconstructionData } = useBasicDataTransform(); | 30 | const { mergeData, deconstructionData } = useBasicDataTransform(); |
28 | 31 | ||
29 | const triggerChange = () => { | 32 | const triggerChange = () => { |
@@ -73,7 +76,16 @@ export function useSaveAndRedo() { | @@ -73,7 +76,16 @@ export function useSaveAndRedo() { | ||
73 | 76 | ||
74 | const data = nodeData.data; | 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 | return nodes; | 91 | return nodes; |
@@ -118,6 +130,10 @@ export function useSaveAndRedo() { | @@ -118,6 +130,10 @@ export function useSaveAndRedo() { | ||
118 | resetChange(); | 130 | resetChange(); |
119 | }; | 131 | }; |
120 | 132 | ||
133 | + async function getCurrentRuleChainDetail() { | ||
134 | + ruleChainDetail.value = await getRuleChainDetail(unref(getRuleChainId)); | ||
135 | + } | ||
136 | + | ||
121 | async function handleSaveRuleChain( | 137 | async function handleSaveRuleChain( |
122 | connections: ConnectionItemType[], | 138 | connections: ConnectionItemType[], |
123 | nodes: BasicNodeBindData[], | 139 | nodes: BasicNodeBindData[], |
@@ -125,12 +141,13 @@ export function useSaveAndRedo() { | @@ -125,12 +141,13 @@ export function useSaveAndRedo() { | ||
125 | ) { | 141 | ) { |
126 | try { | 142 | try { |
127 | loading.value = true; | 143 | loading.value = true; |
144 | + | ||
128 | const data = await saveRuleChainData({ | 145 | const data = await saveRuleChainData({ |
129 | connections, | 146 | connections, |
130 | nodes, | 147 | nodes, |
131 | firstNodeIndex, | 148 | firstNodeIndex, |
132 | ruleChainId: { | 149 | ruleChainId: { |
133 | - entityType: 'RULE_CHAIN', | 150 | + entityType: RuleChainEntityType.RULE_CHAIN, |
134 | id: unref(getRuleChainId), | 151 | id: unref(getRuleChainId), |
135 | }, | 152 | }, |
136 | }); | 153 | }); |
@@ -188,10 +205,12 @@ export function useSaveAndRedo() { | @@ -188,10 +205,12 @@ export function useSaveAndRedo() { | ||
188 | loading, | 205 | loading, |
189 | debugMarker, | 206 | debugMarker, |
190 | changeMarker, | 207 | changeMarker, |
208 | + ruleChainDetail, | ||
191 | triggerChange, | 209 | triggerChange, |
192 | handleApplyChange, | 210 | handleApplyChange, |
193 | handleRedoChange, | 211 | handleRedoChange, |
194 | handleRemoveDebug, | 212 | handleRemoveDebug, |
195 | getCurrentPageMetaData, | 213 | getCurrentPageMetaData, |
214 | + getCurrentRuleChainDetail, | ||
196 | }; | 215 | }; |
197 | } | 216 | } |
@@ -40,23 +40,28 @@ | @@ -40,23 +40,28 @@ | ||
40 | 40 | ||
41 | const elements = ref([]); | 41 | const elements = ref([]); |
42 | 42 | ||
43 | + const useSaveAndRedoActionType = useSaveAndRedo(); | ||
44 | + | ||
43 | const { | 45 | const { |
44 | loading, | 46 | loading, |
45 | changeMarker, | 47 | changeMarker, |
48 | + ruleChainDetail, | ||
46 | getCurrentPageMetaData, | 49 | getCurrentPageMetaData, |
47 | triggerChange, | 50 | triggerChange, |
48 | handleApplyChange, | 51 | handleApplyChange, |
49 | handleRedoChange, | 52 | handleRedoChange, |
50 | handleRemoveDebug, | 53 | handleRemoveDebug, |
51 | - } = useSaveAndRedo(); | 54 | + getCurrentRuleChainDetail, |
55 | + } = useSaveAndRedoActionType; | ||
52 | 56 | ||
53 | const { flowActionType } = useRuleFlow({ | 57 | const { flowActionType } = useRuleFlow({ |
54 | id: getId, | 58 | id: getId, |
59 | + ruleChainDetail, | ||
55 | createNodeModalActionType, | 60 | createNodeModalActionType, |
56 | createEdgeModalActionType, | 61 | createEdgeModalActionType, |
57 | updateEdgeDrawerActionType, | 62 | updateEdgeDrawerActionType, |
58 | updateNodeDrawerActionType, | 63 | updateNodeDrawerActionType, |
59 | - triggerChange, | 64 | + useSaveAndRedoActionType, |
60 | }); | 65 | }); |
61 | 66 | ||
62 | const { handleOnDragOver, handleOnDrop } = useDragCreate({ | 67 | const { handleOnDragOver, handleOnDrop } = useDragCreate({ |
@@ -79,12 +84,12 @@ | @@ -79,12 +84,12 @@ | ||
79 | ); | 84 | ); |
80 | 85 | ||
81 | const handleDeleteSelectionElements = () => { | 86 | const handleDeleteSelectionElements = () => { |
82 | - flowActionType.removeEdges(unref(flowActionType.getSelectedEdges)); | ||
83 | flowActionType.removeNodes(unref(flowActionType.getSelectedNodes)); | 87 | flowActionType.removeNodes(unref(flowActionType.getSelectedNodes)); |
84 | }; | 88 | }; |
85 | 89 | ||
86 | onMounted(() => { | 90 | onMounted(() => { |
87 | getCurrentPageMetaData(flowActionType); | 91 | getCurrentPageMetaData(flowActionType); |
92 | + getCurrentRuleChainDetail(); | ||
88 | }); | 93 | }); |
89 | 94 | ||
90 | createFlowContext({ | 95 | createFlowContext({ |
@@ -12,7 +12,6 @@ | @@ -12,7 +12,6 @@ | ||
12 | const ROUTER = useRouter(); | 12 | const ROUTER = useRouter(); |
13 | 13 | ||
14 | const handleClick = () => { | 14 | const handleClick = () => { |
15 | - console.log(props); | ||
16 | const { data } = props.nodeProps?.data || ({} as NodeData); | 15 | const { data } = props.nodeProps?.data || ({} as NodeData); |
17 | const { configuration } = (data || {}) as { configuration: Record<'ruleChainId', string> }; | 16 | const { configuration } = (data || {}) as { configuration: Record<'ruleChainId', string> }; |
18 | if (configuration.ruleChainId) { | 17 | if (configuration.ruleChainId) { |
@@ -22,12 +21,13 @@ | @@ -22,12 +21,13 @@ | ||
22 | </script> | 21 | </script> |
23 | 22 | ||
24 | <template> | 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 | <Tooltip color="#fff"> | 25 | <Tooltip color="#fff"> |
27 | <template #title> | 26 | <template #title> |
28 | <span class="text-slate-500 italic">打开规则链</span> | 27 | <span class="text-slate-500 italic">打开规则链</span> |
29 | </template> | 28 | </template> |
30 | <Icon | 29 | <Icon |
30 | + @click="handleClick" | ||
31 | icon="material-symbols:login" | 31 | icon="material-symbols:login" |
32 | class="cursor-pointer svg:text-lg svg:text-light-50 border-1 border-light-50 bg-purple-400 hover:bg-purple-500 rounded" | 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,6 +29,11 @@ | ||
29 | return config?.name; | 29 | return config?.name; |
30 | }); | 30 | }); |
31 | 31 | ||
32 | + const getIconUrl = computed(() => { | ||
33 | + const { iconUrl } = unref(getNodeDefinition); | ||
34 | + return iconUrl; | ||
35 | + }); | ||
36 | + | ||
32 | const getIcon = computed(() => { | 37 | const getIcon = computed(() => { |
33 | const { icon } = unref(getNodeDefinition); | 38 | const { icon } = unref(getNodeDefinition); |
34 | const { category } = unref(getData); | 39 | const { category } = unref(getData); |
@@ -64,10 +69,11 @@ | @@ -64,10 +69,11 @@ | ||
64 | </template> | 69 | </template> |
65 | <main | 70 | <main |
66 | class="basic-node-hover flex items-center w-44 h-12 rounded border px-4 py-2 border-gray-700 dark:text-light-50" | 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 | <div> | 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 | </div> | 77 | </div> |
72 | <BasicToolbar v-if="!getData.config?.disableAction" v-bind="$props" /> | 78 | <BasicToolbar v-if="!getData.config?.disableAction" v-bind="$props" /> |
73 | <div class="flex text-xs flex-col ml-2 text-left truncate"> | 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 | +<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,7 +113,11 @@ | ||
113 | <Empty v-if="!shadowComponent" description="未找到链接组件" /> | 113 | <Empty v-if="!shadowComponent" description="未找到链接组件" /> |
114 | </Spin> | 114 | </Spin> |
115 | </Tabs.TabPane> | 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 | <BasicEvents :elementInfo="elementInfo" /> | 121 | <BasicEvents :elementInfo="elementInfo" /> |
118 | </Tabs.TabPane> | 122 | </Tabs.TabPane> |
119 | <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.HELP]" :key="TabsPanelEnum.HELP"> | 123 | <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.HELP]" :key="TabsPanelEnum.HELP"> |
@@ -137,6 +137,7 @@ export interface NodeData<T = BasicNodeFormData> { | @@ -137,6 +137,7 @@ export interface NodeData<T = BasicNodeFormData> { | ||
137 | category?: CategoryConfigType; | 137 | category?: CategoryConfigType; |
138 | config?: NodeItemConfigType; | 138 | config?: NodeItemConfigType; |
139 | data?: T; | 139 | data?: T; |
140 | + created?: boolean; | ||
140 | } | 141 | } |
141 | 142 | ||
142 | export interface EdgeData { | 143 | export interface EdgeData { |
@@ -18,3 +18,20 @@ export interface ConnectionItemType { | @@ -18,3 +18,20 @@ export interface ConnectionItemType { | ||
18 | toIndex: number; | 18 | toIndex: number; |
19 | type: string; | 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 | +} |