Commit 5e6e9897728f85da0c96bfe5b6231cffd906f0c5

Authored by ww
1 parent a2bfd451

feat: 新增规则链设计器

Showing 69 changed files with 2878 additions and 18 deletions
... ... @@ -5,14 +5,14 @@
5 5 "public/resource/tinymce/langs"
6 6 ],
7 7 "cSpell.words": [
  8 + "clazz",
8 9 "Cmds",
9 10 "COAP",
10 11 "echarts",
11 12 "edrx",
12   - "EFENTO",
  13 + "EFENTO",
13 14 "fingerprintjs",
14 15 "flvjs",
15   - "flvjs",
16 16 "inited",
17 17 "liveui",
18 18 "MQTT",
... ... @@ -21,6 +21,7 @@
21 21 "rtsp",
22 22 "SCADA",
23 23 "SNMP",
  24 + "UNACK",
24 25 "unref",
25 26 "vben",
26 27 "videojs",
... ...
... ... @@ -39,6 +39,10 @@
39 39 "@iconify/iconify": "^2.0.3",
40 40 "@logicflow/core": "^0.6.9",
41 41 "@logicflow/extension": "^0.6.9",
  42 + "@vue-flow/background": "^1.2.0",
  43 + "@vue-flow/controls": "^1.1.0",
  44 + "@vue-flow/core": "^1.22.1",
  45 + "@vue-flow/node-toolbar": "^1.1.0",
42 46 "@vueuse/core": "^10.1.0",
43 47 "@zxcvbn-ts/core": "^1.0.0-beta.0",
44 48 "ace-builds": "^1.4.14",
... ... @@ -68,7 +72,7 @@
68 72 "vditor": "^3.8.6",
69 73 "video.js": "^7.20.3",
70 74 "videojs-flvjs-es6": "^1.0.1",
71   - "vue": "3.2.31",
  75 + "vue": "3.3.4",
72 76 "vue-i18n": "9.1.7",
73 77 "vue-json-pretty": "^2.0.4",
74 78 "vue-router": "^4.0.11",
... ...
  1 +import { defHttp } from '/@/utils/http/axios';
  2 +import { RuleChainType } from '/@/views/rule/designer/types/ruleNode';
  3 +
  4 +enum Api {
  5 + SAVE = '/ruleChain/metadata',
  6 +}
  7 +
  8 +export const getRuleChainData = (id: string) => {
  9 + return defHttp.get<RuleChainType>(
  10 + {
  11 + url: `/ruleChain/${id}/metadata`,
  12 + },
  13 + { joinPrefix: false }
  14 + );
  15 +};
  16 +
  17 +export const saveRuleChainData = (data: RuleChainType) => {
  18 + return defHttp.post(
  19 + {
  20 + url: Api.SAVE,
  21 + data,
  22 + },
  23 + { joinPrefix: false }
  24 + );
  25 +};
... ...
  1 +export enum AlarmStatus {
  2 + CLEARED_UN_ACK = 'CLEARED_UNACK',
  3 + ACTIVE_UN_ACK = 'ACTIVE_UNACK',
  4 + CLEARED_ACK = 'CLEARED_ACK',
  5 + ACTIVE_ACK = 'ACTIVE_ACK',
  6 +}
  7 +
  8 +export enum AlarmStatusMean {
  9 + CLEARED_UNACK = '清除未确认',
  10 + ACTIVE_UNACK = '激活未确认',
  11 + CLEARED_ACK = '清除已确认',
  12 + ACTIVE_ACK = '激活已确认',
  13 +}
... ...
... ... @@ -3,20 +3,7 @@ import { FormSchema } from '/@/components/Form';
3 3 import { BasicColumn } from '/@/components/Table';
4 4 import moment from 'moment';
5 5 import { findDictItemByCode } from '/@/api/system/dict';
6   -
7   -export enum AlarmStatus {
8   - CLEARED_UN_ACK = 'CLEARED_UNACK',
9   - ACTIVE_UN_ACK = 'ACTIVE_UNACK',
10   - CLEARED_ACK = 'CLEARED_ACK',
11   - ACTIVE_ACK = 'ACTIVE_ACK',
12   -}
13   -
14   -export enum AlarmStatusMean {
15   - CLEARED_UNACK = '清除未确认',
16   - ACTIVE_UNACK = '激活未确认',
17   - CLEARED_ACK = '清除已确认',
18   - ACTIVE_ACK = '激活已确认',
19   -}
  6 +import { AlarmStatus, AlarmStatusMean } from '/@/enums/alarmEnum';
20 7
21 8 export const alarmSearchSchemas: FormSchema[] = [
22 9 {
... ...
1   -import { AlarmStatus, AlarmStatusMean } from '../config/detail.config';
2 1 import { clearOrAckAlarm, getDeviceAlarm } from '/@/api/device/deviceManager';
3 2 import { notification, Button, Tag } from 'ant-design-vue';
4 3 import { h, onMounted, onUnmounted } from 'vue';
... ... @@ -8,6 +7,7 @@ import { RoleEnum } from '/@/enums/roleEnum';
8 7 import { usePermission } from '/@/hooks/web/usePermission';
9 8 import { useUserStore } from '/@/store/modules/user';
10 9 import { useGlobSetting } from '/@/hooks/setting';
  10 +import { AlarmStatus, AlarmStatusMean } from '/@/enums/alarmEnum';
11 11
12 12 interface UseAlarmNotifyParams {
13 13 alarmNotifyStatus?: AlarmStatus;
... ...
  1 +export enum FilterCategoryComponentEnum {
  2 + CHECK_ALARM_STATUS = 'CheckAlarmStatus',
  3 +}
  4 +
  5 +export enum EntryComponentEnum {
  6 + INPUT = 'Input',
  7 +}
... ...
  1 +export enum HandleTypeEnum {
  2 + SOURCE = 'source',
  3 + TARGET = 'target',
  4 +}
  5 +
  6 +export enum NodeTypeEnum {
  7 + CUSTOM = 'custom',
  8 +}
  9 +
  10 +export enum EdgeTypeEnum {
  11 + CUSTOM = 'custom',
  12 +}
  13 +
  14 +export enum BasicConnectionModalEnum {
  15 + BASIC = 'BASIC',
  16 + CUSTOM = 'CUSTOM',
  17 +}
  18 +
  19 +export enum MarkerArrowEnum {
  20 + BASIC_ARROW = 'basicArrow',
  21 + BASIC_ARROW_SELECTED = 'basicArrowSelected',
  22 +}
  23 +
  24 +export enum ElementsTypeEnum {
  25 + NODE = 'NODE',
  26 + EDGE = 'EDGE',
  27 +}
... ...
  1 +export enum FetchNodeComFlagTypeENum {
  2 + CONNECTION_MODAL = 'CONNECTION_MODAL',
  3 + CREATE_MODAL = 'CREATE_MODAL',
  4 +}
  5 +
  6 +export enum NodeBindDataFieldEnum {
  7 + NAME = 'name',
  8 + ALARM_STATUS_LIST = 'alarmStatusList',
  9 + DESCRIPTION = 'description',
  10 + DEBUG_MODE = 'debugMode',
  11 +}
  12 +
  13 +export enum NodeBindDataFieldNameEnum {
  14 + NAME = '名称',
  15 + ALARM_STATUS_LIST = 'Alarm status filter',
  16 + DESCRIPTION = '说明',
  17 + DEBUG_MODE = '调试模式',
  18 +}
  19 +
  20 +export enum EdgeBindDataFieldEnum {
  21 + TYPE = 'type',
  22 +}
  23 +
  24 +export enum EdgeBindDataFieldNameEnum {
  25 + TYPE = '链接标签',
  26 +}
... ...
  1 +import type { Connection } from '@vue-flow/core';
  2 +import { EdgeTypeEnum } from '../enum';
  3 +import type { EdgeData } from '../types/node';
  4 +import { buildUUID } from '/@/utils/uuid';
  5 +
  6 +export function useAddEdges() {
  7 + const getAddedgesParams = (params: Connection, data: string | string[] | any) => {
  8 + return { type: EdgeTypeEnum.CUSTOM, data: { data } as EdgeData, id: buildUUID(), ...params };
  9 + };
  10 +
  11 + return { getAddedgesParams };
  12 +}
... ...
  1 +import { Node } from '@vue-flow/core';
  2 +import { NodeTypeEnum } from '../enum';
  3 +import { buildUUID } from '/@/utils/uuid';
  4 +
  5 +export const useAddNodes = () => {
  6 + const getAddNodesParams = (
  7 + position: Node['position'],
  8 + data: object,
  9 + options?: Partial<Node>
  10 + ): Node => {
  11 + return {
  12 + id: buildUUID(),
  13 + type: NodeTypeEnum.CUSTOM,
  14 + position,
  15 + data,
  16 + ...options,
  17 + };
  18 + };
  19 +
  20 + return { getAddNodesParams };
  21 +};
... ...
  1 +import { Component, computed, ref, shallowRef, toRaw, unref } from 'vue';
  2 +import { fetchConnectionComponent, fetchCreateComponent } from '../packages';
  3 +import { CreateModalDefineExposeType, AwaitPopupWindowReturnDataType } from '../types';
  4 +import { EdgeData, NodeData } from '../types/node';
  5 +import { DataActionModeEnum } from '/@/enums/toolEnum';
  6 +import { ElementsTypeEnum } from '../enum';
  7 +
  8 +interface OptionsType {
  9 + mode: DataActionModeEnum;
  10 + type: ElementsTypeEnum;
  11 +}
  12 +
  13 +export function useAwaitPopupWindowBindData(
  14 + options: OptionsType = { mode: DataActionModeEnum.CREATE, type: ElementsTypeEnum.NODE }
  15 +) {
  16 + const { type, mode } = options;
  17 +
  18 + const visible = ref(false);
  19 +
  20 + const nodeData = ref<NodeData>();
  21 +
  22 + const edgeData = ref<EdgeData>();
  23 +
  24 + const spinning = ref(false);
  25 +
  26 + const resolveFn = ref<(options: AwaitPopupWindowReturnDataType) => void>();
  27 +
  28 + const createComponentEl = ref<Nullable<CreateModalDefineExposeType>>();
  29 +
  30 + const shadowComponent = shallowRef<Nullable<Component>>();
  31 +
  32 + const getNodeSetValue = computed(() => {
  33 + return unref(mode) === DataActionModeEnum.CREATE
  34 + ? unref(nodeData)?.config?.configurationDescriptor.nodeDefinition.defaultConfiguration
  35 + : unref(nodeData)?.data?.configuration;
  36 + });
  37 +
  38 + const getEdgeSetValue = computed(() => {
  39 + return {
  40 + type:
  41 + unref(mode) === DataActionModeEnum.CREATE
  42 + ? unref(nodeData)?.config?.configurationDescriptor?.nodeDefinition.relationTypes
  43 + : unref(edgeData)?.data,
  44 + };
  45 + });
  46 +
  47 + const getSetValue = computed(() =>
  48 + unref(type) === ElementsTypeEnum.EDGE ? unref(getEdgeSetValue) : unref(getNodeSetValue)
  49 + );
  50 +
  51 + const getComponentKey = computed(() => unref(nodeData)?.config?.key);
  52 +
  53 + const handleFetchComponent = async (nodeOptions: NodeData, edgeOptions?: EdgeData) => {
  54 + nodeData.value = nodeOptions;
  55 + edgeData.value = edgeOptions;
  56 +
  57 + spinning.value = true;
  58 + shadowComponent.value = null;
  59 + const modules =
  60 + ElementsTypeEnum.NODE === type
  61 + ? await fetchCreateComponent(nodeOptions.config!)
  62 + : await fetchConnectionComponent(nodeOptions.config!);
  63 +
  64 + const component = (await modules?.()) as Record<'default', Component>;
  65 + shadowComponent.value = component?.default;
  66 + spinning.value = false;
  67 + };
  68 +
  69 + const open = async (
  70 + nodeData: NodeData,
  71 + edgeData?: EdgeData
  72 + ): Promise<AwaitPopupWindowReturnDataType> => {
  73 + await handleFetchComponent(nodeData, edgeData);
  74 + return new Promise((_resolve) => {
  75 + visible.value = true;
  76 + resolveFn.value = _resolve;
  77 + });
  78 + };
  79 +
  80 + const handleOnMounted = () => {
  81 + unref(createComponentEl)?.setFieldsValue?.(toRaw(unref(getSetValue)), toRaw(unref(nodeData)));
  82 + };
  83 +
  84 + const validate = async () => {
  85 + return (await unref(createComponentEl)?.validate?.()) || { flag: true };
  86 + };
  87 +
  88 + const handleSubmitData = (extraData: Recordable = {}, value: Recordable = {}) => {
  89 + return Object.assign(
  90 + {},
  91 + extraData,
  92 + unref(type) === ElementsTypeEnum.NODE ? { configuration: toRaw(unref(value)) } : value
  93 + );
  94 + };
  95 +
  96 + const handleSubmit = async (extraData: Recordable = {}) => {
  97 + const { flag } = await validate();
  98 + if (!flag) return;
  99 + const value = await unref(createComponentEl)?.getFieldsValue?.();
  100 + unref(resolveFn)?.({
  101 + flag: true,
  102 + data: handleSubmitData(extraData, value),
  103 + } as AwaitPopupWindowReturnDataType);
  104 + visible.value = false;
  105 + };
  106 +
  107 + const handleCancel = () => {
  108 + unref(resolveFn)?.({ flag: false, data: null } as AwaitPopupWindowReturnDataType);
  109 + visible.value = false;
  110 + };
  111 +
  112 + return {
  113 + visible,
  114 + nodeData,
  115 + spinning,
  116 + shadowComponent,
  117 + createComponentEl,
  118 + getComponentKey,
  119 + open,
  120 + handleOnMounted,
  121 + handleSubmit,
  122 + handleCancel,
  123 + };
  124 +}
... ...
  1 +import { Ref, toRaw, unref } from 'vue';
  2 +import { BasicNodeBindData, NodeData } from '../types/node';
  3 +import { Elements, GraphNode } from '@vue-flow/core';
  4 +import { RuleChainType } from '../types/ruleNode';
  5 +import { allComponents } from '../packages';
  6 +import { RuleNodeTypeEnum } from '../packages/index.type';
  7 +import { buildUUID } from '/@/utils/uuid';
  8 +import { isNullOrUnDef } from '/@/utils/is';
  9 +import { useAddNodes } from './useAddNodes';
  10 +import { useAddEdges } from './useAddEdges';
  11 +
  12 +export function useBasicDataTransform() {
  13 + const nodeConfigMap = new Map<string, NodeData>();
  14 +
  15 + function initNodeConfigMap() {
  16 + for (const key of Object.keys(allComponents)) {
  17 + const category = allComponents[key as RuleNodeTypeEnum];
  18 + for (const nodeConfig of category.components) {
  19 + const { clazz } = nodeConfig;
  20 + nodeConfigMap.set(clazz, { config: nodeConfig, category: category.category });
  21 + }
  22 + }
  23 + }
  24 +
  25 + function mergeData(data: NodeData['data'], nodeData: NodeData, node: GraphNode) {
  26 + const { x: layoutX, y: layoutY } = node.computedPosition;
  27 +
  28 + return {
  29 + debugMode: !!data?.debugMode,
  30 + name: data?.name,
  31 + type: nodeData.config?.clazz,
  32 + configuration: toRaw(unref(data?.configuration)),
  33 + additionalInfo: {
  34 + description: data?.description,
  35 + layoutX,
  36 + layoutY,
  37 + },
  38 + };
  39 + }
  40 +
  41 + function deconstructionConnection(
  42 + ruleChain: Ref<RuleChainType> | RuleChainType,
  43 + inputNodeId: string
  44 + ) {
  45 + const { connections, nodes, firstNodeIndex } = unref(ruleChain);
  46 + const indexMap = new Map<number, BasicNodeBindData>();
  47 + const groupByConnections = new Map<string, string[]>();
  48 + const SOURCE_HANDLE = '__handle-right';
  49 + const TARGET_HANDLE = '__handle-left';
  50 + const SEPARATOR = ',';
  51 + const edges: Elements = [];
  52 + const { getAddedgesParams } = useAddEdges();
  53 +
  54 + nodes.forEach((item, index) => indexMap.set(index, item));
  55 +
  56 + connections.forEach((item) => {
  57 + const { fromIndex, toIndex, type } = item;
  58 + const key = `${fromIndex}${SEPARATOR}${toIndex}`;
  59 + if (!groupByConnections.has(key)) groupByConnections.set(key, []);
  60 +
  61 + const types = groupByConnections.get(key)!;
  62 + types.push(type);
  63 + });
  64 +
  65 + for (const [key, types] of Array.from(groupByConnections.entries())) {
  66 + const [fromIndex, toIndex] = key.split(SEPARATOR);
  67 +
  68 + const sourceNode = indexMap.get(Number(fromIndex));
  69 + const targetNode = indexMap.get(Number(toIndex));
  70 + const source = sourceNode!.id!.id;
  71 + const target = targetNode!.id!.id;
  72 + const sourceHandle = `${source}${SOURCE_HANDLE}`;
  73 + const targetHandle = `${target}${TARGET_HANDLE}`;
  74 +
  75 + edges.push(
  76 + getAddedgesParams(
  77 + {
  78 + source,
  79 + target,
  80 + sourceHandle,
  81 + targetHandle,
  82 + },
  83 + { type: types }
  84 + )
  85 + );
  86 + }
  87 +
  88 + if (!isNullOrUnDef(firstNodeIndex)) {
  89 + const targetNode = indexMap.get(firstNodeIndex);
  90 + const source = inputNodeId;
  91 + const target = targetNode!.id!.id;
  92 + const sourceHandle = `${source}$${SOURCE_HANDLE}`;
  93 + const targetHandle = `${target}${TARGET_HANDLE}`;
  94 + edges.push(
  95 + getAddedgesParams(
  96 + {
  97 + source,
  98 + target,
  99 + sourceHandle,
  100 + targetHandle,
  101 + },
  102 + {}
  103 + )
  104 + );
  105 + }
  106 +
  107 + return edges;
  108 + }
  109 +
  110 + function genNewNodeByData(node: BasicNodeBindData, config: NodeData) {
  111 + const { additionalInfo, configuration, debugMode, name, id } = node;
  112 + const { layoutX, layoutY, description } = additionalInfo || {};
  113 + const { getAddNodesParams } = useAddNodes();
  114 +
  115 + return getAddNodesParams(
  116 + { x: layoutX!, y: layoutY! },
  117 + {
  118 + ...config,
  119 + data: {
  120 + configuration,
  121 + debugMode,
  122 + description,
  123 + name,
  124 + },
  125 + },
  126 + {
  127 + id: id?.id || buildUUID(),
  128 + }
  129 + );
  130 + }
  131 +
  132 + function deconstructionNode(nodes: RuleChainType['nodes']) {
  133 + const addNodes: Elements = [];
  134 + for (const node of unref(nodes)) {
  135 + const { type } = node;
  136 + if (!type) continue;
  137 + const nodeConfig = nodeConfigMap.get(type);
  138 +
  139 + if (!nodeConfig) {
  140 + throw `No component configuration of type '${type}' was found`;
  141 + }
  142 +
  143 + const newNode = genNewNodeByData(node, nodeConfig);
  144 +
  145 + addNodes.push(newNode);
  146 + }
  147 + return addNodes;
  148 + }
  149 +
  150 + function deconstructionData(
  151 + ruleChain: RuleChainType | Ref<RuleChainType | undefined>,
  152 + inputNodeId: string
  153 + ) {
  154 + if (!ruleChain || !unref(ruleChain)) return;
  155 + ruleChain = toRaw(unref(ruleChain))!;
  156 +
  157 + const nodes = deconstructionNode(ruleChain?.nodes || []);
  158 +
  159 + const edges = deconstructionConnection(ruleChain!, inputNodeId);
  160 +
  161 + return {
  162 + nodes,
  163 + edges,
  164 + };
  165 + }
  166 +
  167 + initNodeConfigMap();
  168 +
  169 + return {
  170 + mergeData,
  171 + deconstructionData,
  172 + };
  173 +}
... ...
  1 +import { EdgeProps, useEdge } from '@vue-flow/core';
  2 +import { computed, unref } from 'vue';
  3 +import { MarkerArrowEnum } from '../enum';
  4 +
  5 +export const useConnectionFocus = (props: EdgeProps) => {
  6 + const { edge } = useEdge(props.id);
  7 +
  8 + const getSelected = computed(() => unref(edge)?.selected);
  9 +
  10 + const getMarkerEnd = computed(
  11 + () =>
  12 + `url(#${
  13 + unref(getSelected) ? MarkerArrowEnum.BASIC_ARROW_SELECTED : MarkerArrowEnum.BASIC_ARROW
  14 + })`
  15 + );
  16 +
  17 + return {
  18 + getMarkerEnd,
  19 + getSelected,
  20 + };
  21 +};
... ...
  1 +import type { NodeItemConfigType } from '../types/node';
  2 +
  3 +export function useCreateNodeKey(scope: string) {
  4 + return {
  5 + key: scope,
  6 + createComponentKey: `VC_CREATE_${scope}`,
  7 + connectionComponentKey: `VCC_CONNECTION_${scope}`,
  8 + } as NodeItemConfigType;
  9 +}
... ...
  1 +import { Ref, toRaw, unref } from 'vue';
  2 +import { NodeData } from '../types/node';
  3 +
  4 +export function useDataTool() {
  5 + /**
  6 + * @description 通过NodeData获取默认配置信息
  7 + * @param nodeData
  8 + * @returns
  9 + */
  10 + function getDefaultConfigurationByNodeData<T = any>(nodeData: Ref<NodeData> | NodeData) {
  11 + nodeData = toRaw(unref(nodeData));
  12 +
  13 + const { nodeDefinition } = nodeData.config?.configurationDescriptor || {};
  14 + const { defaultConfiguration } = nodeDefinition || {};
  15 +
  16 + return { defaultConfiguration: defaultConfiguration as T };
  17 + }
  18 +
  19 + /**
  20 + * @description 通过NodeData获取节点绑定信息
  21 + * @param nodeData
  22 + * @returns
  23 + */
  24 + function getBindDataByNodeData<T = any>(nodeData: Ref<NodeData> | NodeData) {
  25 + nodeData = toRaw(unref(nodeData));
  26 +
  27 + const data = nodeData.data as T;
  28 +
  29 + return { data };
  30 + }
  31 +
  32 + return { getDefaultConfigurationByNodeData, getBindDataByNodeData };
  33 +}
... ...
  1 +import { Ref, toRaw, unref } from 'vue';
  2 +import type { VueFlowStore } from '@vue-flow/core';
  3 +import type { FlowElRef } from '../types/flow';
  4 +import type { CreateNodeModal } from '../src/components/CreateNodeModal';
  5 +import type { DragTransferData } from '../types/node';
  6 +import { useAddNodes } from './useAddNodes';
  7 +
  8 +type EffectSymbol = DataTransfer['dropEffect'];
  9 +
  10 +export const TRANSFER_DATA_KEY = 'NODE_INFO';
  11 +export const EFFECT_SYMBOL: EffectSymbol = 'move';
  12 +
  13 +interface UseDragCreateOptionsType {
  14 + el: Ref<Nullable<FlowElRef>>;
  15 + createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
  16 + flowActionType: VueFlowStore;
  17 + triggerChange: () => void;
  18 +}
  19 +
  20 +export function useDragCreate(options: UseDragCreateOptionsType) {
  21 + const { el, createNodeModalActionType, flowActionType, triggerChange } = options;
  22 + const { project, addNodes } = flowActionType;
  23 + const { getAddNodesParams } = useAddNodes();
  24 +
  25 + const handleOnDrop = async (event: DragEvent) => {
  26 + const value = event.dataTransfer?.getData(TRANSFER_DATA_KEY);
  27 + const transferData: DragTransferData = JSON.parse(value || '');
  28 + const { options: nodeData, offsetX, offsetY } = transferData;
  29 + const { flag, data } = (await unref(createNodeModalActionType)?.open(nodeData)) || {};
  30 + if (!flag) return;
  31 +
  32 + const flowBounds = unref(el)?.$el.getBoundingClientRect() as DOMRect;
  33 +
  34 + const position = project({
  35 + x: event.clientX - flowBounds.left - offsetX,
  36 + y: event.clientY - flowBounds.top - offsetY,
  37 + });
  38 +
  39 + const newNode = getAddNodesParams(position, { ...toRaw(unref(nodeData)), data });
  40 + addNodes(newNode);
  41 +
  42 + triggerChange();
  43 + };
  44 +
  45 + const handleOnDragOver = (event: DragEvent) => {
  46 + event.preventDefault();
  47 +
  48 + if (event.dataTransfer) event.dataTransfer.dropEffect = EFFECT_SYMBOL;
  49 + };
  50 +
  51 + const handleOnDragStart = (event: DragEvent, options: object) => {
  52 + if (event.dataTransfer) {
  53 + event.dataTransfer.setData(TRANSFER_DATA_KEY, JSON.stringify(options));
  54 + event.dataTransfer.effectAllowed = EFFECT_SYMBOL;
  55 + }
  56 + };
  57 +
  58 + return {
  59 + handleOnDrop,
  60 + handleOnDragOver,
  61 + handleOnDragStart,
  62 + };
  63 +}
... ...
  1 +import type { Ref } from 'vue';
  2 +import { inject, provide } from 'vue';
  3 +import type { VueFlowStore } from '@vue-flow/core';
  4 +import type { CreateNodeModal } from '../src/components/CreateNodeModal';
  5 +import type { CreateEdgeModal } from '../src/components/CreateEdgeModal';
  6 +import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer';
  7 +import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer';
  8 +
  9 +const SYMBOL = Symbol('flow-context');
  10 +
  11 +interface FlowContextOptionsType {
  12 + /**
  13 + * @description 节点 actions
  14 + */
  15 + createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
  16 +
  17 + /**
  18 + * @description 连接线 actions
  19 + */
  20 + createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>;
  21 +
  22 + /**
  23 + * @description 节点更新 actions
  24 + */
  25 + updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>;
  26 +
  27 + /**
  28 + * @description 连接线更新 actions
  29 + */
  30 + updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>;
  31 +
  32 + /**
  33 + * @description vue flow store
  34 + */
  35 + flowActionType: VueFlowStore;
  36 +
  37 + /**
  38 + * @description 更新变化
  39 + */
  40 + triggerChange: () => void;
  41 +}
  42 +
  43 +export const createFlowContext = (options: FlowContextOptionsType) => {
  44 + provide(SYMBOL, options);
  45 +};
  46 +
  47 +export const useFlowContext = () => {
  48 + return inject(SYMBOL) as FlowContextOptionsType;
  49 +};
... ...
  1 +import { useFullscreen } from '@vueuse/core';
  2 +import type { Ref } from 'vue';
  3 +import { computed, unref } from 'vue';
  4 +
  5 +export function useFullScreen(el: Ref<Nullable<HTMLDivElement>>) {
  6 + const { toggle, isFullscreen } = useFullscreen(el);
  7 +
  8 + const getFullScreenIcon = computed(() =>
  9 + unref(isFullscreen) ? 'bx:exit-fullscreen' : 'mdi:fullscreen'
  10 + );
  11 +
  12 + return {
  13 + getFullScreenIcon,
  14 + handleFullScreen: toggle,
  15 + };
  16 +}
... ...
  1 +import { Config as InputConfig } from '../packages/Entry/Input/config';
  2 +import { useAddNodes } from './useAddNodes';
  3 +
  4 +export function useInputNode() {
  5 + const getInputNodeConfig = (id?: string) => {
  6 + const { getAddNodesParams } = useAddNodes();
  7 +
  8 + const newNode = getAddNodesParams(
  9 + { x: 80, y: 50 },
  10 + {
  11 + ...new InputConfig(),
  12 + data: {
  13 + name: '输入',
  14 + description: '规则链的逻辑输入,将传入消息转发到下一个相关规则节点。',
  15 + },
  16 + },
  17 + { id, draggable: false }
  18 + );
  19 +
  20 + return newNode;
  21 + };
  22 +
  23 + return { getInputNodeConfig };
  24 +}
... ...
  1 +import type {
  2 + Connection,
  3 + EdgeComponent,
  4 + NodeComponent,
  5 + ValidConnectionFunc,
  6 + GraphNode,
  7 +} from '@vue-flow/core';
  8 +import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core';
  9 +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';
  13 +import { EdgeTypeEnum, NodeTypeEnum } from '../enum';
  14 +import { BasicEdge, BasicNode } from '../src/components';
  15 +import type { EdgeData, NodeData } from '../types/node';
  16 +import type { CreateEdgeModal } from '../src/components/CreateEdgeModal';
  17 +import { isInputHandle, isOutputHandle } from '../utils';
  18 +import { useAddEdges } from './useAddEdges';
  19 +import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer';
  20 +import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer';
  21 +import { isNumber } from '/@/utils/is';
  22 +
  23 +interface UseRuleFlowOptionsType {
  24 + id: string;
  25 + createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
  26 + createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>;
  27 + updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>;
  28 + updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>;
  29 + triggerChange: () => void;
  30 +}
  31 +
  32 +const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => {
  33 + const { sourceHandle, targetHandle, source, target } = connection;
  34 +
  35 + return (
  36 + isOutputHandle(sourceHandle || '') && isInputHandle(targetHandle || '') && source !== target
  37 + );
  38 +};
  39 +
  40 +export function useRuleFlow(options: UseRuleFlowOptionsType) {
  41 + const {
  42 + id,
  43 + createEdgeModalActionType,
  44 + updateEdgeDrawerActionType,
  45 + updateNodeDrawerActionType,
  46 + triggerChange,
  47 + } = options;
  48 +
  49 + const flowActionType = useVueFlow({
  50 + id,
  51 + maxZoom: 1,
  52 + minZoom: 1,
  53 + panOnScroll: true,
  54 + selectionMode: SelectionMode.Partial,
  55 + nodeTypes: {
  56 + [NodeTypeEnum.CUSTOM]: markRaw(BasicNode) as NodeComponent,
  57 + },
  58 + edgeTypes: {
  59 + [EdgeTypeEnum.CUSTOM]: markRaw(BasicEdge) as EdgeComponent,
  60 + },
  61 + connectionLineOptions: {
  62 + type: ConnectionLineType.Bezier,
  63 + },
  64 + defaultViewport: {
  65 + x: 0,
  66 + y: 0,
  67 + },
  68 + isValidConnection(connection, elements) {
  69 + const validateList = [validateInputAndOutput];
  70 + const targetData = elements.targetNode.data as NodeData;
  71 +
  72 + if (
  73 + targetData.category?.validateConnection &&
  74 + isFunction(targetData.category.validateConnection)
  75 + )
  76 + validateList.push(targetData.category?.validateConnection);
  77 +
  78 + if (targetData.config?.validateConnection && isFunction(targetData.config.validateConnection))
  79 + validateList.push(targetData.config.validateConnection);
  80 +
  81 + if (!validateList.every((item) => item(connection, elements))) return false;
  82 +
  83 + return true;
  84 + },
  85 + });
  86 +
  87 + const {
  88 + getEdges,
  89 + addEdges,
  90 + findEdge,
  91 + findNode,
  92 + setViewport,
  93 + removeEdges,
  94 + onConnect,
  95 + onPaneReady,
  96 + onNodeDoubleClick,
  97 + onEdgeDoubleClick,
  98 + onNodeDragStop,
  99 + } = flowActionType;
  100 +
  101 + const { getAddedgesParams } = useAddEdges();
  102 +
  103 + onConnect(async (params) => {
  104 + const { source } = params;
  105 + const sourceNode = findNode(source);
  106 + const sourceData = sourceNode?.data as NodeData;
  107 +
  108 + let types: string[] = [];
  109 +
  110 + if (sourceData && validateHasLabelConnection(sourceData)) {
  111 + const { flag, data } = (await unref(createEdgeModalActionType)?.open(sourceData)) || {};
  112 + if (!flag) return;
  113 + types = toRaw(unref(data));
  114 + }
  115 +
  116 + handleMaxConnectionPoint(sourceNode);
  117 +
  118 + addEdges(getAddedgesParams(params, types));
  119 +
  120 + triggerChange();
  121 + });
  122 +
  123 + onPaneReady(async () => {
  124 + setViewport({ x: 0, y: 0, zoom: 1 });
  125 + });
  126 +
  127 + onNodeDoubleClick(async ({ node }) => {
  128 + const { flag, data } =
  129 + (await unref(updateNodeDrawerActionType)?.open(
  130 + toRaw((node as NodeData)?.data as unknown as NodeData)
  131 + )) || {};
  132 +
  133 + if (!flag) return;
  134 +
  135 + const currentNode = findNode(node.id);
  136 +
  137 + (currentNode!.data as NodeData).data = data;
  138 + });
  139 +
  140 + onEdgeDoubleClick(async ({ edge }) => {
  141 + if (!validateHasLabelConnection(edge.sourceNode.data)) return;
  142 +
  143 + const { flag, data } =
  144 + (await unref(updateEdgeDrawerActionType)?.open(
  145 + toRaw(unref(edge.sourceNode?.data as unknown as NodeData)),
  146 + toRaw(unref(edge.data as EdgeData))
  147 + )) || {};
  148 +
  149 + if (!flag) return;
  150 +
  151 + const currentEdge = findEdge(edge.id);
  152 +
  153 + (currentEdge!.data as EdgeData).data = toRaw(unref(data));
  154 + });
  155 +
  156 + onNodeDragStop(() => {
  157 + triggerChange();
  158 + });
  159 +
  160 + /**
  161 + * @description 验证是否有连接label
  162 + * @param sourceData
  163 + * @returns
  164 + */
  165 + function validateHasLabelConnection(sourceData: NodeData) {
  166 + return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length;
  167 + }
  168 +
  169 + function handleMaxConnectionPoint(sourceNode?: GraphNode) {
  170 + if (!sourceNode) return;
  171 +
  172 + const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint;
  173 +
  174 + if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return;
  175 +
  176 + const sourceId = sourceNode.id;
  177 + const connectionPool = unref(getEdges).filter((item) => item.source === sourceId);
  178 + if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) {
  179 + removeEdges(connectionPool[0].id);
  180 + }
  181 + }
  182 +
  183 + return { flowActionType };
  184 +}
... ...
  1 +import type { VueFlowStore, Getters, Elements } from '@vue-flow/core';
  2 +import { ComputedRef, ref, unref } from 'vue';
  3 +import { BasicNodeBindData, EdgeData, NodeData } from '../types/node';
  4 +import { EntryComponentEnum } from '../enum/category';
  5 +import { useBasicDataTransform } from './useBasicDataTransform';
  6 +import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner';
  7 +import { ConnectionItemType, RuleChainType } from '../types/ruleNode';
  8 +import { useInputNode } from './useInputNode';
  9 +import { buildUUID } from '/@/utils/uuid';
  10 +
  11 +const ignoreNodeKeys: string[] = [EntryComponentEnum.INPUT];
  12 +
  13 +export function useSaveAndRedo() {
  14 + const changeMarker = ref(false);
  15 +
  16 + const loading = ref(false);
  17 +
  18 + const redoDataRef = ref<Elements>([]);
  19 +
  20 + const { mergeData, deconstructionData } = useBasicDataTransform();
  21 +
  22 + const triggerChange = () => {
  23 + changeMarker.value = true;
  24 + };
  25 +
  26 + const resetChange = () => {
  27 + changeMarker.value = false;
  28 + };
  29 +
  30 + /**
  31 + * @description 保存连接信息
  32 + */
  33 + function getConnections(
  34 + nodesRef: ComputedRef<Getters['getNodes']> | Getters['getNodes'],
  35 + edges: ComputedRef<Getters['getEdges']>
  36 + ) {
  37 + const nodeIndexMap = new Map();
  38 +
  39 + const connections: ConnectionItemType[] = [];
  40 +
  41 + unref(nodesRef).forEach((item, index) => {
  42 + nodeIndexMap.set(item.id, index);
  43 + });
  44 +
  45 + for (const item of unref(edges)) {
  46 + const { data, target, source } = item;
  47 + const { data: bindData } = data as EdgeData;
  48 + const { type } = bindData || {};
  49 + const fromIndex = nodeIndexMap.get(source);
  50 + const toIndex = nodeIndexMap.get(target);
  51 + type?.forEach((key) => {
  52 + connections.push({ fromIndex, toIndex, type: key });
  53 + });
  54 + }
  55 +
  56 + return connections;
  57 + }
  58 +
  59 + function getNodes(nodesRef: ComputedRef<Getters['getNodes']> | Getters['getNodes']) {
  60 + const nodes: BasicNodeBindData[] = [];
  61 +
  62 + for (const node of unref(nodesRef)) {
  63 + const nodeData = node.data as NodeData;
  64 +
  65 + if (ignoreNodeKeys.includes(nodeData.config?.key as string)) continue;
  66 +
  67 + const data = nodeData.data;
  68 +
  69 + nodes.push(mergeData(data, nodeData, node));
  70 + }
  71 +
  72 + return nodes;
  73 + }
  74 +
  75 + function getFirsetNodeIndex(
  76 + nodesRef: ComputedRef<Getters['getNodes']> | Getters['getNodes'],
  77 + edges: ComputedRef<Getters['getEdges']>
  78 + ) {
  79 + const inputNode = unref(edges).find(
  80 + (item) => (item.sourceNode.data as NodeData).config?.key === EntryComponentEnum.INPUT
  81 + );
  82 +
  83 + if (inputNode) {
  84 + const targetId = inputNode.target;
  85 + const index = unref(nodesRef).findIndex((item) => item.id === targetId);
  86 + return index;
  87 + }
  88 + }
  89 +
  90 + const handleApplyChange = (flowActionType: VueFlowStore) => {
  91 + if (!unref(changeMarker)) return;
  92 +
  93 + const edgesRef = flowActionType.getEdges;
  94 +
  95 + const extraIgnoreNodeRef = unref(flowActionType.getNodes).filter(
  96 + (item) => !ignoreNodeKeys.includes((item.data as NodeData).config?.key as string)
  97 + );
  98 +
  99 + const connections = getConnections(extraIgnoreNodeRef, edgesRef);
  100 +
  101 + const nodes = getNodes(extraIgnoreNodeRef);
  102 +
  103 + const firstNodeIndex = getFirsetNodeIndex(extraIgnoreNodeRef, edgesRef);
  104 +
  105 + handleSaveRuleChain(connections, nodes, firstNodeIndex);
  106 + };
  107 +
  108 + const handleRedoChange = (flowActionType: VueFlowStore) => {
  109 + if (!unref(changeMarker)) return;
  110 + flowActionType.setElements(unref(redoDataRef));
  111 + resetChange();
  112 + };
  113 +
  114 + async function handleSaveRuleChain(
  115 + connections: ConnectionItemType[],
  116 + nodes: BasicNodeBindData[],
  117 + firstNodeIndex?: number
  118 + ) {
  119 + try {
  120 + loading.value = true;
  121 + const data = await saveRuleChainData({
  122 + connections,
  123 + nodes,
  124 + firstNodeIndex,
  125 + ruleChainId: {
  126 + entityType: 'RULE_CHAIN',
  127 + id: 'd6ddea70-25da-11ee-bc6b-47e715464e68',
  128 + },
  129 + });
  130 +
  131 + parseRuleChain(data);
  132 +
  133 + resetChange();
  134 + } finally {
  135 + loading.value = false;
  136 + }
  137 + }
  138 +
  139 + async function getCurrentPageMetaData(flowActionType: VueFlowStore) {
  140 + try {
  141 + loading.value = true;
  142 +
  143 + const id = 'd6ddea70-25da-11ee-bc6b-47e715464e68';
  144 +
  145 + const data = await getRuleChainData(id);
  146 +
  147 + const elements = parseRuleChain(data);
  148 +
  149 + flowActionType.setElements(elements);
  150 +
  151 + resetChange();
  152 + } finally {
  153 + loading.value = false;
  154 + }
  155 + }
  156 +
  157 + function parseRuleChain(ruleChain: RuleChainType) {
  158 + const inputId = buildUUID();
  159 +
  160 + const { getInputNodeConfig } = useInputNode();
  161 +
  162 + const value = deconstructionData(ruleChain, inputId);
  163 +
  164 + const { nodes = [], edges = [] } = value || {};
  165 +
  166 + const inputNode = getInputNodeConfig(inputId);
  167 +
  168 + const elements = [inputNode, ...nodes, ...edges];
  169 +
  170 + redoDataRef.value = elements;
  171 +
  172 + return elements;
  173 + }
  174 +
  175 + return {
  176 + loading,
  177 + changeMarker,
  178 + triggerChange,
  179 + handleApplyChange,
  180 + handleRedoChange,
  181 + getCurrentPageMetaData,
  182 + };
  183 +}
... ...
  1 +export function useTextWidth(text: string, fontSize: number) {
  2 + const LetterMap = {
  3 + ' ': 0.3329986572265625,
  4 + a: 0.5589996337890625,
  5 + A: 0.6569992065429687,
  6 + b: 0.58599853515625,
  7 + B: 0.6769989013671875,
  8 + c: 0.5469985961914062,
  9 + C: 0.7279998779296875,
  10 + d: 0.58599853515625,
  11 + D: 0.705999755859375,
  12 + e: 0.554998779296875,
  13 + E: 0.63699951171875,
  14 + f: 0.37299957275390627,
  15 + F: 0.5769989013671875,
  16 + g: 0.5909988403320312,
  17 + G: 0.7479995727539063,
  18 + h: 0.555999755859375,
  19 + H: 0.7199996948242188,
  20 + i: 0.255999755859375,
  21 + I: 0.23699951171875,
  22 + j: 0.26699981689453123,
  23 + J: 0.5169998168945312,
  24 + k: 0.5289993286132812,
  25 + K: 0.6899993896484375,
  26 + l: 0.23499908447265624,
  27 + L: 0.5879989624023437,
  28 + m: 0.854998779296875,
  29 + M: 0.8819992065429687,
  30 + n: 0.5589996337890625,
  31 + N: 0.7189987182617188,
  32 + o: 0.58599853515625,
  33 + O: 0.7669998168945312,
  34 + p: 0.58599853515625,
  35 + P: 0.6419998168945312,
  36 + q: 0.58599853515625,
  37 + Q: 0.7669998168945312,
  38 + r: 0.3649993896484375,
  39 + R: 0.6759994506835938,
  40 + s: 0.504998779296875,
  41 + S: 0.6319992065429687,
  42 + t: 0.354998779296875,
  43 + T: 0.6189987182617187,
  44 + u: 0.5599990844726562,
  45 + U: 0.7139999389648437,
  46 + v: 0.48199920654296874,
  47 + V: 0.6389999389648438,
  48 + w: 0.754998779296875,
  49 + W: 0.929998779296875,
  50 + x: 0.5089996337890625,
  51 + X: 0.63699951171875,
  52 + y: 0.4959991455078125,
  53 + Y: 0.66199951171875,
  54 + z: 0.48699951171875,
  55 + Z: 0.6239990234375,
  56 + 0: 0.6,
  57 + 1: 0.40099945068359377,
  58 + 2: 0.6,
  59 + 3: 0.6,
  60 + 4: 0.6,
  61 + 5: 0.6,
  62 + 6: 0.6,
  63 + 7: 0.5469985961914062,
  64 + 8: 0.6,
  65 + 9: 0.6,
  66 + '[': 0.3329986572265625,
  67 + ']': 0.3329986572265625,
  68 + ',': 0.26399993896484375,
  69 + '.': 0.26399993896484375,
  70 + ';': 0.26399993896484375,
  71 + ':': 0.26399993896484375,
  72 + '{': 0.3329986572265625,
  73 + '}': 0.3329986572265625,
  74 + '\\': 0.5,
  75 + '|': 0.19499969482421875,
  76 + '=': 0.604998779296875,
  77 + '+': 0.604998779296875,
  78 + '-': 0.604998779296875,
  79 + _: 0.5,
  80 + '`': 0.3329986572265625,
  81 + ' ~': 0.8329986572265625,
  82 + '!': 0.3329986572265625,
  83 + '@': 0.8579986572265625,
  84 + '#': 0.6,
  85 + $: 0.6,
  86 + '%': 0.9699996948242188,
  87 + '^': 0.517999267578125,
  88 + '&': 0.7259994506835937,
  89 + '*': 0.505999755859375,
  90 + '(': 0.3329986572265625,
  91 + ')': 0.3329986572265625,
  92 + '<': 0.604998779296875,
  93 + '>': 0.604998779296875,
  94 + '/': 0.5,
  95 + '?': 0.53699951171875,
  96 + '"': 0.33699951171875,
  97 + };
  98 +
  99 + // 计算非中文字符宽度
  100 + const getLetterWidth = (letter: string, fontSize: number) => fontSize * (LetterMap[letter] || 1);
  101 +
  102 + // 计算文本宽度
  103 + const getTextWidth = (text: string, fontSize: number) => {
  104 + // 中文匹配正则
  105 + const pattern = new RegExp('[\u4E00-\u9FA5]+');
  106 + // 文本宽度
  107 + const textWidth = text.split('').reduce((pre, curLetter) => {
  108 + // 单个字符宽度
  109 + const letterWidth = pattern.test(curLetter) ? fontSize : getLetterWidth(curLetter, fontSize);
  110 + return pre + letterWidth;
  111 + }, 0);
  112 + return textWidth;
  113 + };
  114 +
  115 + return getTextWidth(text, fontSize);
  116 +}
... ...
  1 +export { default as RuleChainsDesigner } from './index.vue';
... ...
  1 +<script setup lang="ts">
  2 + import { Background, BackgroundVariant } from '@vue-flow/background';
  3 + import { Controls } from '@vue-flow/controls';
  4 + import { Panel, PanelPosition, VueFlow } from '@vue-flow/core';
  5 + import { computed, onMounted, Ref, ref, unref } from 'vue';
  6 + import './style';
  7 + import { BasicConnectionArrow, BasicConnectionLine, BasicEdge, Sidebar } from './src/components';
  8 + import type { FlowElRef } from './types/flow';
  9 + import { useDragCreate } from './hook/useDragCreate';
  10 + import { createFlowContext } from './hook/useFlowContext';
  11 + import { CreateNodeModal } from './src/components/CreateNodeModal';
  12 + import { useRuleFlow } from './hook/useRuleFlow';
  13 + import { CreateEdgeModal } from './src/components/CreateEdgeModal';
  14 + import { useFullScreen } from './hook/useFullScreen';
  15 + import { useSaveAndRedo } from './hook/useSaveAndRedo';
  16 + import { Icon } from '/@/components/Icon';
  17 + import { UpdateNodeDrawer } from './src/components/UpdateNodeDrawer';
  18 + import { UpdateEdgeDrawer } from './src/components/UpdateEdgeDrawer';
  19 +
  20 + const getId = Number(Math.random().toString().substring(2)).toString(16);
  21 +
  22 + const rootElRef = ref<Nullable<HTMLDivElement>>(null);
  23 +
  24 + const createNodeModalActionType = ref<Nullable<InstanceType<typeof CreateNodeModal>>>(null);
  25 +
  26 + const createEdgeModalActionType = ref<Nullable<InstanceType<typeof CreateEdgeModal>>>(null);
  27 +
  28 + const updateNodeDrawerActionType = ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>(null);
  29 +
  30 + const updateEdgeDrawerActionType = ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>(null);
  31 +
  32 + const flowElRef = ref<Nullable<FlowElRef>>(null);
  33 +
  34 + const elements = ref([]);
  35 +
  36 + const {
  37 + changeMarker,
  38 + getCurrentPageMetaData,
  39 + triggerChange,
  40 + handleApplyChange,
  41 + handleRedoChange,
  42 + } = useSaveAndRedo();
  43 +
  44 + const { flowActionType } = useRuleFlow({
  45 + id: getId,
  46 + createNodeModalActionType,
  47 + createEdgeModalActionType,
  48 + updateEdgeDrawerActionType,
  49 + updateNodeDrawerActionType,
  50 + triggerChange,
  51 + });
  52 +
  53 + const { handleOnDragOver, handleOnDrop } = useDragCreate({
  54 + el: flowElRef,
  55 + createNodeModalActionType,
  56 + flowActionType,
  57 + triggerChange,
  58 + });
  59 +
  60 + const { handleFullScreen, getFullScreenIcon } = useFullScreen(
  61 + rootElRef as unknown as Ref<Nullable<HTMLDivElement>>
  62 + );
  63 +
  64 + const handleGetContainer = () => unref(rootElRef)!;
  65 +
  66 + const getDeleteDisplayState = computed(() => unref(flowActionType.getSelectedElements).length);
  67 +
  68 + const handleDeleteSelectionElements = () => {
  69 + flowActionType.removeSelectedElements();
  70 + };
  71 +
  72 + onMounted(() => {
  73 + getCurrentPageMetaData(flowActionType);
  74 + });
  75 +
  76 + createFlowContext({
  77 + createEdgeModalActionType,
  78 + createNodeModalActionType,
  79 + updateEdgeDrawerActionType,
  80 + updateNodeDrawerActionType,
  81 + flowActionType,
  82 + triggerChange,
  83 + });
  84 +</script>
  85 +
  86 +<template>
  87 + <main ref="rootElRef" class="w-full h-full flex relative" @drop="handleOnDrop">
  88 + <Sidebar />
  89 +
  90 + <VueFlow
  91 + :id="getId"
  92 + ref="flowElRef"
  93 + v-model="elements"
  94 + class="w-full h-full"
  95 + @dragover="handleOnDragOver"
  96 + >
  97 + <template #connection-line="props">
  98 + <BasicConnectionLine v-bind="props" />
  99 + </template>
  100 + <template #edge-custom="props">
  101 + <BasicEdge v-bind="props" />
  102 + </template>
  103 +
  104 + <BasicConnectionArrow />
  105 +
  106 + <Background :variant="BackgroundVariant.Lines" :gap="25" pattern-color="#cfcfcf" />
  107 +
  108 + <Controls :position="PanelPosition.BottomLeft" />
  109 +
  110 + <Panel position="bottom-right" class="controls">
  111 + <section class="flex gap-4">
  112 + <button
  113 + :style="{ transform: `translateY(${getDeleteDisplayState ? 0 : '72px'})` }"
  114 + class="button-box-shadow w-14 h-14 flex justify-center items-center bg-orange-600 rounded-full transition-transform transform"
  115 + @click="handleDeleteSelectionElements"
  116 + >
  117 + <Icon class="!text-3xl !text-light-50" icon="mdi:delete" />
  118 + </button>
  119 + <button
  120 + class="button-box-shadow w-14 h-14 flex justify-center items-center bg-gray-400 rounded-full opacity-50"
  121 + >
  122 + <Icon class="!text-3xl !text-light-50" icon="carbon:debug" />
  123 + </button>
  124 + <button
  125 + :class="changeMarker ? '!bg-orange-600 !opacity-100' : 'opacity-50'"
  126 + class="button-box-shadow w-14 h-14 flex justify-center items-center bg-gray-400 rounded-full"
  127 + @click="handleApplyChange(flowActionType)"
  128 + >
  129 + <Icon class="!text-3xl !text-light-50" icon="mdi:tick" />
  130 + </button>
  131 + <button
  132 + :class="changeMarker ? '!bg-orange-600 !opacity-100' : 'opacity-50'"
  133 + class="button-box-shadow w-14 h-14 flex justify-center items-center bg-gray-400 rounded-full"
  134 + @click="handleRedoChange(flowActionType)"
  135 + >
  136 + <Icon class="!text-3xl !text-light-50" icon="ic:baseline-close" />
  137 + </button>
  138 + </section>
  139 + </Panel>
  140 +
  141 + <Panel position="top-right">
  142 + <button
  143 + class="w-10 h-10 bg-gray-300 flex justify-center items-center rounded-full"
  144 + @click="handleFullScreen"
  145 + >
  146 + <Icon class="!text-2xl" :icon="getFullScreenIcon" />
  147 + </button>
  148 + </Panel>
  149 + </VueFlow>
  150 +
  151 + <CreateNodeModal ref="createNodeModalActionType" :get-container="handleGetContainer" />
  152 + <CreateEdgeModal ref="createEdgeModalActionType" :get-container="handleGetContainer" />
  153 +
  154 + <UpdateEdgeDrawer ref="updateEdgeDrawerActionType" />
  155 + <UpdateNodeDrawer ref="updateNodeDrawerActionType" />
  156 + </main>
  157 +</template>
  158 +
  159 +<style scoped>
  160 + .button-box-shadow {
  161 + box-shadow: 0 3px 5px -1px #0003, 0 6px 10px 0 #00000024, 0 1px 18px 0 #0000001f;
  162 + }
  163 +</style>
... ...
  1 +import { cloneDeep } from 'lodash-es';
  2 +import { PublicNodeItemClass } from '../../../types/node';
  3 +import type {
  4 + CategoryConfigType,
  5 + CreateComponentType,
  6 + NodeItemConfigType,
  7 +} from '../../../types/node';
  8 +import { InputConfig } from '.';
  9 +import { EntryCategoryConfig } from '..';
  10 +
  11 +export class Config extends PublicNodeItemClass implements CreateComponentType {
  12 + public config: NodeItemConfigType = cloneDeep(InputConfig);
  13 +
  14 + public categoryConfig: CategoryConfigType = cloneDeep(EntryCategoryConfig);
  15 +
  16 + constructor() {
  17 + super();
  18 + }
  19 +}
... ...
  1 +import { EntryComponentEnum } from '../../../enum/category';
  2 +import { useCreateNodeKey } from '../../../hook/useCreateNodeKey';
  3 +import type { NodeItemConfigType } from '../../../types/node';
  4 +import { RuleNodeTypeEnum } from '../../index.type';
  5 +
  6 +const keys = useCreateNodeKey(EntryComponentEnum.INPUT);
  7 +
  8 +export const InputConfig: NodeItemConfigType = {
  9 + ...keys,
  10 + categoryType: RuleNodeTypeEnum.ENTRY,
  11 + clazz: EntryComponentEnum.INPUT,
  12 + maxConnectionPoint: 1,
  13 + backgroundColor: '#95E898',
  14 + configurationDescriptor: {
  15 + nodeDefinition: {
  16 + icon: 'material-symbols:input-sharp',
  17 + outEnabled: true,
  18 + },
  19 + },
  20 +};
... ...
  1 +import type { CategoryConfigType } from '../../types/node';
  2 +import { RuleNodeTypeEnum } from '../index.type';
  3 +
  4 +export const EntryCategoryConfig: CategoryConfigType = {
  5 + category: RuleNodeTypeEnum.ENTRY,
  6 + backgroundColor: '#95E898',
  7 + title: '入口',
  8 + icon: 'material-symbols:input-sharp',
  9 + description: '使用配置条件筛选传入消息',
  10 +};
... ...
  1 +import { cloneDeep } from 'lodash-es';
  2 +import { PublicNodeItemClass } from '../../../types/node';
  3 +import type {
  4 + CategoryConfigType,
  5 + CreateComponentType,
  6 + NodeItemConfigType,
  7 +} from '../../../types/node';
  8 +import { FilterCategoryConfig } from '..';
  9 +import { CheckAlarmStatusConfig } from '.';
  10 +
  11 +export class Config extends PublicNodeItemClass implements CreateComponentType {
  12 + public config: NodeItemConfigType = cloneDeep(CheckAlarmStatusConfig);
  13 +
  14 + public categoryConfig: CategoryConfigType = cloneDeep(FilterCategoryConfig);
  15 +
  16 + constructor() {
  17 + super();
  18 + }
  19 +}
... ...
  1 +import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  2 +import { FormSchema } from '/@/components/Form';
  3 +import { AlarmStatus, AlarmStatusMean } from '/@/enums/alarmEnum';
  4 +
  5 +export const formSchemas: FormSchema[] = [
  6 + {
  7 + field: NodeBindDataFieldEnum.ALARM_STATUS_LIST,
  8 + component: 'Select',
  9 + label: NodeBindDataFieldNameEnum.ALARM_STATUS_LIST,
  10 + required: true,
  11 + componentProps: {
  12 + mode: 'multiple',
  13 + getPopupContainer: () => document.body,
  14 + placeholder: `请选择${NodeBindDataFieldNameEnum.ALARM_STATUS_LIST}`,
  15 + options: [
  16 + {
  17 + label: AlarmStatusMean[AlarmStatus.CLEARED_UN_ACK],
  18 + value: AlarmStatus.CLEARED_UN_ACK,
  19 + },
  20 + {
  21 + label: AlarmStatusMean[AlarmStatus.ACTIVE_UN_ACK],
  22 + value: AlarmStatus.ACTIVE_UN_ACK,
  23 + },
  24 + {
  25 + label: AlarmStatusMean[AlarmStatus.CLEARED_ACK],
  26 + value: AlarmStatus.CLEARED_ACK,
  27 + },
  28 + {
  29 + label: AlarmStatusMean[AlarmStatus.ACTIVE_ACK],
  30 + value: AlarmStatus.ACTIVE_ACK,
  31 + },
  32 + ],
  33 + },
  34 + },
  35 +];
... ...
  1 +<script lang="ts" setup>
  2 + import type { CreateModalDefineExposeType } from '../../../types';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { formSchemas } from './create.config';
  5 + import { NodeData } from '../../../types/node';
  6 +
  7 + defineProps<{
  8 + config: NodeData;
  9 + }>();
  10 +
  11 + const [register, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({
  12 + schemas: formSchemas,
  13 + showActionButtonGroup: false,
  14 + });
  15 +
  16 + const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => {
  17 + await validate();
  18 + const value = getFieldsValue() || {};
  19 + return value;
  20 + };
  21 +
  22 + const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => {
  23 + resetFields();
  24 + setFieldsValue(value);
  25 + };
  26 +
  27 + defineExpose({
  28 + setFieldsValue: setValue,
  29 + getFieldsValue: getValue,
  30 + } as CreateModalDefineExposeType);
  31 +</script>
  32 +
  33 +<template>
  34 + <BasicForm @register="register" />
  35 +</template>
... ...
  1 +import { FilterCategoryComponentEnum } from '../../../enum/category';
  2 +import { useCreateNodeKey } from '../../../hook/useCreateNodeKey';
  3 +import type { NodeItemConfigType } from '../../../types/node';
  4 +import { RuleNodeTypeEnum } from '../../index.type';
  5 +import { AlarmStatus } from '/@/enums/alarmEnum';
  6 +
  7 +const keys = useCreateNodeKey(FilterCategoryComponentEnum.CHECK_ALARM_STATUS);
  8 +
  9 +export interface CheckAlarmStatusDataType {
  10 + alarmStatusList: AlarmStatus[];
  11 +}
  12 +
  13 +export const CheckAlarmStatusConfig: NodeItemConfigType = {
  14 + ...keys,
  15 + clazz: 'org.thingsboard.rule.engine.filter.TbCheckAlarmStatusNode',
  16 + categoryType: RuleNodeTypeEnum.FILTER,
  17 + name: 'check alarm status',
  18 + backgroundColor: '#ede550',
  19 + configurationDescriptor: {
  20 + nodeDefinition: {
  21 + icon: 'tabler:circuit-ground',
  22 + details:
  23 + 'If the alarm status matches the specified one - msg is success if does not match - msg is failure.',
  24 + description: 'Checks alarm status.',
  25 + inEnabled: true,
  26 + outEnabled: true,
  27 + relationTypes: ['True', 'False', 'Failure'],
  28 + customRelations: false,
  29 + ruleChainNode: false,
  30 + defaultConfiguration: {
  31 + alarmStatusList: [AlarmStatus.ACTIVE_ACK, AlarmStatus.ACTIVE_UN_ACK],
  32 + },
  33 + },
  34 + },
  35 +};
... ...
  1 +import type { CategoryConfigType, NodeItemConfigType } from '../../types/node';
  2 +import { RuleNodeTypeEnum } from '../index.type';
  3 +import { CheckAlarmStatusConfig } from './CheckAlarmStatus';
  4 +
  5 +export const FilterCategoryConfig: CategoryConfigType = {
  6 + category: RuleNodeTypeEnum.FILTER,
  7 + backgroundColor: '#ede550',
  8 + title: '筛选器',
  9 + icon: 'tabler:circuit-ground',
  10 + description: '使用配置条件筛选传入消息',
  11 +};
  12 +
  13 +export const FilterComponents: NodeItemConfigType[] = [CheckAlarmStatusConfig];
... ...
  1 +import { BasicConnectionModalEnum } from '../enum';
  2 +import { FetchNodeComFlagTypeENum } from '../enum/node';
  3 +import type { CategoryConfigType, NodeItemConfigType } from '../types/node';
  4 +import { FilterCategoryConfig, FilterComponents } from './Filter';
  5 +import { RuleNodeTypeEnum } from './index.type';
  6 +
  7 +const createModules = import.meta.glob('../packages/**/create.vue');
  8 +const connectionModules = import.meta.glob('../packages/**/connection.vue');
  9 +
  10 +const BasicConnectionComponent = {
  11 + [BasicConnectionModalEnum.BASIC]: () =>
  12 + import('../src/components/CreateEdgeModal/BasicRelations.vue'),
  13 + [BasicConnectionModalEnum.CUSTOM]: () =>
  14 + import('../src/components/CreateEdgeModal/CustomRelations.vue'),
  15 +};
  16 +
  17 +export const fetchNodeComponent = (key: string, flag: FetchNodeComFlagTypeENum) => {
  18 + const modules =
  19 + flag === FetchNodeComFlagTypeENum.CONNECTION_MODAL ? connectionModules : createModules;
  20 + const modulesName = Object.keys(modules);
  21 + const path = modulesName.find((item) => {
  22 + const temp = item.split('/');
  23 + return temp[temp.length - 2] === key;
  24 + });
  25 + return path ? modules[path] : null;
  26 +};
  27 +
  28 +export const fetchConnectionComponent = async (config: NodeItemConfigType) => {
  29 + const { configurationDescriptor } = config;
  30 + const { nodeDefinition } = configurationDescriptor;
  31 + const { customRelations } = nodeDefinition;
  32 + return customRelations
  33 + ? BasicConnectionComponent[BasicConnectionModalEnum.CUSTOM]
  34 + : BasicConnectionComponent[BasicConnectionModalEnum.BASIC];
  35 +};
  36 +
  37 +export const fetchCreateComponent = async (config: NodeItemConfigType) => {
  38 + const { key } = config;
  39 + return fetchNodeComponent(key, FetchNodeComFlagTypeENum.CREATE_MODAL);
  40 +};
  41 +
  42 +export const allComponents: Record<
  43 + RuleNodeTypeEnum,
  44 + { category: CategoryConfigType; components: NodeItemConfigType[] }
  45 +> = {
  46 + [RuleNodeTypeEnum.FILTER]: {
  47 + category: FilterCategoryConfig,
  48 + components: FilterComponents,
  49 + },
  50 +};
... ...
  1 +export enum RuleNodeTypeEnum {
  2 + FILTER = 'FILTER',
  3 + ENTRY = 'ENTRY',
  4 +}
... ...
  1 +<script lang="ts" setup>
  2 + import { MarkerArrowEnum } from '../../../enum';
  3 +</script>
  4 +
  5 +<template>
  6 + <svg>
  7 + <defs>
  8 + <marker
  9 + :id="MarkerArrowEnum.BASIC_ARROW"
  10 + markerWidth="5"
  11 + markerHeight="5"
  12 + viewBox="-6 -6 12 12"
  13 + refX="5"
  14 + refY="0"
  15 + markerUnits="strokeWidth"
  16 + orient="auto"
  17 + >
  18 + <polygon points="-2,0 -5,5 5,0 -5,-5" stroke="gray" fill="gray" stroke-width="1px" />
  19 + </marker>
  20 + <marker
  21 + :id="MarkerArrowEnum.BASIC_ARROW_SELECTED"
  22 + markerWidth="5"
  23 + markerHeight="5"
  24 + viewBox="-6 -6 12 12"
  25 + refX="5"
  26 + refY="0"
  27 + markerUnits="strokeWidth"
  28 + orient="auto"
  29 + >
  30 + <polygon points="-2,0 -5,5 5,0 -5,-5" stroke="red" fill="red" stroke-width="1px" />
  31 + </marker>
  32 + </defs>
  33 + </svg>
  34 +</template>
... ...
  1 +<script setup lang="ts">
  2 + import { BaseEdge, ConnectionLineProps, getBezierPath } from '@vue-flow/core';
  3 + import { computed } from 'vue';
  4 +
  5 + type CustomConnectionLineProps = Partial<ConnectionLineProps>;
  6 +
  7 + const props = defineProps<CustomConnectionLineProps>();
  8 +
  9 + const path = computed(() => getBezierPath(props as unknown as Required<ConnectionLineProps>));
  10 +</script>
  11 +
  12 +<template>
  13 + <BaseEdge v-bind="props" :path="path[0]" :marker-end="markerEnd" />
  14 +</template>
... ...
  1 +<script setup lang="ts">
  2 + import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
  3 + import type { EdgeProps } from '@vue-flow/core';
  4 + import { computed, toRaw, unref } from 'vue';
  5 + import type { EdgeData, NodeData } from '../../../types/node';
  6 + import { useConnectionFocus } from '../../../hook/useConnectionFocus';
  7 + import { useFlowContext } from '../../../hook/useFlowContext';
  8 + import { useTextWidth } from '../../../hook/useTextWidth';
  9 +
  10 + const props = defineProps<EdgeProps>();
  11 +
  12 + const { flowActionType, updateEdgeDrawerActionType } = useFlowContext();
  13 +
  14 + const { getMarkerEnd, getSelected } = useConnectionFocus(props);
  15 +
  16 + const getData = computed(() => props.data as EdgeData);
  17 +
  18 + const getLabel = computed(() => {
  19 + let { data } = unref(getData);
  20 + const type = data?.type || [];
  21 + return type.join(' / ');
  22 + });
  23 +
  24 + const getLabelWidth = computed(() => useTextWidth(unref(getLabel), 14) + 10);
  25 +
  26 + const path = computed(() => getBezierPath(props));
  27 +
  28 + const handleOpenEdit = async () => {
  29 + const { flag, data } =
  30 + (await unref(updateEdgeDrawerActionType)?.open(
  31 + toRaw(unref(props.sourceNode?.data as unknown as NodeData)),
  32 + toRaw(unref(props.data as EdgeData))
  33 + )) || {};
  34 +
  35 + if (!flag) return;
  36 +
  37 + const currentEdge = flowActionType.findEdge(props.id);
  38 +
  39 + (currentEdge!.data as EdgeData).data = data;
  40 +
  41 + console.log(props);
  42 + };
  43 +
  44 + const handleDelete = () => {
  45 + flowActionType?.removeEdges(props.id);
  46 + };
  47 +</script>
  48 +
  49 +<template>
  50 + <BaseEdge
  51 + v-bind="props"
  52 + :path="path[0]"
  53 + :marker-end="getMarkerEnd"
  54 + :label="getLabel"
  55 + :label-x="path[1]"
  56 + :label-y="path[2]"
  57 + :label-style="{ fill: '#003a79', fontWeight: 700, fontSize: 14 }"
  58 + :label-show-bg="true"
  59 + :label-bg-style="{ fill: '#fff', stroke: '#003a79', strokeWidth: 2, width: getLabelWidth }"
  60 + :label-bg-padding="[5, 5]"
  61 + :label-bg-border-radius="10"
  62 + />
  63 + <EdgeLabelRenderer>
  64 + <section
  65 + :style="{
  66 + pointerEvents: 'all',
  67 + position: 'absolute',
  68 + transform: `translate(0, -3rem) translate(${path[1]}px,${path[2]}px)`,
  69 + }"
  70 + class="nodrag nopan label-container relative"
  71 + >
  72 + <div
  73 + v-if="getSelected"
  74 + class="actions flex absolute gap-1 -top-full transform translate-y-1 right w-16 h-7 justify-center"
  75 + >
  76 + <div
  77 + class="border-2 rounded-1 border-light-50 bg-red-500 w-7 h-7 flex justify-center items-center cursor-pointer"
  78 + @click="handleOpenEdit"
  79 + >
  80 + <Icon icon="mdi:lead-pencil" color="#fff" />
  81 + </div>
  82 + <div
  83 + class="border-2 rounded-1 border-light-50 bg-red-500 w-7 h-7 flex justify-center items-center cursor-pointer"
  84 + @click="handleDelete"
  85 + >
  86 + <Icon icon="mdi:close" color="#fff" />
  87 + </div>
  88 + </div>
  89 + </section>
  90 + </EdgeLabelRenderer>
  91 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import type { NodeProps } from '@vue-flow/core';
  3 + import { Handle, Position } from '@vue-flow/core';
  4 + import { computed, unref } from 'vue';
  5 + import { Tooltip } from 'ant-design-vue';
  6 + import { Icon } from '/@/components/Icon';
  7 + import type { NodeData, NodeDefinition } from '../../../types/node';
  8 + import BasicToolbar from './BasicToolbar.vue';
  9 +
  10 + const props = defineProps<NodeProps>();
  11 +
  12 + const getData = computed(() => props.data as NodeData);
  13 +
  14 + const getNodeDefinition = computed<NodeDefinition>(() => {
  15 + const { config } = unref(getData);
  16 + const { configurationDescriptor } = config || {};
  17 + const { nodeDefinition } = configurationDescriptor || {};
  18 + return nodeDefinition || {};
  19 + });
  20 +
  21 + const getHasInEnabled = computed(() => unref(getNodeDefinition).inEnabled);
  22 +
  23 + const getHasOutEnabled = computed(() => unref(getNodeDefinition).outEnabled);
  24 +
  25 + const getLabel = computed(() => {
  26 + const { config } = unref(getData);
  27 +
  28 + return config?.name;
  29 + });
  30 +
  31 + const getIcon = computed(() => {
  32 + const { icon } = unref(getNodeDefinition);
  33 + const { category } = unref(getData);
  34 + const { icon: categoryIcon } = category || {};
  35 + return icon || categoryIcon || 'tabler:circuit-ground';
  36 + });
  37 +
  38 + const getName = computed(() => {
  39 + const { data } = unref(getData);
  40 + return data?.name;
  41 + });
  42 +
  43 + const getBackgroundColor = computed(() => {
  44 + const { config, category } = unref(getData);
  45 + const { backgroundColor: categoryBackgroundColor } = category || {};
  46 + const { backgroundColor } = config || {};
  47 + return backgroundColor || categoryBackgroundColor;
  48 + });
  49 +</script>
  50 +
  51 +<template>
  52 + <Tooltip color="#fff" :mouse-enter-delay="0.5" placement="right">
  53 + <template #title>
  54 + <section class="text-dark-900 text-xs">
  55 + <p class="mb-0 font-bold"> {{ getData?.data?.name }} </p>
  56 + <p class="mb-0 mt-2 text-gray-500 italic">
  57 + {{ getData.category?.title }}
  58 + {{ getData?.category?.title && getData.config?.name ? '-' : '' }}
  59 + {{ getData.config?.name }}
  60 + </p>
  61 + <p class="mt-1 mb-0 text-gray-500">{{ getData.data?.description }}</p>
  62 + </section>
  63 + </template>
  64 + <main
  65 + class="basic-node-hover flex items-center w-44 h-12 rounded border px-4 py-2 border-gray-700"
  66 + :style="{ backgroundColor: getBackgroundColor }"
  67 + >
  68 + <div>
  69 + <Icon class="text-2xl" :icon="getIcon" />
  70 + </div>
  71 + <BasicToolbar v-bind="$props" />
  72 + <div class="flex text-xs flex-col ml-2 text-left truncate">
  73 + <span class="text-gray-700 w-full truncate">{{ getLabel }}</span>
  74 + <span class="w-full truncate">{{ getName }}</span>
  75 + </div>
  76 + <Handle v-if="getHasInEnabled" type="source" :position="Position.Left" />
  77 + <Handle v-if="getHasOutEnabled" type="source" :position="Position.Right" />
  78 + </main>
  79 + </Tooltip>
  80 +</template>
  81 +
  82 +<style lang="css" scoped>
  83 + .basic-node-hover::before {
  84 + content: '';
  85 + position: absolute;
  86 + width: 100%;
  87 + height: 100%;
  88 + left: 0;
  89 + display: none;
  90 + background-color: #00000027;
  91 + }
  92 +
  93 + .basic-node-hover:hover::before {
  94 + display: block;
  95 + }
  96 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { Icon } from '/@/components/Icon';
  3 + import { NodeProps, Position } from '@vue-flow/core';
  4 + import { NodeToolbar } from '@vue-flow/node-toolbar';
  5 + import { computed, toRaw, unref } from 'vue';
  6 + import { useFlowContext } from '../../../hook/useFlowContext';
  7 + import type { NodeData } from '../../../types/node';
  8 +
  9 + const props = defineProps<NodeProps>();
  10 +
  11 + const getData = computed(() => props.data as NodeData);
  12 +
  13 + const { updateNodeDrawerActionType, flowActionType } = useFlowContext();
  14 +
  15 + const handleOpenEdit = async () => {
  16 + const { flag, data } =
  17 + (await unref(updateNodeDrawerActionType)?.open(toRaw(unref(getData)))) || {};
  18 +
  19 + if (!flag) return;
  20 +
  21 + const currentNode = flowActionType?.findNode(props.id);
  22 +
  23 + (currentNode!.data as NodeData).data = data;
  24 + };
  25 +
  26 + const handleDelete = () => {
  27 + flowActionType?.removeNodes(props.id);
  28 + };
  29 +</script>
  30 +
  31 +<template>
  32 + <NodeToolbar :is-visible="data.toolbarVisible" :position="Position.Top" align="end" :offset="2">
  33 + <section class="flex gap-1 transform translate-y-1">
  34 + <div
  35 + class="border-2 rounded-1 border-light-50 bg-red-500 w-7 h-7 flex justify-center items-center cursor-pointer"
  36 + @click="handleOpenEdit"
  37 + >
  38 + <Icon icon="mdi:lead-pencil" color="#fff" />
  39 + </div>
  40 + <div
  41 + class="border-2 rounded-1 border-light-50 bg-red-500 w-7 h-7 flex justify-center items-center cursor-pointer"
  42 + @click="handleDelete"
  43 + >
  44 + <Icon icon="mdi:close" color="#fff" />
  45 + </div>
  46 + </section>
  47 + </NodeToolbar>
  48 +</template>
... ...
  1 +export { default as BasicEdge } from './BasicEdge.vue';
  2 +export { default as BasicNode } from './BasicNode.vue';
  3 +export { default as BasicConnectionLine } from './BasicConnectionLine.vue';
  4 +export { default as BasicConnectionArrow } from './BasicConnectionArrow.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { unref } from 'vue';
  3 + import { EdgeBindDataFieldEnum, EdgeBindDataFieldNameEnum } from '../../../enum/node';
  4 + import { ConnectionModalDefineExposeType } from '../../../types';
  5 + import { NodeData } from '../../../types/node';
  6 + import { BasicForm, FormSchema, useForm } from '/@/components/Form';
  7 +
  8 + defineProps<{
  9 + config: NodeData;
  10 + }>();
  11 +
  12 + const getSchemas = (options: string[] = []): FormSchema[] => {
  13 + return [
  14 + {
  15 + field: EdgeBindDataFieldEnum.TYPE,
  16 + component: 'Select',
  17 + label: EdgeBindDataFieldNameEnum.TYPE,
  18 + componentProps: {
  19 + getPopupContainer: () => document.body,
  20 + options: unref(options).map((item) => ({ label: item, value: item })),
  21 + mode: 'multiple',
  22 + placeholder: '请选择链接标签',
  23 + },
  24 + },
  25 + ];
  26 + };
  27 +
  28 + const [register, { updateSchema, setFieldsValue, getFieldsValue }] = useForm({
  29 + showActionButtonGroup: false,
  30 + schemas: getSchemas(),
  31 + });
  32 +
  33 + const getValue: ConnectionModalDefineExposeType['getFieldsValue'] = async () => {
  34 + return getFieldsValue();
  35 + };
  36 +
  37 + const setValue: ConnectionModalDefineExposeType['setFieldsValue'] = async (value, nodeData) => {
  38 + const { type } = value || {};
  39 + const relationTypes =
  40 + unref(nodeData)?.config?.configurationDescriptor.nodeDefinition.relationTypes;
  41 + await updateSchema(getSchemas(relationTypes));
  42 + setFieldsValue(type);
  43 + };
  44 +
  45 + defineExpose({
  46 + getFieldsValue: getValue,
  47 + setFieldsValue: setValue,
  48 + } as ConnectionModalDefineExposeType);
  49 +</script>
  50 +
  51 +<template>
  52 + <BasicForm @register="register" />
  53 +</template>
... ...
  1 +<script lang="ts" setup></script>
  2 +
  3 +<template>
  4 + <div>自定义</div>
  5 +</template>
... ...
  1 +export { default as CreateEdgeModal } from './index.vue';
  2 +export { default as BasicRelations } from './BasicRelations.vue';
  3 +export { default as CustomRelations } from './CustomRelations.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Spin, Empty } from 'ant-design-vue';
  3 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  4 + import { BasicModal } from '/@/components/Modal';
  5 + import { useAwaitPopupWindowBindData } from '../../../hook/useAwaitPopupWindowBindData';
  6 + import { ElementsTypeEnum } from '../../../enum';
  7 +
  8 + defineProps<{
  9 + getContainer: () => any;
  10 + }>();
  11 +
  12 + const {
  13 + visible,
  14 + nodeData,
  15 + spinning,
  16 + shadowComponent,
  17 + getComponentKey,
  18 + createComponentEl,
  19 + open,
  20 + handleSubmit,
  21 + handleCancel,
  22 + handleOnMounted,
  23 + } = useAwaitPopupWindowBindData({ type: ElementsTypeEnum.EDGE, mode: DataActionModeEnum.CREATE });
  24 +
  25 + defineExpose({ open });
  26 +</script>
  27 +
  28 +<template>
  29 + <BasicModal
  30 + v-model:visible="visible"
  31 + title="添加链接"
  32 + :get-container="getContainer"
  33 + @ok="handleSubmit"
  34 + @cancel="handleCancel"
  35 + >
  36 + <Spin :spinning="spinning">
  37 + <component
  38 + :is="shadowComponent"
  39 + v-if="shadowComponent"
  40 + ref="createComponentEl"
  41 + :config="nodeData"
  42 + :key="getComponentKey"
  43 + @vue:mounted="handleOnMounted"
  44 + />
  45 + <Empty v-if="!shadowComponent" description="未找到链接组件" />
  46 + </Spin>
  47 + </BasicModal>
  48 +</template>
... ...
  1 +import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node';
  2 +import { FormSchema } from '/@/components/Form';
  3 +
  4 +export const TopFormSchemas: FormSchema[] = [
  5 + {
  6 + field: NodeBindDataFieldEnum.NAME,
  7 + component: 'Input',
  8 + label: NodeBindDataFieldNameEnum.NAME,
  9 + required: true,
  10 + colProps: {
  11 + span: 16,
  12 + },
  13 + componentProps: {
  14 + placeholder: `请输入${NodeBindDataFieldNameEnum.NAME}`,
  15 + },
  16 + },
  17 + {
  18 + field: NodeBindDataFieldEnum.DEBUG_MODE,
  19 + component: 'Checkbox',
  20 + label: NodeBindDataFieldNameEnum.DEBUG_MODE,
  21 + colProps: {
  22 + offset: 2,
  23 + span: 6,
  24 + },
  25 + itemProps: {
  26 + labelCol: { span: 14 },
  27 + wrapperCol: { span: 10 },
  28 + },
  29 + componentProps: {
  30 + placeholder: `请输入${NodeBindDataFieldNameEnum.NAME}`,
  31 + },
  32 + },
  33 +];
  34 +
  35 +export const BottomFormSchemas: FormSchema[] = [
  36 + {
  37 + field: NodeBindDataFieldEnum.DESCRIPTION,
  38 + component: 'InputTextArea',
  39 + label: NodeBindDataFieldNameEnum.DESCRIPTION,
  40 + componentProps: {
  41 + placeholder: `请输入${NodeBindDataFieldNameEnum.DESCRIPTION}`,
  42 + autoSize: true,
  43 + },
  44 + },
  45 +];
... ...
  1 +export { default as CreateNodeModal } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Empty, Spin } from 'ant-design-vue';
  3 + import { BasicModal } from '/@/components/Modal';
  4 + import { useAwaitPopupWindowBindData } from '../../../hook/useAwaitPopupWindowBindData';
  5 + import { ElementsTypeEnum } from '../../../enum';
  6 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  7 + import { useForm, BasicForm } from '/@/components/Form';
  8 + import { BottomFormSchemas, TopFormSchemas } from './config';
  9 +
  10 + defineProps<{
  11 + getContainer: () => any;
  12 + }>();
  13 +
  14 + const [topFormRegister, topFormActionType] = useForm({
  15 + schemas: TopFormSchemas,
  16 + showActionButtonGroup: false,
  17 + });
  18 +
  19 + const [bottomFormRegister, bottomFormActionType] = useForm({
  20 + schemas: BottomFormSchemas,
  21 + showActionButtonGroup: false,
  22 + });
  23 +
  24 + const {
  25 + visible,
  26 + spinning,
  27 + nodeData,
  28 + shadowComponent,
  29 + getComponentKey,
  30 + createComponentEl,
  31 + open,
  32 + handleSubmit,
  33 + handleCancel,
  34 + handleOnMounted,
  35 + } = useAwaitPopupWindowBindData({ type: ElementsTypeEnum.NODE, mode: DataActionModeEnum.CREATE });
  36 +
  37 + const validate = async () => {
  38 + await topFormActionType.validate();
  39 + await bottomFormActionType.validate();
  40 + };
  41 +
  42 + const getFieldsValue = () => {
  43 + const topValue = topFormActionType.getFieldsValue() || {};
  44 + const bottomValue = bottomFormActionType.getFieldsValue() || {};
  45 + return {
  46 + ...topValue,
  47 + ...bottomValue,
  48 + };
  49 + };
  50 +
  51 + const resetFieldsValue = () => {
  52 + topFormActionType.resetFields();
  53 + bottomFormActionType.resetFields();
  54 + };
  55 +
  56 + const handleModalOk = async () => {
  57 + await validate();
  58 + const data = getFieldsValue();
  59 + await handleSubmit(data);
  60 + resetFieldsValue();
  61 + };
  62 +
  63 + defineExpose({ open });
  64 +</script>
  65 +
  66 +<template>
  67 + <BasicModal
  68 + v-model:visible="visible"
  69 + :title="`添加规则节点: ${nodeData?.config?.name}`"
  70 + :get-container="getContainer"
  71 + @ok="handleModalOk"
  72 + @cancel="handleCancel"
  73 + >
  74 + <Spin :spinning="spinning">
  75 + <section v-if="shadowComponent" class="w-full h-full">
  76 + <BasicForm @register="topFormRegister" />
  77 + <component
  78 + :is="shadowComponent"
  79 + ref="createComponentEl"
  80 + :key="getComponentKey"
  81 + :config="nodeData"
  82 + @vue:mounted="handleOnMounted"
  83 + />
  84 + <BasicForm @register="bottomFormRegister" />
  85 + </section>
  86 + <Empty v-if="!shadowComponent" description="未找到创建组件" />
  87 + </Spin>
  88 + </BasicModal>
  89 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { Collapse, Tooltip } from 'ant-design-vue';
  3 + import { computed, ref } from 'vue';
  4 + import { allComponents } from '../../../packages';
  5 + import { RuleNodeTypeEnum } from '../../../packages/index.type';
  6 + import ItemPanel from './ItemPanel.vue';
  7 + import { Icon } from '/@/components/Icon';
  8 +
  9 + const props = defineProps<{
  10 + searchText?: string;
  11 + }>();
  12 +
  13 + const activeKey = ref(Object.values(RuleNodeTypeEnum));
  14 +
  15 + const getCategory = computed(() => {
  16 + const { searchText } = props;
  17 + let components = Object.values(allComponents);
  18 + searchText &&
  19 + (components = components.map((item) => ({
  20 + ...item,
  21 + components: item.components.filter((temp) => temp.name.includes(searchText)),
  22 + })));
  23 + return components;
  24 + });
  25 +</script>
  26 +
  27 +<template>
  28 + <Collapse v-model:activeKey="activeKey" class="rule-chain-sidebar">
  29 + <Collapse.Panel v-for="item in getCategory" :key="item.category.category" :show-arrow="false">
  30 + <template #header>
  31 + <Tooltip placement="right" color="#fff">
  32 + <template #title>
  33 + <p class="text-dark-900 font-bold text-xs m-0 py-1">
  34 + {{ item.category.title }}
  35 + </p>
  36 + <p class="text-gray-500 text-xs m-0 py-1">
  37 + {{ item.category.description }}
  38 + </p>
  39 + </template>
  40 + <div class="flex items-center">
  41 + <Icon class="text-2xl" icon="tabler:circuit-ground" />
  42 + <div class="flex-1 ml-2 select-none">
  43 + {{ item.category.title }}
  44 + </div>
  45 + <Icon
  46 + class="text-2xl transform transition-transform"
  47 + icon="ep:arrow-down"
  48 + :class="activeKey.includes(item.category.category!) ? '' : 'rotate-180'"
  49 + />
  50 + </div>
  51 + </Tooltip>
  52 + </template>
  53 + <ItemPanel :components="item.components" :category="item.category" />
  54 + </Collapse.Panel>
  55 + </Collapse>
  56 +</template>
  57 +
  58 +<style scoped>
  59 + .rule-chain-sidebar {
  60 + background-color: transparent !important;
  61 + }
  62 +
  63 + .rule-chain-sidebar:deep(.ant-collapse-content-box) {
  64 + padding: 0;
  65 + }
  66 +
  67 + .rule-chain-sidebar:deep(.ant-collapse-header) {
  68 + background-color: #e6e6e6 !important;
  69 + }
  70 +
  71 + .rule-chain-sidebar:deep(.ant-collapse-content) {
  72 + background-color: transparent;
  73 + }
  74 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import type { CategoryConfigType, NodeItemConfigType } from '../../../types/node';
  3 + import SidebarNodeItem from './SidebarNodeItem.vue';
  4 +
  5 + defineProps<{
  6 + category?: CategoryConfigType;
  7 + components?: NodeItemConfigType[];
  8 + }>();
  9 +</script>
  10 +
  11 +<template>
  12 + <section
  13 + class="rule-designer-collapse-container w-full flex items-center flex-col gap-2 relative"
  14 + :class="components?.length ? 'p-4' : 'p-0'"
  15 + >
  16 + <SidebarNodeItem
  17 + v-for="item in components"
  18 + :key="item.key"
  19 + :config="item"
  20 + :category="category"
  21 + />
  22 + </section>
  23 +</template>
  24 +
  25 +<style scoped>
  26 + .rule-designer-collapse-container::after {
  27 + position: absolute;
  28 + content: '';
  29 + width: 100%;
  30 + height: 100%;
  31 + z-index: 1;
  32 + top: 0;
  33 + background-color: #fff;
  34 + }
  35 +</style>
... ...
  1 +<script lang="ts" setup>
  2 + import { Icon } from '/@/components/Icon';
  3 + import { computed, ref, toRaw, unref } from 'vue';
  4 + import { Tooltip } from 'ant-design-vue';
  5 + import type { NodeData } from '../../../types/node';
  6 + import { EFFECT_SYMBOL, TRANSFER_DATA_KEY } from '../../../hook/useDragCreate';
  7 +
  8 + const props = defineProps<NodeData>();
  9 +
  10 + const visible = ref(false);
  11 +
  12 + const getNodeDefinition = computed(() => {
  13 + const { config } = props;
  14 + const { configurationDescriptor } = config || {};
  15 + const { nodeDefinition } = configurationDescriptor || {};
  16 + return nodeDefinition || {};
  17 + });
  18 +
  19 + const getIcon = computed(() => {
  20 + const { icon } = unref(getNodeDefinition);
  21 + const { icon: categoryIcon } = props.category || {};
  22 + return icon || categoryIcon || 'tabler:circuit-ground';
  23 + });
  24 +
  25 + const getBackgroundColor = computed(() => {
  26 + const { config, category } = props;
  27 + const { backgroundColor } = config || {};
  28 + const { backgroundColor: categoryBackgroundColor } = category || {};
  29 + return backgroundColor || categoryBackgroundColor;
  30 + });
  31 +
  32 + const handleOnDragStart = (event: DragEvent, options: object) => {
  33 + if (event.dataTransfer) {
  34 + event.dataTransfer.setData(
  35 + TRANSFER_DATA_KEY,
  36 + JSON.stringify({ offsetX: event.offsetX, offsetY: event.offsetY, options })
  37 + );
  38 + event.dataTransfer.effectAllowed = EFFECT_SYMBOL;
  39 + }
  40 + };
  41 +
  42 + const handleDragStart = (event: DragEvent) => {
  43 + visible.value = false;
  44 + handleOnDragStart(event, toRaw(unref(props)));
  45 + };
  46 +</script>
  47 +
  48 +<template>
  49 + <Tooltip
  50 + v-model:visible="visible"
  51 + placement="left"
  52 + color="#fff"
  53 + trigger="hover"
  54 + :mouse-enter-delay="0.3"
  55 + >
  56 + <template #title>
  57 + <section class="text-dark-900">
  58 + <h1 class="font-bold">
  59 + {{ config?.name }}
  60 + </h1>
  61 + <p class="italic text-gray-600">
  62 + {{ getNodeDefinition.description }}
  63 + </p>
  64 + <p>{{ getNodeDefinition.details }}</p>
  65 + </section>
  66 + </template>
  67 + <main
  68 + :draggable="true"
  69 + class="sidebar-basic-node flex items-center cursor-pointer w-44 h-12 rounded border relative px-4 py-2 border-gray-700 z-10"
  70 + :style="{ backgroundColor: getBackgroundColor }"
  71 + @dragstart="handleDragStart"
  72 + >
  73 + <div>
  74 + <Icon class="text-2xl" :icon="getIcon" />
  75 + </div>
  76 + <div class="flex text-xs flex-col ml-2 text-left">
  77 + <span class="text-gray-700 w-full truncate">{{ config?.name }}</span>
  78 + <!-- <span class="w-full truncate">name</span> -->
  79 + </div>
  80 + <div class="w-4 h-4 bg-dark-50 rounded-1 border absolute -left-2 border-light-50"></div>
  81 + <div class="w-4 h-4 bg-dark-50 rounded-1 border absolute -right-2 border-light-50"></div>
  82 + </main>
  83 + </Tooltip>
  84 +</template>
  85 +
  86 +<style lang="css" scoped>
  87 + .sidebar-basic-node::before {
  88 + content: '';
  89 + position: absolute;
  90 + width: 100%;
  91 + height: 100%;
  92 + left: 0;
  93 + display: none;
  94 + background-color: #00000027;
  95 + }
  96 +
  97 + .sidebar-basic-node:hover::before {
  98 + display: block;
  99 + }
  100 +</style>
... ...
  1 +export { default as Sidebar } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { Icon } from '/@/components/Icon';
  3 + import type { CSSProperties } from 'vue';
  4 + import { computed, ref, unref } from 'vue';
  5 + import { Tooltip, Input } from 'ant-design-vue';
  6 + import CategoryPanel from './CategoryPanel.vue';
  7 +
  8 + const expand = ref(true);
  9 +
  10 + const searchText = ref('');
  11 +
  12 + const getSidebarStyle = computed<CSSProperties>(() => {
  13 + return {
  14 + flex: `0 0 ${unref(expand) ? '250px' : 0}`,
  15 + width: unref(expand) ? '250px' : 0,
  16 + };
  17 + });
  18 +
  19 + const handleExpandMenu = () => {
  20 + expand.value = !unref(expand);
  21 + };
  22 +</script>
  23 +
  24 +<template>
  25 + <main
  26 + class="rule-designer-sidebar z-50 shadow shadow-lg shadow-dark-600 transition-all overflow-hidden relative"
  27 + :style="getSidebarStyle"
  28 + >
  29 + <section class="bg-sky-900 text-light-50 px-2 h-12 flex items-center">
  30 + <Tooltip title="查找节点">
  31 + <div
  32 + class="w-8 h-8 cursor-pointer flex justify-center items-center rounded-1 hover:bg-opacity-90"
  33 + >
  34 + <Icon icon="ion:search" class="text-xl" />
  35 + </div>
  36 + </Tooltip>
  37 + <div>
  38 + <Input
  39 + v-model:value="searchText"
  40 + placeholder="查找节点"
  41 + style="border-bottom: 1px solid #fff !important"
  42 + class="!border-none !border-light-50 !outline-none !bg-transparent !focus:border-none !focus:shadow-none !text-light-50"
  43 + />
  44 + </div>
  45 + <Tooltip title="关闭">
  46 + <div
  47 + class="w-8 h-8 cursor-pointer flex justify-center items-center rounded-1 hover:bg-opacity-90"
  48 + >
  49 + <Icon icon="uiw:left" class="text-2xl" @click="handleExpandMenu" />
  50 + </div>
  51 + </Tooltip>
  52 + </section>
  53 + <CategoryPanel :search-text="searchText" />
  54 + </main>
  55 + <Tooltip title="打开节点库">
  56 + <div
  57 + class="absolute top-2 left-2 z-20 bg-sky-800 w-10 h-10 cursor-pointer flex justify-center items-center rounded-1 opacity-50"
  58 + @click="handleExpandMenu"
  59 + >
  60 + <Icon icon="ion:menu-sharp" class="!text-2xl !text-light-50" />
  61 + </div>
  62 + </Tooltip>
  63 +</template>
  64 +
  65 +<style scoped>
  66 + .rule-designer-sidebar::after {
  67 + content: '';
  68 + position: absolute;
  69 + width: 100%;
  70 + height: 100%;
  71 + background: #fff;
  72 + }
  73 +</style>
... ...
  1 +export { default as UpdateEdgeDrawer } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicDrawer } from '/@/components/Drawer';
  3 + import { Spin, Empty } from 'ant-design-vue';
  4 + import { useAwaitPopupWindowBindData } from '../../../hook/useAwaitPopupWindowBindData';
  5 + import { ElementsTypeEnum } from '../../../enum';
  6 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  7 +
  8 + const {
  9 + visible,
  10 + spinning,
  11 + nodeData,
  12 + createComponentEl,
  13 + shadowComponent,
  14 + getComponentKey,
  15 + open,
  16 + handleCancel,
  17 + handleSubmit,
  18 + handleOnMounted,
  19 + } = useAwaitPopupWindowBindData({ mode: DataActionModeEnum.UPDATE, type: ElementsTypeEnum.EDGE });
  20 +
  21 + defineExpose({
  22 + open,
  23 + });
  24 +</script>
  25 +
  26 +<template>
  27 + <BasicDrawer
  28 + v-model:visible="visible"
  29 + width="700px"
  30 + showFooter
  31 + showOkBtn
  32 + showCancelBtn
  33 + @ok="handleSubmit"
  34 + @close="handleCancel"
  35 + >
  36 + <template #title>
  37 + <h2 class="font-bold text-2xl truncate">{{ nodeData?.data?.name }}</h2>
  38 + <p class="mb-0 text-gray-700">规则节点详情</p>
  39 + </template>
  40 + <Spin :spinning="spinning">
  41 + <component
  42 + :is="shadowComponent"
  43 + v-if="shadowComponent"
  44 + ref="createComponentEl"
  45 + :config="nodeData"
  46 + :key="getComponentKey"
  47 + @vue:mounted="handleOnMounted"
  48 + />
  49 + <Empty v-if="!shadowComponent" description="未找到链接组件" />
  50 + </Spin>
  51 + </BasicDrawer>
  52 +</template>
... ...
  1 +<script lang="ts" setup></script>
  2 +
  3 +<template>
  4 + <div>事件统计</div>
  5 +</template>
... ...
  1 +<script lang="ts" setup>
  2 + import { NodeData } from '../../../types/node';
  3 +
  4 + defineProps<{ nodeData?: NodeData }>();
  5 +</script>
  6 +
  7 +<template>
  8 + <section>
  9 + <h1 class="font-bold text-lg">{{ nodeData?.config?.name }}</h1>
  10 + <p class="text-gray-600 italic">
  11 + {{ nodeData?.config?.configurationDescriptor.nodeDefinition.description }}</p
  12 + >
  13 + <p>{{ nodeData?.config?.configurationDescriptor.nodeDefinition.details }}</p>
  14 + </section>
  15 +</template>
... ...
  1 +export enum TabsPanelEnum {
  2 + DETAIL = 'DETAIL',
  3 + EVENT = 'EVENT',
  4 + HELP = 'HELP',
  5 +}
  6 +
  7 +export enum TabsPanelNameEnum {
  8 + DETAIL = '详情',
  9 + EVENT = '事件',
  10 + HELP = '帮助',
  11 +}
... ...
  1 +export { default as UpdateNodeDrawer } from './index.vue';
... ...
  1 +<script lang="ts" setup>
  2 + import { BasicDrawer } from '/@/components/Drawer';
  3 + import { Tabs, Spin, Empty } from 'ant-design-vue';
  4 + import { TabsPanelEnum, TabsPanelNameEnum } from './config';
  5 + import { useAwaitPopupWindowBindData } from '../../../hook/useAwaitPopupWindowBindData';
  6 + import HelpMessage from './HelpMessage.vue';
  7 + import EventTotal from './EventTotal.vue';
  8 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  9 + import { ElementsTypeEnum } from '../../../enum';
  10 + import { useForm, BasicForm } from '/@/components/Form';
  11 + import { BottomFormSchemas, TopFormSchemas } from '../CreateNodeModal/config';
  12 + import { toRaw, unref } from 'vue-demi';
  13 +
  14 + const [topFormRegister, topFormActionType] = useForm({
  15 + schemas: TopFormSchemas,
  16 + showActionButtonGroup: false,
  17 + });
  18 +
  19 + const [bottomFormRegister, bottomFormActionType] = useForm({
  20 + schemas: BottomFormSchemas,
  21 + showActionButtonGroup: false,
  22 + });
  23 +
  24 + const {
  25 + visible,
  26 + spinning,
  27 + nodeData,
  28 + getComponentKey,
  29 + shadowComponent,
  30 + createComponentEl,
  31 + open,
  32 + handleCancel,
  33 + handleSubmit,
  34 + handleOnMounted,
  35 + } = useAwaitPopupWindowBindData({ mode: DataActionModeEnum.UPDATE, type: ElementsTypeEnum.NODE });
  36 +
  37 + const handleTopFormMounted = () => {
  38 + const data = unref(nodeData)?.data || {};
  39 + topFormActionType.setFieldsValue(toRaw(unref(data)));
  40 + };
  41 +
  42 + const handleBottomFormMounted = () => {
  43 + const data = unref(nodeData)?.data || {};
  44 + bottomFormActionType.setFieldsValue(toRaw(unref(data)));
  45 + };
  46 +
  47 + const validate = async () => {
  48 + await topFormActionType.validate();
  49 + await bottomFormActionType.validate();
  50 + };
  51 +
  52 + const getFieldsValue = () => {
  53 + const topValue = topFormActionType.getFieldsValue() || {};
  54 + const bottomValue = bottomFormActionType.getFieldsValue() || {};
  55 + return {
  56 + ...topValue,
  57 + ...bottomValue,
  58 + };
  59 + };
  60 +
  61 + const resetFieldsValue = () => {
  62 + topFormActionType.resetFields();
  63 + bottomFormActionType.resetFields();
  64 + };
  65 +
  66 + const handleModalOk = async () => {
  67 + await validate();
  68 + const data = getFieldsValue();
  69 + await handleSubmit(data);
  70 + resetFieldsValue();
  71 + };
  72 +
  73 + defineExpose({
  74 + open,
  75 + });
  76 +</script>
  77 +
  78 +<template>
  79 + <BasicDrawer
  80 + v-model:visible="visible"
  81 + width="700px"
  82 + showFooter
  83 + showCancelBtn
  84 + showOkBtn
  85 + @ok="handleModalOk"
  86 + @close="handleCancel"
  87 + >
  88 + <template #title>
  89 + <h2 class="font-bold text-2xl truncate">{{ nodeData?.data?.name }}</h2>
  90 + <p class="mb-0 text-gray-700">
  91 + <span> {{ nodeData?.category?.title }}</span>
  92 + <span class="mx-1">-</span>
  93 + <span>{{ nodeData?.config?.name }}</span>
  94 + </p>
  95 + </template>
  96 + <Tabs>
  97 + <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.DETAIL]" :key="TabsPanelEnum.DETAIL">
  98 + <Spin :spinning="spinning">
  99 + <section v-if="shadowComponent" class="w-full h-full">
  100 + <BasicForm @register="topFormRegister" @vue:mounted="handleTopFormMounted" />
  101 + <component
  102 + :is="shadowComponent"
  103 + ref="createComponentEl"
  104 + :config="nodeData"
  105 + :key="getComponentKey"
  106 + @vue:mounted="handleOnMounted"
  107 + />
  108 + <BasicForm @register="bottomFormRegister" @vue:mounted="handleBottomFormMounted" />
  109 + </section>
  110 + <Empty v-if="!shadowComponent" description="未找到链接组件" />
  111 + </Spin>
  112 + </Tabs.TabPane>
  113 + <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.EVENT]" :key="TabsPanelEnum.EVENT">
  114 + <EventTotal />
  115 + </Tabs.TabPane>
  116 + <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.HELP]" :key="TabsPanelEnum.HELP">
  117 + <HelpMessage :nodeData="nodeData" />
  118 + </Tabs.TabPane>
  119 + </Tabs>
  120 + </BasicDrawer>
  121 +</template>
