Commit 5a1f1ae031dc48bb6a8b2d912c5452b5a42206d9

Authored by xp.Huang
2 parents 7f7e88ae 84825d99

Merge branch 'feat/rule-chain' into 'main_dev'

Feat/rule chain

See merge request yunteng/thingskit-front!851
1 -import { DeviceInfoItemType, DeviceTypeItem, PageParams } from './model'; 1 +import { DeviceInfoItemType, DeviceTypeItem, PageParams, ScriptTestParams } from './model';
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 4
@@ -6,6 +6,7 @@ enum Api { @@ -6,6 +6,7 @@ enum Api {
6 GET_DEVICE_INFOS = '/tenant/deviceInfos', 6 GET_DEVICE_INFOS = '/tenant/deviceInfos',
7 GET_DEVICE_TYPE = '/device/types', 7 GET_DEVICE_TYPE = '/device/types',
8 TENANT_QUEUE = '/tenant/queues', 8 TENANT_QUEUE = '/tenant/queues',
  9 + TEST_SCRIPT = '/ruleChain/testScript',
9 } 10 }
10 11
11 enum Entity { 12 enum Entity {
@@ -142,3 +143,13 @@ export const getEntityEdge = (params: PageParams) => { @@ -142,3 +143,13 @@ export const getEntityEdge = (params: PageParams) => {
142 } 143 }
143 ); 144 );
144 }; 145 };
  146 +
  147 +export const testScript = (data: ScriptTestParams) => {
  148 + return defHttp.post<Record<'error' | 'output', string>>(
  149 + {
  150 + url: Api.TEST_SCRIPT,
  151 + data,
  152 + },
  153 + { joinPrefix: false }
  154 + );
  155 +};
@@ -52,3 +52,12 @@ export interface PageParams { @@ -52,3 +52,12 @@ export interface PageParams {
52 sortProperty?: string; 52 sortProperty?: string;
53 sortOrder?: string; 53 sortOrder?: string;
54 } 54 }
  55 +
  56 +export interface ScriptTestParams {
  57 + argNames?: string[];
  58 + metadata?: Recordable;
  59 + msg?: string;
  60 + msgType?: string;
  61 + script?: string;
  62 + scriptType?: string;
  63 +}
@@ -4,12 +4,23 @@ import { defHttp } from '/@/utils/http/axios'; @@ -4,12 +4,23 @@ import { defHttp } from '/@/utils/http/axios';
4 import { RuleChainDetail, 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 + SAVE_RULE_CHAINS = '/ruleChain',
7 GET_RULE_CHAINS_DETAIL = '/ruleChain', 8 GET_RULE_CHAINS_DETAIL = '/ruleChain',
8 SAVE = '/ruleChain/metadata', 9 SAVE = '/ruleChain/metadata',
9 GET_RULE_CHAINES = '/ruleChains', 10 GET_RULE_CHAINES = '/ruleChains',
10 GET_RULE_NODE_EVENTS = '/events/RULE_NODE', 11 GET_RULE_NODE_EVENTS = '/events/RULE_NODE',
11 } 12 }
12 13
  14 +export const saveRuleChainDetail = (params: Partial<RuleChainDetail>) => {
  15 + return defHttp.post<RuleChainDetail>(
  16 + {
  17 + url: Api.SAVE_RULE_CHAINS,
  18 + data: params,
  19 + },
  20 + { joinPrefix: false }
  21 + );
  22 +};
  23 +