... ...
  1 +export * from './Basic';
  2 +export * from './Sidebar';
... ...
  1 +import '@vue-flow/core/dist/style.css';
  2 +import '@vue-flow/core/dist/theme-default.css';
  3 +import '@vue-flow/controls/dist/style.css';
  4 +import './override.css';
... ...
  1 +.vue-flow__handle {
  2 + width: 16px;
  3 + height: 16px;
  4 +}
  5 +
  6 +.vue-flow__handle-right {
  7 + right: -8px;
  8 +}
  9 +
  10 +.vue-flow__handle-left {
  11 + left: -8px;
  12 +}
  13 +
  14 +.vue-flow__background.vue-flow__container {
  15 + background-color: #e9e9e9;
  16 +}
  17 +
  18 +.vue-flow__edge-path {
  19 + stroke-width: 4;
  20 + stroke: gray;
  21 + transition: stroke-width 0.3s;
  22 +}
  23 +
  24 +.connection-focus {
  25 + stroke-width: 6;
  26 +}
  27 +
  28 +/* 连接线选中时 颜色改变 */
  29 +.vue-flow__edge.selected > .vue-flow__edge-path {
  30 + stroke: red !important;
  31 +}
  32 +
  33 +/* 连接线移入放大效果 */
  34 +.vue-flow__edge:hover > .vue-flow__edge-path {
  35 + stroke-width: 6 !important;
  36 +}
  37 +
  38 +.vue-flow__edge-textbg,
  39 +.vue-flow__edge-text {
  40 + transition: transform 0.2s;
  41 +}
  42 +
  43 +.vue-flow__edge:hover .vue-flow__edge-textbg {
  44 + transform: scale(1.1)
  45 +}
  46 +
  47 +.vue-flow__edge:hover .vue-flow__edge-text{
  48 + transform: scale(1.1)
  49 +}
  50 +
  51 +/* selection 选择框 */
  52 +.vue-flow__nodesselection-rect, .vue-flow__selection{
  53 + border-width: 3px;
  54 +}
... ...
  1 +import type { VueFlow } from '@vue-flow/core';
  2 +
  3 +export type FlowElRef = InstanceType<typeof VueFlow>;
... ...
  1 +import type { RuleError } from 'ant-design-vue/lib/form/interface';
  2 +import { DefaultEdgeConfig, DefaultNodeConfig, NodeData } from './node';
  3 +
  4 +export interface AwaitPopupWindowReturnDataType<T = any> {
  5 + flag: boolean;
  6 + data: Nullable<T>;
  7 +}
  8 +
  9 +export interface BasicSetFieldValueType<D = DefaultNodeConfig, C = Recordable> {
  10 + defaultConfig: D;
  11 + config: C;
  12 +}
  13 +
  14 +export interface CreateModalDefineExposeType {
  15 + validate?: () => Promise<{ flag: boolean; error?: RuleError }>;
  16 + getFieldsValue?: () => Promise<Recordable>;
  17 + setFieldsValue?: (value?: DefaultNodeConfig, nodeData?: NodeData) => void;
  18 +}
  19 +
  20 +export interface CreateModalDefineEmitType {
  21 + (eventName: 'mounted'): void;
  22 +}
  23 +
  24 +export interface ConnectionModalReturnDataType<T = string | string[]> {
  25 + flag: boolean;
  26 + data: Nullable<T>;
  27 +}
  28 +
  29 +export interface ConnectionModalDefineExposeType {
  30 + validate?: () => Promise<{ flag: boolean; error?: RuleError }>;
  31 + getFieldsValue?: () => Promise<Recordable>;
  32 + setFieldsValue?: (value: DefaultEdgeConfig, nodeData?: NodeData) => void;
  33 +}