13 export const getRuleChainDetail = (id: string) => { 24 export const getRuleChainDetail = (id: string) => {
14 return defHttp.get<RuleChainDetail>( 25 return defHttp.get<RuleChainDetail>(
15 { 26 {
@@ -41,8 +41,12 @@ export const MENU_LIST = 'MENU_LIST'; @@ -41,8 +41,12 @@ export const MENU_LIST = 'MENU_LIST';
41 41
42 export const RULE_NODE_LOCAL_CACHE_KEY = 'RULE__NODE__KEY__'; 42 export const RULE_NODE_LOCAL_CACHE_KEY = 'RULE__NODE__KEY__';
43 43
  44 +export const RULE_CHAIN_IMPORT_LOCAL_CACHE_KEY = 'RULE__CHAIN__IMPORT__KEY__';
  45 +
44 export const RULE_NODE_KEY = 'RULE_NODE'; 46 export const RULE_NODE_KEY = 'RULE_NODE';
45 47
  48 +export const RULE_CHAIN_KEY = 'RULE_CHAIN';
  49 +
46 export enum CacheTypeEnum { 50 export enum CacheTypeEnum {
47 SESSION, 51 SESSION,
48 LOCAL, 52 LOCAL,
@@ -18,4 +18,6 @@ export const PageEnum = { @@ -18,4 +18,6 @@ export const PageEnum = {
18 SHARE_PAGE: '/share/:viewType/:id/:publicId', 18 SHARE_PAGE: '/share/:viewType/:id/:publicId',
19 19
20 RULE_CHAIN_DETAIL: '/rule/chain/:id', 20 RULE_CHAIN_DETAIL: '/rule/chain/:id',
  21 +
  22 + RULE_CHAIN_DETAIL_IMPORT: '/rule/chain/import',
21 }; 23 };
1 -import type { Connection } from '@vue-flow/core'; 1 +import type { Connection, Edge } from '@vue-flow/core';
2 import { EdgeTypeEnum } from '../enum'; 2 import { EdgeTypeEnum } from '../enum';
3 import type { EdgeData } from '../types/node'; 3 import type { EdgeData } from '../types/node';
4 import { buildUUID } from '/@/utils/uuid'; 4 import { buildUUID } from '/@/utils/uuid';
5 5
6 export function useAddEdges() { 6 export function useAddEdges() {
7 - const getAddedgesParams = (params: Connection, data: string | string[] | any) => { 7 + const getAddedgesParams = (params: Connection | Edge, data: string | string[] | any) => {
8 return { type: EdgeTypeEnum.CUSTOM, data: { data } as EdgeData, id: buildUUID(), ...params }; 8 return { type: EdgeTypeEnum.CUSTOM, data: { data } as EdgeData, id: buildUUID(), ...params };
9 }; 9 };
10 10
@@ -8,6 +8,7 @@ import { buildUUID } from '/@/utils/uuid'; @@ -8,6 +8,7 @@ import { buildUUID } from '/@/utils/uuid';
8 import { isNullOrUnDef } from '/@/utils/is'; 8 import { isNullOrUnDef } from '/@/utils/is';
9 import { useAddNodes } from './useAddNodes'; 9 import { useAddNodes } from './useAddNodes';
10 import { useAddEdges } from './useAddEdges'; 10 import { useAddEdges } from './useAddEdges';
  11 +import { RuleChainEntityType } from '../enum/entity';
11 12
12 export function useBasicDataTransform() { 13 export function useBasicDataTransform() {
13 const nodeConfigMap = new Map<string, NodeData>(); 14 const nodeConfigMap = new Map<string, NodeData>();
@@ -67,8 +68,8 @@ export function useBasicDataTransform() { @@ -67,8 +68,8 @@ export function useBasicDataTransform() {
67 68
68 const sourceNode = indexMap.get(Number(fromIndex)); 69 const sourceNode = indexMap.get(Number(fromIndex));
69 const targetNode = indexMap.get(Number(toIndex)); 70 const targetNode = indexMap.get(Number(toIndex));
70 - const source = sourceNode!.id!.id;  
71 - const target = targetNode!.id!.id; 71 + const source = sourceNode?.id?.id || buildUUID();
  72 + const target = targetNode?.id?.id || buildUUID();
72 const sourceHandle = `${source}${SOURCE_HANDLE}`; 73 const sourceHandle = `${source}${SOURCE_HANDLE}`;
73 const targetHandle = `${target}${TARGET_HANDLE}`; 74 const targetHandle = `${target}${TARGET_HANDLE}`;
74 75
@@ -88,7 +89,7 @@ export function useBasicDataTransform() { @@ -88,7 +89,7 @@ export function useBasicDataTransform() {
88 if (!isNullOrUnDef(firstNodeIndex)) { 89 if (!isNullOrUnDef(firstNodeIndex)) {
89 const targetNode = indexMap.get(firstNodeIndex); 90 const targetNode = indexMap.get(firstNodeIndex);
90 const source = inputNodeId; 91 const source = inputNodeId;
91 - const target = targetNode!.id!.id; 92 + const target = targetNode?.id?.id || buildUUID();
92 const sourceHandle = `${source}$${SOURCE_HANDLE}`; 93 const sourceHandle = `${source}$${SOURCE_HANDLE}`;
93 const targetHandle = `${target}${TARGET_HANDLE}`; 94 const targetHandle = `${target}${TARGET_HANDLE}`;
94 edges.push( 95 edges.push(
@@ -112,7 +113,7 @@ export function useBasicDataTransform() { @@ -112,7 +113,7 @@ export function useBasicDataTransform() {
112 const { layoutX, layoutY, description } = additionalInfo || {}; 113 const { layoutX, layoutY, description } = additionalInfo || {};
113 const { getAddNodesParams } = useAddNodes(); 114 const { getAddNodesParams } = useAddNodes();
114 115
115 - return getAddNodesParams( 116 + const value = getAddNodesParams(
116 { x: layoutX!, y: layoutY! }, 117 { x: layoutX!, y: layoutY! },
117 { 118 {
118 ...config, 119 ...config,
@@ -128,6 +129,10 @@ export function useBasicDataTransform() { @@ -128,6 +129,10 @@ export function useBasicDataTransform() {
128 id: id?.id || buildUUID(), 129 id: id?.id || buildUUID(),
129 } 130 }
130 ); 131 );
  132 +
  133 + if (!id?.id) node.id = { id: value.id, entityType: RuleChainEntityType.RULE_NODE };
  134 +
  135 + return value;
131 } 136 }
132 137
133 function deconstructionNode(nodes: RuleChainType['nodes']) { 138 function deconstructionNode(nodes: RuleChainType['nodes']) {
1 -import { GraphEdge, GraphNode, VueFlowStore } from '@vue-flow/core';  
2 -import { getRuleNodeCache, setRuleNodeCache } from './useRuleCopyPaste'; 1 +import { GraphEdge, GraphNode, VueFlowStore, pointToRendererPoint } from '@vue-flow/core';
  2 +import { getRuleNodeCache, setRuleNodeCache } from './useRuleChainCache';
3 import { RuleContextMenuEnum } from './useRuleChainContextMenu'; 3 import { RuleContextMenuEnum } from './useRuleChainContextMenu';
4 import { useAddNodes } from './useAddNodes'; 4 import { useAddNodes } from './useAddNodes';
5 import { buildUUID } from '/@/utils/uuid'; 5 import { buildUUID } from '/@/utils/uuid';
@@ -65,15 +65,15 @@ export function useContextMenuAction() { @@ -65,15 +65,15 @@ export function useContextMenuAction() {
65 const copy = (params: HandleContextMenuActionParamsType) => { 65 const copy = (params: HandleContextMenuActionParamsType) => {
66 const { node } = params; 66 const { node } = params;
67 if (!node) return; 67 if (!node) return;
68 - const { position, data } = node; 68 + const { position, data, id } = node;
69 const { getAddNodesParams } = useAddNodes(); 69 const { getAddNodesParams } = useAddNodes();
70 const { x, y } = position; 70 const { x, y } = position;
71 71
72 - const value = getAddNodesParams(position, data, { id: buildUUID() }); 72 + const value = getAddNodesParams(position, data, { id });
73 73
74 setRuleNodeCache({ 74 setRuleNodeCache({
75 nodes: [value], 75 nodes: [value],
76 - originX: x + NODE_WIDTH / 2 + x, 76 + originX: x + NODE_WIDTH / 2,
77 originY: y + NODE_HEIGHT / 2, 77 originY: y + NODE_HEIGHT / 2,
78 }); 78 });
79 }; 79 };
@@ -81,24 +81,49 @@ export function useContextMenuAction() { @@ -81,24 +81,49 @@ export function useContextMenuAction() {
81 const paste = (params: HandleContextMenuActionParamsType) => { 81 const paste = (params: HandleContextMenuActionParamsType) => {
82 const { event, flowActionType, useSaveAndRedoActionType } = params; 82 const { event, flowActionType, useSaveAndRedoActionType } = params;
83 const { triggerChange } = useSaveAndRedoActionType || {}; 83 const { triggerChange } = useSaveAndRedoActionType || {};
  84 +
84 const { getAddNodesParams } = useAddNodes(); 85 const { getAddNodesParams } = useAddNodes();
85 const { getAddedgesParams } = useAddEdges(); 86 const { getAddedgesParams } = useAddEdges();
86 - const clientX = (event as MouseEvent).offsetX;  
87 - const clientY = (event as MouseEvent).offsetY;  
88 87
89 const { edges = [], nodes = [], originX, originY } = getRuleNodeCache(); 88 const { edges = [], nodes = [], originX, originY } = getRuleNodeCache();
90 89
  90 + const { x: clientX, y: clientY } = pointToRendererPoint(
  91 + { x: (event as MouseEvent).offsetX, y: (event as MouseEvent).offsetY },
  92 + flowActionType!.getViewport(),
  93 + false,
  94 + [25, 25]
  95 + );
  96 +
  97 + const xAxisMoveDistance = clientX - originX!;
  98 + const yAxisMoveDistance = clientY - originY!;
  99 +
  100 + const newEdges = edges.map((edge) =>
  101 + getAddedgesParams({ ...edge, id: buildUUID() }, edge.data)
  102 + );
  103 +
91 const newNode = nodes.map((node) => { 104 const newNode = nodes.map((node) => {
92 - const { position, data, id } = node; 105 + const { position, data, id: oldId } = node;
93 const { x, y } = position; 106 const { x, y } = position;
  107 + const newId = buildUUID();
94 108
95 - const newX = clientX - originX! + x + NODE_WIDTH / 2;  
96 - const newY = clientY - originY! + y + NODE_HEIGHT / 2; 109 + const newX = xAxisMoveDistance + x;
  110 + const newY = yAxisMoveDistance + y;
97 111
98 - return getAddNodesParams({ x: newX, y: newY }, { ...data, created: false }, { id });  
99 - }); 112 + for (const connection of newEdges || []) {
  113 + if (connection.source.includes(oldId)) {
  114 + connection.source = newId;
  115 + connection.sourceHandle = connection.sourceHandle?.replaceAll(oldId, newId);
  116 + continue;
  117 + }
100 118
101 - const newEdges = edges.map((edge) => getAddedgesParams(edge, edge.data)); 119 + if (connection.target.includes(oldId)) {
  120 + connection.target = newId;
  121 + connection.targetHandle = connection.targetHandle?.replaceAll(oldId, newId);
  122 + }
  123 + }
  124 +
  125 + return getAddNodesParams({ x: newX, y: newY }, { ...data, created: false }, { id: newId });
  126 + });
102 127
103 flowActionType?.addNodes(newNode); 128 flowActionType?.addNodes(newNode);
104 flowActionType?.addEdges(newEdges); 129 flowActionType?.addEdges(newEdges);
@@ -153,23 +178,9 @@ export function useContextMenuAction() { @@ -153,23 +178,9 @@ export function useContextMenuAction() {
153 ); 178 );
154 179
155 const nodes = unref(flowActionType?.getSelectedNodes)?.map((node) => { 180 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 - } 181 + const { id } = node;
171 182
172 - return getAddNodesParams(node.position, toRaw(unref(node.data)), { id: newId }); 183 + return getAddNodesParams(node.position, toRaw(unref(node.data)), { id });
173 }); 184 });
174 185
175 const originRect = getElementsCenter(unref(flowActionType?.getSelectedNodes) || []); 186 const originRect = getElementsCenter(unref(flowActionType?.getSelectedNodes) || []);
@@ -6,7 +6,7 @@ export function useInputNode() { @@ -6,7 +6,7 @@ export function useInputNode() {
6 const { getAddNodesParams } = useAddNodes(); 6 const { getAddNodesParams } = useAddNodes();
7 7
8 const newNode = getAddNodesParams( 8 const newNode = getAddNodesParams(
9 - { x: 80, y: 50 }, 9 + { x: 75, y: 50 },
10 { 10 {
11 ...new InputConfig(), 11 ...new InputConfig(),
12 data: { 12 data: {
src/views/rule/designer/hook/useRuleChainCache.ts renamed from src/views/rule/designer/hook/useRuleCopyPaste.ts
1 import { Edge, Node } from '@vue-flow/core'; 1 import { Edge, Node } from '@vue-flow/core';
2 -import { RULE_NODE_KEY, RULE_NODE_LOCAL_CACHE_KEY } from '/@/enums/cacheEnum'; 2 +import {
  3 + RULE_NODE_KEY,
  4 + RULE_CHAIN_KEY,
  5 + RULE_NODE_LOCAL_CACHE_KEY,
  6 + RULE_CHAIN_IMPORT_LOCAL_CACHE_KEY,
  7 +} from '/@/enums/cacheEnum';
3 import { createLocalStorage } from '/@/utils/cache'; 8 import { createLocalStorage } from '/@/utils/cache';
  9 +import { RuleChainDetail, RuleChainType } from '../types/ruleNode';
4 10
5 const ruleNodeStorage = createLocalStorage({ prefixKey: RULE_NODE_LOCAL_CACHE_KEY }); 11 const ruleNodeStorage = createLocalStorage({ prefixKey: RULE_NODE_LOCAL_CACHE_KEY });
  12 +const ruleChainStorage = createLocalStorage({ prefixKey: RULE_CHAIN_IMPORT_LOCAL_CACHE_KEY });
6 13
7 interface RuleNodeCacheType { 14 interface RuleNodeCacheType {
8 nodes?: Node[]; 15 nodes?: Node[];
@@ -11,6 +18,11 @@ interface RuleNodeCacheType { @@ -11,6 +18,11 @@ interface RuleNodeCacheType {
11 originY?: number; 18 originY?: number;
12 } 19 }
13 20
  21 +export interface RuleChainCacheType {
  22 + ruleChain: Partial<RuleChainDetail>;
  23 + metadata: RuleChainType;
  24 +}
  25 +
14 export const setRuleNodeCache = ({ 26 export const setRuleNodeCache = ({
15 nodes = [], 27 nodes = [],
16 edges = [], 28 edges = [],
@@ -29,6 +41,14 @@ export const getRuleNodeCache = (): RuleNodeCacheType => ruleNodeStorage.get(RUL @@ -29,6 +41,14 @@ export const getRuleNodeCache = (): RuleNodeCacheType => ruleNodeStorage.get(RUL
29 41
30 export const checkHasCacheRuleNode = () => !!getRuleNodeCache(); 42 export const checkHasCacheRuleNode = () => !!getRuleNodeCache();
31 43
  44 +export const getRuleChainImportCache = (): RuleChainCacheType =>
  45 + ruleChainStorage.get(RULE_CHAIN_KEY) || {};
  46 +
  47 +export const setRuleChainImportCache = (value: RuleChainCacheType) =>
  48 + ruleChainStorage.set(RULE_CHAIN_KEY, value);
  49 +
  50 +export const clearRuleChainImportCache = () => ruleChainStorage.remove(RULE_CHAIN_KEY);
  51 +
32 function initRuleNodeStorage() { 52 function initRuleNodeStorage() {
33 const value = ruleNodeStorage.get(RULE_NODE_KEY); 53 const value = ruleNodeStorage.get(RULE_NODE_KEY);
34 value && ruleNodeStorage.set(RULE_NODE_KEY, value); 54 value && ruleNodeStorage.set(RULE_NODE_KEY, value);
@@ -2,7 +2,7 @@ import { NodeMouseEvent } from '@vue-flow/core'; @@ -2,7 +2,7 @@ import { NodeMouseEvent } from '@vue-flow/core';
2 import { useContextMenu } from '../src/components/RuleChainContextMenu'; 2 import { useContextMenu } from '../src/components/RuleChainContextMenu';
3 import { RuleChainContextMenuItemType } from '../src/components/RuleChainContextMenu/index.type'; 3 import { RuleChainContextMenuItemType } from '../src/components/RuleChainContextMenu/index.type';
4 import { ElementsTypeEnum } from '../enum'; 4 import { ElementsTypeEnum } from '../enum';
5 -import { checkHasCacheRuleNode } from './useRuleCopyPaste'; 5 +import { checkHasCacheRuleNode } from './useRuleChainCache';
6 6
7 export enum RuleContextMenuEnum { 7 export enum RuleContextMenuEnum {
8 DETAIL = 'DETAIL', 8 DETAIL = 'DETAIL',
@@ -17,6 +17,8 @@ export enum RuleContextMenuEnum { @@ -17,6 +17,8 @@ export enum RuleContextMenuEnum {
17 UNDO_CHANGE = 'UNDO_CHANGE', 17 UNDO_CHANGE = 'UNDO_CHANGE',
18 18
19 SELECT_ALL = 'SELECT_ALL', 19 SELECT_ALL = 'SELECT_ALL',
  20 +
  21 + CREATE_RULE_CHAIN = 'CREATE_RULE_CHAIN',
20 } 22 }
21 23
22 export enum RuleContextMenuNameEnum { 24 export enum RuleContextMenuNameEnum {
@@ -32,6 +34,8 @@ export enum RuleContextMenuNameEnum { @@ -32,6 +34,8 @@ export enum RuleContextMenuNameEnum {
32 UNDO_CHANGE = '撤销更改', 34 UNDO_CHANGE = '撤销更改',
33 35
34 SELECT_ALL = '选择全部', 36 SELECT_ALL = '选择全部',
  37 +
  38 + CREATE_RULE_CHAIN = '创建规则链',
35 } 39 }
36 40
37 export enum RuleChainContextMenuIconEnum { 41 export enum RuleChainContextMenuIconEnum {
@@ -48,21 +52,22 @@ export enum RuleChainContextMenuIconEnum { @@ -48,21 +52,22 @@ export enum RuleChainContextMenuIconEnum {
48 52
49 SELECT_ALL = 'material-symbols:select-all', 53 SELECT_ALL = 'material-symbols:select-all',
50 54
  55 + CREATE_RULE_CHAIN = 'material-symbols:settings-ethernet',
  56 +
51 // LINK = 'material-symbols:trending-flat', 57 // LINK = 'material-symbols:trending-flat',
52 } 58 }
53 59
54 -export enum RuleChainContextMenuShortcutKeyEnum {  
55 - DELETE = 'Ctrl(⌘) X', 60 +export enum RuleChainContextMenuShortcutKeyEnum {}
  61 +// DELETE = 'Ctrl(⌘) X',
56 62
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 +// SELECT_COPY = 'Ctrl(⌘) C',
  64 +// PASTE = 'Ctrl(⌘) V',
  65 +// UNSELECTED = 'Esc',
  66 +// DELETE_SELECT = 'Del',
  67 +// APPLY_CHANGE = 'Ctrl(⌘) S',
  68 +// UNDO_CHANGE = 'Ctrl(⌘) Z',
63 69
64 - SELECT_ALL = 'Ctrl(⌘) A',  
65 -} 70 +// SELECT_ALL = 'Ctrl(⌘) A',
66 71
67 const getMenuItem = (key: RuleContextMenuEnum, handler: Fn, disabled = false) => { 72 const getMenuItem = (key: RuleContextMenuEnum, handler: Fn, disabled = false) => {
68 return { 73 return {
@@ -96,6 +101,7 @@ export function useCreateRuleChainContextMenu() { @@ -96,6 +101,7 @@ export function useCreateRuleChainContextMenu() {
96 const createElementsSelectedContextMenu = ( 101 const createElementsSelectedContextMenu = (
97 params: NodeMouseEvent, 102 params: NodeMouseEvent,
98 changeMarker: boolean, 103 changeMarker: boolean,
  104 + hasCreateChainMenuItem = false,
99 elementsType: ElementsTypeEnum.NODE = ElementsTypeEnum.NODE 105 elementsType: ElementsTypeEnum.NODE = ElementsTypeEnum.NODE
100 ): Promise<RuleContextMenuEnum | ''> => { 106 ): Promise<RuleContextMenuEnum | ''> => {
101 return new Promise(async (resolve) => { 107 return new Promise(async (resolve) => {
@@ -107,6 +113,9 @@ export function useCreateRuleChainContextMenu() { @@ -107,6 +113,9 @@ export function useCreateRuleChainContextMenu() {
107 getMenuItem(RuleContextMenuEnum.PASTE, resolve, !checkHasCacheRuleNode()), 113 getMenuItem(RuleContextMenuEnum.PASTE, resolve, !checkHasCacheRuleNode()),
108 getDivider(), 114 getDivider(),
109 getMenuItem(RuleContextMenuEnum.UNSELECTED, resolve), 115 getMenuItem(RuleContextMenuEnum.UNSELECTED, resolve),
  116 + ...(hasCreateChainMenuItem
  117 + ? [getMenuItem(RuleContextMenuEnum.CREATE_RULE_CHAIN, resolve)]
  118 + : []),
110 getMenuItem(RuleContextMenuEnum.DELETE_SELECT, resolve), 119 getMenuItem(RuleContextMenuEnum.DELETE_SELECT, resolve),
111 getDivider(), 120 getDivider(),
112 getMenuItem(RuleContextMenuEnum.APPLY_CHANGE, resolve, !changeMarker), 121 getMenuItem(RuleContextMenuEnum.APPLY_CHANGE, resolve, !changeMarker),
@@ -92,6 +92,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -92,6 +92,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
92 92
93 if (!validateList.every((item) => item(connection, elements))) return false; 93 if (!validateList.every((item) => item(connection, elements))) return false;
94 94
  95 + if (validateCircularreference(elements.sourceNode!, elements.targetNode, elements.edges))
  96 + return false;
  97 +
95 return true; 98 return true;
96 }, 99 },
97 }); 100 });
@@ -134,7 +137,6 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -134,7 +137,6 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
134 137
135 triggerChange(); 138 triggerChange();
136 }); 139 });
137 -  
138 onPaneReady(async () => { 140 onPaneReady(async () => {
139 setViewport({ x: 0, y: 0, zoom: 1 }); 141 setViewport({ x: 0, y: 0, zoom: 1 });
140 }); 142 });
@@ -164,7 +166,8 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -164,7 +166,8 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
164 const menuType = params.node.selected 166 const menuType = params.node.selected
165 ? await createElementsSelectedContextMenu( 167 ? await createElementsSelectedContextMenu(
166 params, 168 params,
167 - unref(useSaveAndRedoActionType.changeMarker) 169 + unref(useSaveAndRedoActionType.changeMarker),
  170 + validateSelectionElementsCanCreateRuleChain()
168 ) 171 )
169 : await createNodeContextMenu(params); 172 : await createNodeContextMenu(params);
170 173
@@ -212,7 +215,8 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -212,7 +215,8 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
212 const menuType = unref(flowActionType.getSelectedElements).length 215 const menuType = unref(flowActionType.getSelectedElements).length
213 ? await createElementsSelectedContextMenu( 216 ? await createElementsSelectedContextMenu(
214 getCreatePanelContextMenuParams(params), 217 getCreatePanelContextMenuParams(params),
215 - unref(useSaveAndRedoActionType.changeMarker) 218 + unref(useSaveAndRedoActionType.changeMarker),
  219 + validateSelectionElementsCanCreateRuleChain()
216 ) 220 )
217 : await createPanelContextMenu( 221 : await createPanelContextMenu(
218 getCreatePanelContextMenuParams(params), 222 getCreatePanelContextMenuParams(params),
@@ -238,6 +242,55 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -238,6 +242,55 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
238 return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length; 242 return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length;
239 } 243 }
240 244
  245 + /**
  246 + * @description 验证是否循环引用
  247 + */
  248 + function validateCircularreference(
  249 + sourceNode: GraphNode,
  250 + targetNode: GraphNode,
  251 + edges: GraphEdge[]
  252 + ) {
  253 + const sourceId = sourceNode.id;
  254 + const targetId = targetNode.id;
  255 +
  256 + const getAllIndex = (
  257 + edges: GraphEdge[],
  258 + validate: (item: GraphEdge, index: number) => boolean
  259 + ) => {
  260 + const indexList: number[] = [];
  261 +
  262 + for (let i = 0; i < edges.length; i++) {
  263 + const item = edges[i];
  264 + if (validate(item, i)) {
  265 + indexList.push(i);
  266 + }
  267 + }
  268 +
  269 + return indexList;
  270 + };
  271 +
  272 + const validate = (source: string, startSource: string) => {
  273 + const nextNodesIndex = getAllIndex(edges, (item) => item.source === source);
  274 +
  275 + if (!nextNodesIndex.length) return false;
  276 +
  277 + for (const nextNodeIndex of nextNodesIndex) {
  278 + const nextNode = edges[nextNodeIndex];
  279 + if (nextNode && nextNode.target === startSource) {
  280 + return true;
  281 + }
  282 + return validate(nextNode.target, startSource);
  283 + }
  284 + };
  285 +
  286 + return validate(targetId, sourceId);
  287 + }
  288 +
  289 + /**
  290 + * @description 处理最大链接点
  291 + * @param sourceNode
  292 + * @returns
  293 + */
241 function handleMaxConnectionPoint(sourceNode?: GraphNode) { 294 function handleMaxConnectionPoint(sourceNode?: GraphNode) {
242 if (!sourceNode) return; 295 if (!sourceNode) return;
243 296
@@ -329,5 +382,17 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -329,5 +382,17 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
329 } as NodeMouseEvent; 382 } as NodeMouseEvent;
330 } 383 }
331 384
  385 + function validateSelectionElementsCanCreateRuleChain() {
  386 + const nodes = unref(flowActionType.getSelectedNodes);
  387 + const edges = unref(flowActionType.getSelectedEdges);
  388 +
  389 + for (const node of nodes) {
  390 + const index = edges.findIndex((edge) => edge.target === node.id || edge.source === node.id);
  391 + if (!~index) return false;
  392 + }
  393 +
  394 + return true;
  395 + }
  396 +
332 return { flowActionType }; 397 return { flowActionType };
333 } 398 }
@@ -3,12 +3,19 @@ import { ComputedRef, computed, ref, unref } from 'vue'; @@ -3,12 +3,19 @@ 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, getRuleChainDetail, saveRuleChainData } from '/@/api/ruleDesigner'; 6 +import {
  7 + getRuleChainData,
  8 + getRuleChainDetail,
  9 + saveRuleChainData,
  10 + saveRuleChainDetail,
  11 +} from '/@/api/ruleDesigner';
7 import { ConnectionItemType, RuleChainDetail, RuleChainType } from '../types/ruleNode'; 12 import { ConnectionItemType, RuleChainDetail, RuleChainType } from '../types/ruleNode';
8 import { useInputNode } from './useInputNode'; 13 import { useInputNode } from './useInputNode';
9 import { buildUUID } from '/@/utils/uuid'; 14 import { buildUUID } from '/@/utils/uuid';
10 -import { useRoute } from 'vue-router'; 15 +import { useRoute, useRouter } from 'vue-router';
11 import { RuleChainEntityType } from '../enum/entity'; 16 import { RuleChainEntityType } from '../enum/entity';
  17 +import { PageEnum } from '/@/enums/pageEnum';
  18 +import { clearRuleChainImportCache, getRuleChainImportCache } from './useRuleChainCache';
12 19
13 const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT]; 20 const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT];
14 21
@@ -19,11 +26,15 @@ export function useSaveAndRedo() { @@ -19,11 +26,15 @@ export function useSaveAndRedo() {
19 26
20 const redoDataRef = ref<Elements>([]); 27 const redoDataRef = ref<Elements>([]);
21 28
22 - const route = useRoute(); 29 + const ROUTE = useRoute();
  30 +
  31 + const ROUTER = useRouter();
23 32
24 const debugMarker = ref(false); 33 const debugMarker = ref(false);
25 34
26 - const getRuleChainId = computed(() => (route.params as Record<'id', string>).id); 35 + const getRuleChainId = computed(() => (ROUTE.params as Record<'id', string>).id);
  36 +
  37 + const getIsImportFlag = computed(() => ROUTE.fullPath === PageEnum.RULE_CHAIN_DETAIL_IMPORT);
27 38
28 const ruleChainDetail = ref<RuleChainDetail>(); 39 const ruleChainDetail = ref<RuleChainDetail>();
29 40
@@ -131,7 +142,9 @@ export function useSaveAndRedo() { @@ -131,7 +142,9 @@ export function useSaveAndRedo() {
131 }; 142 };
132 143
133 async function getCurrentRuleChainDetail() { 144 async function getCurrentRuleChainDetail() {
134 - ruleChainDetail.value = await getRuleChainDetail(unref(getRuleChainId)); 145 + ruleChainDetail.value = unref(getIsImportFlag)
  146 + ? (getImportChainDetail() as RuleChainDetail)
  147 + : await getRuleChainDetail(unref(getRuleChainId));
135 } 148 }
136 149
137 async function handleSaveRuleChain( 150 async function handleSaveRuleChain(
@@ -142,19 +155,35 @@ export function useSaveAndRedo() { @@ -142,19 +155,35 @@ export function useSaveAndRedo() {
142 try { 155 try {
143 loading.value = true; 156 loading.value = true;
144 157
  158 + let ruleChainId = unref(getRuleChainId);
  159 + if (unref(getIsImportFlag)) {
  160 + const detail = await saveRuleChainDetail(unref(ruleChainDetail)!);
  161 + ruleChainDetail.value = detail;
  162 + ruleChainId = detail.id.id;
  163 + }
  164 +
145 const data = await saveRuleChainData({ 165 const data = await saveRuleChainData({
146 connections, 166 connections,
147 nodes, 167 nodes,
148 firstNodeIndex, 168 firstNodeIndex,
149 ruleChainId: { 169 ruleChainId: {
150 entityType: RuleChainEntityType.RULE_CHAIN, 170 entityType: RuleChainEntityType.RULE_CHAIN,
151 - id: unref(getRuleChainId), 171 + id: ruleChainId,
152 }, 172 },
153 }); 173 });
154 174
155 parseRuleChain(data); 175 parseRuleChain(data);
156 176
157 resetChange(); 177 resetChange();
  178 +
  179 + if (unref(getIsImportFlag)) {
  180 + clearRuleChainImportCache();
  181 +
  182 + ROUTER.replace({
  183 + path: `/rule/chain/${ruleChainId}`,
  184 + replace: true,
  185 + });
  186 + }
158 } finally { 187 } finally {
159 loading.value = false; 188 loading.value = false;
160 } 189 }
@@ -164,13 +193,16 @@ export function useSaveAndRedo() { @@ -164,13 +193,16 @@ export function useSaveAndRedo() {
164 try { 193 try {
165 loading.value = true; 194 loading.value = true;
166 195
167 - const data = await getRuleChainData(unref(getRuleChainId)); 196 + const data = unref(getIsImportFlag)
  197 + ? await getImportMetadata()
  198 + : await getRuleChainData(unref(getRuleChainId));
  199 + if (!data) return;
168 200
169 const elements = parseRuleChain(data); 201 const elements = parseRuleChain(data);
170 202
171 flowActionType.setElements(elements); 203 flowActionType.setElements(elements);
172 204
173 - resetChange(); 205 + unref(getIsImportFlag) ? triggerChange() : resetChange();
174 } finally { 206 } finally {
175 loading.value = false; 207 loading.value = false;
176 } 208 }
@@ -201,6 +233,12 @@ export function useSaveAndRedo() { @@ -201,6 +233,12 @@ export function useSaveAndRedo() {
201 triggerChange(); 233 triggerChange();
202 }; 234 };
203 235
  236 + const getImportMetadata = () => {
  237 + return getRuleChainImportCache().metadata;
  238 + };
  239 +
  240 + const getImportChainDetail = () => getRuleChainImportCache().ruleChain;
  241 +
204 return { 242 return {
205 loading, 243 loading,
206 debugMarker, 244 debugMarker,
@@ -77,13 +77,33 @@ @@ -77,13 +77,33 @@
77 return { msg, msgType, metadata: toRaw(unref(metadata)), javascriptFunction }; 77 return { msg, msgType, metadata: toRaw(unref(metadata)), javascriptFunction };
78 }; 78 };
79 79
  80 + function executeTestScript() {
  81 + try {
  82 + const { javaScriptEditorProps } = props;
  83 + const { paramsName } = javaScriptEditorProps;
  84 + const fn = new Function(...(paramsName || [])!, unref(scriptContent));
  85 + const value = getValue();
  86 + const executeParams = paramsName!.map((key) => value[key]);
  87 +
  88 + return fn(...executeParams);
  89 + } catch (error) {
  90 + return error;
  91 + }
  92 + }
  93 +
80 const handleTestScript = async () => { 94 const handleTestScript = async () => {
81 const flag = await handleValidate(); 95 const flag = await handleValidate();
  96 +
82 flag && emit('test', getValue()); 97 flag && emit('test', getValue());
  98 +
  99 + const result = executeTestScript();
  100 +
  101 + outputContent.value = result;
83 }; 102 };
84 103
85 const handleSave = async () => { 104 const handleSave = async () => {
86 const flag = await handleValidate(); 105 const flag = await handleValidate();
  106 +
87 flag && emit('save', getValue()); 107 flag && emit('save', getValue());
88 }; 108 };
89 109
1 <script lang="ts" setup> 1 <script lang="ts" setup>
  2 + import { Badge } from 'ant-design-vue';
2 import { cloneDeep } from 'lodash'; 3 import { cloneDeep } from 'lodash';
3 import { computed, ref, unref } from 'vue'; 4 import { computed, ref, unref } from 'vue';
4 import { allComponents } from '../../../packages'; 5 import { allComponents } from '../../../packages';
@@ -27,13 +28,21 @@ @@ -27,13 +28,21 @@
27 <template> 28 <template>
28 <section class="absolute top-11 w-full flex flex-auto h-[calc(100%-2.75rem)] bg-neutral-100"> 29 <section class="absolute top-11 w-full flex flex-auto h-[calc(100%-2.75rem)] bg-neutral-100">
29 <nav class="w-20 min-w-20 p-2 border-t border-light-50"> 30 <nav class="w-20 min-w-20 p-2 border-t border-light-50">
30 - <CategoryItem 31 + <Badge
31 v-for="category in allComponents" 32 v-for="category in allComponents"
32 :key="category.category.category" 33 :key="category.category.category"
33 - v-model:activeKey="activeKey"  
34 - :category="category"  
35 - /> 34 + :dot="!!searchText &&
  35 + category.components.some((item) =>
  36 + item.name.toUpperCase().includes(searchText!.toUpperCase())
  37 + )
  38 + "
  39 + :offset="[-10, 10]"
  40 + class="!block"
  41 + >
  42 + <CategoryItem v-model:activeKey="activeKey" :category="category" />
  43 + </Badge>
36 </nav> 44 </nav>
  45 +
37 <body class="p-4 w-full flex flex-col gap-2 items-center overflow-x-hidden overflow-y-auto"> 46 <body class="p-4 w-full flex flex-col gap-2 items-center overflow-x-hidden overflow-y-auto">
38 <NodeItem 47 <NodeItem
39 v-for="config in getCurrentCategoryNode.components" 48 v-for="config in getCurrentCategoryNode.components"
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 <template> 26 <template>
27 <BasicDrawer 27 <BasicDrawer
28 v-model:visible="visible" 28 v-model:visible="visible"
29 - width="700px" 29 + width="40%"
30 showFooter 30 showFooter
31 showOkBtn 31 showOkBtn
32 showCancelBtn 32 showCancelBtn
@@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
79 <template> 79 <template>
80 <BasicDrawer 80 <BasicDrawer
81 v-model:visible="visible" 81 v-model:visible="visible"
82 - width="700px" 82 + width="40%"
83 showFooter 83 showFooter
84 showCancelBtn 84 showCancelBtn
85 showOkBtn 85 showOkBtn