... ...
  1 +import type { ValidConnectionFunc } from '@vue-flow/core';
  2 +import type { RuleNodeTypeEnum } from '../packages/index.type';
  3 +
  4 +export interface NodeItemConfigType {
  5 + /**
  6 + * @description 组件key
  7 + */
  8 + key: string;
  9 +
  10 + /**
  11 + * @description 创建组件
  12 + */
  13 + createComponentKey?: string;
  14 +
  15 + /**
  16 + * @description 连接组件
  17 + */
  18 + connectionComponentKey?: string;
  19 +
  20 + /**
  21 + * @description 背景颜色
  22 + */
  23 + backgroundColor?: string;
  24 +
  25 + /**
  26 + * @description 连接校验
  27 + */
  28 + validateConnection?: ValidConnectionFunc;
  29 +
  30 + /**
  31 + * @description 类别
  32 + */
  33 + categoryType: string;
  34 +
  35 + type: string;
  36 +
  37 + /**
  38 + * @description 归属
  39 + */
  40 + scope?: string;
  41 +
  42 + /**
  43 + * @description 节点名称
  44 + */
  45 + name: string;
  46 +
  47 + clazz: string;
  48 +
  49 + /**
  50 + * @description 最大连接点
  51 + */
  52 + maxConnectionPoint?: number;
  53 +
  54 + /**
  55 + * @description 配置描述
  56 + */
  57 + configurationDescriptor: ConfigurationDescriptor;
  58 +}
  59 +
  60 +export interface CategoryConfigType {
  61 + /**
  62 + * @description 类别
  63 + */
  64 + category?: RuleNodeTypeEnum;
  65 +
  66 + /**
  67 + * @description 背景颜色
  68 + */
  69 + backgroundColor?: string;
  70 +
  71 + /**
  72 + * @description icon
  73 + */
  74 + icon?: string;
  75 +
  76 + /**
  77 + * @description 标题
  78 + */
  79 + title?: string;
  80 +
  81 + /**
  82 + * @description 连接校验
  83 + */
  84 + validateConnection?: ValidConnectionFunc;
  85 +
  86 + /**
  87 + * @description 描述
  88 + */
  89 + description?: string;
  90 +}
  91 +
  92 +export interface PublicNodeItemType {
  93 + id?: string;
  94 +}
  95 +
  96 +export class PublicNodeItemClass implements PublicNodeItemType {
  97 + constructor() {}
  98 +}
  99 +
  100 +export interface CreateComponentType {
  101 + config: NodeItemConfigType;
  102 + categoryConfig: CategoryConfigType;
  103 +}
  104 +
  105 +export interface ConfigurationDescriptor {
  106 + nodeDefinition: NodeDefinition;
  107 +}
  108 +
  109 +export interface NodeDefinition<T = object> {
  110 + details?: string;
  111 + description?: string;
  112 + inEnabled?: boolean;
  113 + outEnabled?: boolean;
  114 + relationTypes?: string[];
  115 + customRelations?: boolean;
  116 + ruleChainNode?: boolean;
  117 + defaultConfiguration?: T;
  118 + uiResources?: string[];
  119 + configDirective?: string;
  120 + icon?: string;
  121 + iconUrl?: string;
  122 + docUrl?: string;
  123 +}
  124 +
  125 +export interface DragTransferData {
  126 + options: NodeData;
  127 + offsetX: number;
  128 + offsetY: number;
  129 +}
  130 +
  131 +export interface NodeData<T = BasicNodeFormData> {
  132 + category?: CategoryConfigType;
  133 + config?: NodeItemConfigType;
  134 + data?: T;
  135 +}
  136 +
  137 +export interface EdgeData {
  138 + config: DefaultEdgeConfig;
  139 + data?: BasicEdgeBindData;
  140 +}
  141 +
  142 +export interface BasicNodeBindData {
  143 + id?: { entityType: string; id: string };
  144 + ruleChainId?: { entityType: string; id: string };
  145 + additionalInfo?: AdditionalInfo;
  146 + type?: string;
  147 + name?: string;
  148 + debugMode?: boolean;
  149 + configuration?: Configuration;
  150 +}
  151 +
  152 +export interface AdditionalInfo {
  153 + description?: string;
  154 + layoutX: number;
  155 + layoutY: number;
  156 +}
  157 +
  158 +export interface Configuration {
  159 + alarmStatusList?: string[];
  160 + checkForSingleEntity?: boolean;
  161 + direction?: string;
  162 + entityId?: string;
  163 + entityType?: string;
  164 + relationType?: string;
  165 +}
  166 +
  167 +export interface DefaultNodeConfig {
  168 + alarmStatusList?: string[];
  169 +}
  170 +
  171 +export interface DefaultEdgeConfig {
  172 + type?: string[];
  173 +}
  174 +
  175 +export interface BasicEdgeBindData {
  176 + type?: string[];
  177 +}
  178 +
  179 +export interface BasicNodeFormData {
  180 + configuration?: Recordable;
  181 + description?: string;
  182 + name?: string;
  183 + debugMode?: boolean;
  184 +}
... ...
  1 +import { BasicNodeBindData } from './node';
  2 +
  3 +export interface RuleChainType {
  4 + ruleChainId: Id;
  5 + firstNodeIndex?: number;
  6 + nodes: BasicNodeBindData[];
  7 + connections: ConnectionItemType[];
  8 + ruleChainConnections?: any;
  9 +}
  10 +
  11 +export interface Id {
  12 + entityType: string;
  13 + id: string;
  14 +}
  15 +
  16 +export interface ConnectionItemType {
  17 + fromIndex: number;
  18 + toIndex: number;
  19 + type: string;
  20 +}
... ...
  1 +import { FormProps } from '/@/components/Form';
  2 +
  3 +export const isOutputHandle = (string: string) => {
  4 + const reg = /.*handle-right/;
  5 + return reg.test(string);
  6 +};
  7 +
  8 +export const isInputHandle = (string: string) => {
  9 + const reg = /.*handle-left/;
  10 + return reg.test(string);
  11 +};
  12 +
  13 +export const basicNodeFormConfiguration = (): FormProps => {
  14 + return {
  15 + layout: 'vertical',
  16 + showActionButtonGroup: false,
  17 + };
  18 +};
... ...