Commit 723c20974c72bb50adb712b6144d98b6dc96b3ab

Authored by loveumiko
2 parents f6eb0d28 437eb431

Merge branch 'main_dev' of http://git.yunteng.com/yunteng/thingskit-front into feat/add-rule-chain

Showing 39 changed files with 1131 additions and 124 deletions
1 import { RuleChainPaginationItemType } from './model/type'; 1 import { RuleChainPaginationItemType } from './model/type';
2 import { TBPaginationResult } from '/#/axios'; 2 import { TBPaginationResult } from '/#/axios';
3 import { defHttp } from '/@/utils/http/axios'; 3 import { defHttp } from '/@/utils/http/axios';
4 -import { RuleChainType } from '/@/views/rule/designer/types/ruleNode'; 4 +import { RuleChainDetail, RuleChainType } from '/@/views/rule/designer/types/ruleNode';
5 5
6 enum Api { 6 enum Api {
  7 + GET_RULE_CHAINS_DETAIL = '/ruleChain',
7 SAVE = '/ruleChain/metadata', 8 SAVE = '/ruleChain/metadata',
8 GET_RULE_CHAINES = '/ruleChains', 9 GET_RULE_CHAINES = '/ruleChains',
9 GET_RULE_NODE_EVENTS = '/events/RULE_NODE', 10 GET_RULE_NODE_EVENTS = '/events/RULE_NODE',
10 } 11 }
11 12
  13 +export const getRuleChainDetail = (id: string) => {
  14 + return defHttp.get<RuleChainDetail>(
  15 + {
  16 + url: `${Api.GET_RULE_CHAINS_DETAIL}/${id}`,
  17 + },
  18 + { joinPrefix: false }
  19 + );
  20 +};
  21 +
12 export const getRuleChainData = (id: string) => { 22 export const getRuleChainData = (id: string) => {
13 return defHttp.get<RuleChainType>( 23 return defHttp.get<RuleChainType>(
14 { 24 {
@@ -66,7 +66,7 @@ export interface RoleReqDTO { @@ -66,7 +66,7 @@ export interface RoleReqDTO {
66 name?: string; 66 name?: string;
67 remark?: string; 67 remark?: string;
68 status: number; 68 status: number;
69 - menu: Array<string>; 69 + menu: Array<string | number>;
70 } 70 }
71 71
72 export interface ChangeAccountParams { 72 export interface ChangeAccountParams {
@@ -34,6 +34,7 @@ enum Api { @@ -34,6 +34,7 @@ enum Api {
34 GetAllRoleList = '/role/find/list', 34 GetAllRoleList = '/role/find/list',
35 BaseUserUrl = '/user', 35 BaseUserUrl = '/user',
36 BaseOrganization = '/organization', 36 BaseOrganization = '/organization',
  37 + RESET_USER_PASSWORD = '/user/reset_password/',
37 } 38 }
38 39
39 export const getAccountInfo = (userId: string) => 40 export const getAccountInfo = (userId: string) =>
@@ -172,3 +173,12 @@ export const resetPassword = (params: ChangeAccountParams) => @@ -172,3 +173,12 @@ export const resetPassword = (params: ChangeAccountParams) =>
172 url: Api.BaseUserUrl + '/reset', 173 url: Api.BaseUserUrl + '/reset',
173 params: params, 174 params: params,
174 }); 175 });
  176 +
  177 +/**
  178 + * 清除密码
  179 + * @param params
  180 + */
  181 +export const clearUserPassword = (userId: string) =>
  182 + defHttp.post({
  183 + url: Api.RESET_USER_PASSWORD + userId,
  184 + });
@@ -38,6 +38,11 @@ export const PLATFORM = 'PLATFORM'; @@ -38,6 +38,11 @@ export const PLATFORM = 'PLATFORM';
38 export const PLATFORM_INFO_CACHE_KEY = 'PLATFORM_INFO'; 38 export const PLATFORM_INFO_CACHE_KEY = 'PLATFORM_INFO';
39 39
40 export const MENU_LIST = 'MENU_LIST'; 40 export const MENU_LIST = 'MENU_LIST';
  41 +
  42 +export const RULE_NODE_LOCAL_CACHE_KEY = 'RULE__NODE__KEY__';
  43 +
  44 +export const RULE_NODE_KEY = 'RULE_NODE';
  45 +
41 export enum CacheTypeEnum { 46 export enum CacheTypeEnum {
42 SESSION, 47 SESSION,
43 LOCAL, 48 LOCAL,
@@ -16,4 +16,6 @@ export const PageEnum = { @@ -16,4 +16,6 @@ export const PageEnum = {
16 DEVICE_LIST: '/device/list', 16 DEVICE_LIST: '/device/list',
17 17
18 SHARE_PAGE: '/share/:viewType/:id/:publicId', 18 SHARE_PAGE: '/share/:viewType/:id/:publicId',
  19 +
  20 + RULE_CHAIN_DETAIL: '/rule/chain/:id',
19 }; 21 };
@@ -102,7 +102,12 @@ @@ -102,7 +102,12 @@
102 <template> 102 <template>
103 <StepContainer> 103 <StepContainer>
104 <div class="">设备文件</div> 104 <div class="">设备文件</div>
105 - <Upload.Dragger :fileList="fileList" :customRequest="handleParseFile" accept=".csv" name="file"> 105 + <Upload.Dragger
  106 + v-model:fileList="fileList"
  107 + :customRequest="handleParseFile"
  108 + accept=".csv"
  109 + name="file"
  110 + >
106 <section class="cursor-pointer flex flex-col justify-center items-center"> 111 <section class="cursor-pointer flex flex-col justify-center items-center">
107 <InboxOutlined class="text-[4rem] !text-blue-400" /> 112 <InboxOutlined class="text-[4rem] !text-blue-400" />
108 <div class="text-gray-500">点击上传或拖拽上传</div> 113 <div class="text-gray-500">点击上传或拖拽上传</div>
@@ -68,7 +68,7 @@ export const formSchemas: FormSchema[] = [ @@ -68,7 +68,7 @@ export const formSchemas: FormSchema[] = [
68 componentProps: { 68 componentProps: {
69 placeholder: '请输入标识符', 69 placeholder: '请输入标识符',
70 }, 70 },
71 - colProps: { span: 6 }, 71 + colProps: { span: 7 },
72 }, 72 },
73 { 73 {
74 field: 'eventType', 74 field: 'eventType',
@@ -83,7 +83,7 @@ export const formSchemas: FormSchema[] = [ @@ -83,7 +83,7 @@ export const formSchemas: FormSchema[] = [
83 labelField: 'itemText', 83 labelField: 'itemText',
84 valueField: 'itemValue', 84 valueField: 'itemValue',
85 }, 85 },
86 - colProps: { span: 6 }, 86 + colProps: { span: 7 },
87 }, 87 },
88 { 88 {
89 field: 'dateRange', 89 field: 'dateRange',
@@ -94,6 +94,6 @@ export const formSchemas: FormSchema[] = [ @@ -94,6 +94,6 @@ export const formSchemas: FormSchema[] = [
94 defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')], 94 defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
95 }, 95 },
96 }, 96 },
97 - colProps: { span: 6 }, 97 + colProps: { span: 7 },
98 }, 98 },
99 ]; 99 ];
@@ -39,6 +39,7 @@ @@ -39,6 +39,7 @@
39 type?: string; 39 type?: string;
40 boolClose?: string; 40 boolClose?: string;
41 boolOpen?: string; 41 boolOpen?: string;
  42 + name?: string;
42 detail: DeviceModelOfMatterAttrs; 43 detail: DeviceModelOfMatterAttrs;
43 } 44 }
44 45
@@ -61,23 +62,27 @@ @@ -61,23 +62,27 @@
61 showQuickJumper: true, 62 showQuickJumper: true,
62 hideOnSinglePage: false, 63 hideOnSinglePage: false,
63 showTotal: (total: number) => `共${total}条数据`, 64 showTotal: (total: number) => `共${total}条数据`,
64 - onChange: handleChange,  
65 - onShowSizeChange: handleChange, 65 + onChange: handleFilterChange,
  66 + onShowSizeChange: handleFilterChange,
66 }); 67 });
67 68
68 const socketInfo = reactive({ 69 const socketInfo = reactive({
69 cmdId: 0, 70 cmdId: 0,
70 origin: `${socketUrl}${token}`, 71 origin: `${socketUrl}${token}`,
71 attr: undefined as string | undefined, 72 attr: undefined as string | undefined,
72 - originData: [] as DataSource[],  
73 dataSource: [] as DataSource[], 73 dataSource: [] as DataSource[],
74 message: {} as ReceiveMessage['data'], 74 message: {} as ReceiveMessage['data'],
75 attrKeys: [] as DeviceModelOfMatterAttrs[], 75 attrKeys: [] as DeviceModelOfMatterAttrs[],
  76 + filterAttrKeys: [] as DeviceModelOfMatterAttrs[],
76 }); 77 });
77 78
78 const getPaginationAttrkey = computed<DeviceModelOfMatterAttrs[]>(() => { 79 const getPaginationAttrkey = computed<DeviceModelOfMatterAttrs[]>(() => {
79 const { current = 1, pageSize = 10 } = pagination; 80 const { current = 1, pageSize = 10 } = pagination;
80 - return socketInfo.attrKeys.slice(current * pageSize - pageSize, current * pageSize); 81 + return (
  82 + socketInfo.filterAttrKeys && socketInfo.filterAttrKeys.length
  83 + ? socketInfo.filterAttrKeys
  84 + : socketInfo.attrKeys
  85 + ).slice(current * pageSize - pageSize, current * pageSize);
81 }); 86 });
82 87
83 function createUnsubscribeMessage(cmdId: number) { 88 function createUnsubscribeMessage(cmdId: number) {
@@ -107,6 +112,22 @@ @@ -107,6 +112,22 @@
107 }; 112 };
108 }); 113 });
109 114
  115 + const getFilterSendValue = computed(() => {
  116 + return {
  117 + tsSubCmds: [
  118 + {
  119 + entityType: 'DEVICE',
  120 + entityId: props.deviceDetail!.tbDeviceId,
  121 + scope: 'LATEST_TELEMETRY',
  122 + cmdId: socketInfo.cmdId,
  123 + keys: unref(getPaginationAttrkey)
  124 + .map((item) => item.identifier)
  125 + .join(','),
  126 + },
  127 + ],
  128 + };
  129 + });
  130 +
110 const [registerForm, { getFieldsValue }] = useForm({ 131 const [registerForm, { getFieldsValue }] = useForm({
111 schemas: [ 132 schemas: [
112 { 133 {
@@ -125,20 +146,29 @@ @@ -125,20 +146,29 @@
125 submitFunc: async () => { 146 submitFunc: async () => {
126 try { 147 try {
127 const { value } = getFieldsValue() || {}; 148 const { value } = getFieldsValue() || {};
128 - if (!value) setTableData(socketInfo.originData);  
129 - const data = unref(socketInfo.originData).filter(  
130 - (item) => item.key?.includes(value) || item.value?.includes(value)  
131 - ); 149 +
  150 + pagination.current = 1;
  151 +
  152 + socketInfo.filterAttrKeys = value
  153 + ? unref(socketInfo.attrKeys).filter(
  154 + (item) =>
  155 + item.identifier?.toUpperCase().includes(value.toUpperCase()) ||
  156 + item.name?.toUpperCase().includes(value.toUpperCase())
  157 + )
  158 + : socketInfo.attrKeys;
  159 +
132 await nextTick(); 160 await nextTick();
133 - socketInfo.dataSource = data;  
134 161
135 - setTableData(data); 162 + handleFilterChange();
  163 +
  164 + unref(mode) === EnumTableCardMode.TABLE && setTableData(socketInfo.dataSource);
136 } catch (error) {} 165 } catch (error) {}
137 }, 166 },
138 resetFunc: async () => { 167 resetFunc: async () => {
139 try { 168 try {
140 - socketInfo.dataSource = socketInfo.originData;  
141 - setTableData(socketInfo.originData); 169 + socketInfo.filterAttrKeys = [];
  170 + handleFilterChange();
  171 + unref(mode) === EnumTableCardMode.TABLE && setTableData(socketInfo.dataSource);
142 } catch (error) {} 172 } catch (error) {}
143 }, 173 },
144 }); 174 });
@@ -148,15 +178,19 @@ @@ -148,15 +178,19 @@
148 showTableSetting: true, 178 showTableSetting: true,
149 pagination: pagination as any, 179 pagination: pagination as any,
150 bordered: true, 180 bordered: true,
  181 + resizeHeightOffset: 16,
151 showIndexColumn: false, 182 showIndexColumn: false,
152 }); 183 });
153 184
154 - function handleChange(page: number, pageSize: number) { 185 + function handleFilterChange(
  186 + page: number = pagination.current || 1,
  187 + pageSize: number = pagination.pageSize || 10
  188 + ) {
155 pagination.current = page; 189 pagination.current = page;
156 pagination.pageSize = pageSize; 190 pagination.pageSize = pageSize;
157 send(JSON.stringify(createUnsubscribeMessage(socketInfo.cmdId))); 191 send(JSON.stringify(createUnsubscribeMessage(socketInfo.cmdId)));
158 socketInfo.cmdId = socketInfo.cmdId + 1; 192 socketInfo.cmdId = socketInfo.cmdId + 1;
159 - send(JSON.stringify(unref(getSendValue))); 193 + send(JSON.stringify(unref(getFilterSendValue)));
160 } 194 }
161 195
162 const [registerModal, { openModal }] = useModal(); 196 const [registerModal, { openModal }] = useModal();
@@ -166,8 +200,8 @@ @@ -166,8 +200,8 @@
166 const switchMode = async (value: EnumTableCardMode) => { 200 const switchMode = async (value: EnumTableCardMode) => {
167 mode.value = value; 201 mode.value = value;
168 await nextTick(); 202 await nextTick();
169 - setTableData(socketInfo.originData);  
170 - socketInfo.dataSource = socketInfo.originData; 203 + unref(mode) === EnumTableCardMode.TABLE && setTableData(socketInfo.dataSource);
  204 + socketInfo.filterAttrKeys = [];
171 }; 205 };
172 206
173 const { createMessage } = useMessage(); 207 const { createMessage } = useMessage();
@@ -190,10 +224,15 @@ @@ -190,10 +224,15 @@
190 }; 224 };
191 225
192 const setDataSource = () => { 226 const setDataSource = () => {
193 - socketInfo.originData = socketInfo.dataSource = socketInfo.attrKeys.map((item) => { 227 + socketInfo.dataSource = (
  228 + socketInfo.filterAttrKeys && socketInfo.filterAttrKeys.length
  229 + ? socketInfo.filterAttrKeys
  230 + : socketInfo.attrKeys
  231 + ).map((item) => {
194 const { identifier: key, name, detail, accessMode } = item; 232 const { identifier: key, name, detail, accessMode } = item;
195 const { unit, boolClose, boolOpen, type } = getUnit(detail); 233 const { unit, boolClose, boolOpen, type } = getUnit(detail);
196 - const dataInfo = socketInfo.attrKeys.find((item) => item.identifier === key); 234 + const dataInfo = socketInfo.filterAttrKeys.find((item) => item.identifier === key);
  235 +
197 let time: number | undefined; 236 let time: number | undefined;
198 let value: any | undefined; 237 let value: any | undefined;
199 const message = socketInfo.message[key]; 238 const message = socketInfo.message[key];
@@ -240,7 +279,8 @@ @@ -240,7 +279,8 @@
240 setDataSource(); 279 setDataSource();
241 280
242 await nextTick(); 281 await nextTick();
243 - setTableData(socketInfo.dataSource); 282 +
  283 + unref(mode) === EnumTableCardMode.TABLE && setTableData(socketInfo.dataSource);
244 } 284 }
245 } catch (error) {} 285 } catch (error) {}
246 }, 286 },
@@ -273,6 +273,7 @@ @@ -273,6 +273,7 @@
273 event: DropMenuEvent.SET_DEFAULT, 273 event: DropMenuEvent.SET_DEFAULT,
274 icon: 'ant-design:unordered-list-outlined', 274 icon: 'ant-design:unordered-list-outlined',
275 onClick: handleSetDefault.bind(null, item), 275 onClick: handleSetDefault.bind(null, item),
  276 + disabled: item.default,
276 }, 277 },
277 { 278 {
278 text: '删除', 279 text: '删除',
@@ -283,6 +284,7 @@ @@ -283,6 +284,7 @@
283 title: '是否确认删除操作?', 284 title: '是否确认删除操作?',
284 onConfirm: handleDelete.bind(null, [item.id]), 285 onConfirm: handleDelete.bind(null, [item.id]),
285 }, 286 },
  287 + disabled: item.default,
286 }, 288 },
287 ]" 289 ]"
288 /> 290 />
  1 +export enum RuleChainEntityType {
  2 + RULE_NODE = 'RULE_NODE',
  3 + RULE_CHAIN = 'RULE_CHAIN',
  4 +}
1 import { Node } from '@vue-flow/core'; 1 import { Node } from '@vue-flow/core';
2 import { NodeTypeEnum } from '../enum'; 2 import { NodeTypeEnum } from '../enum';
3 import { buildUUID } from '/@/utils/uuid'; 3 import { buildUUID } from '/@/utils/uuid';
  4 +import { NodeData } from '../types/node';
4 5
5 export const useAddNodes = () => { 6 export const useAddNodes = () => {
6 const getAddNodesParams = ( 7 const getAddNodesParams = (
7 position: Node['position'], 8 position: Node['position'],
8 - data: object, 9 + data: NodeData,
9 options?: Partial<Node> 10 options?: Partial<Node>
10 ): Node => { 11 ): Node => {
11 return { 12 return {
@@ -122,6 +122,7 @@ export function useBasicDataTransform() { @@ -122,6 +122,7 @@ export function useBasicDataTransform() {
122 description, 122 description,
123 name, 123 name,
124 }, 124 },
  125 + created: !!id?.id,
125 }, 126 },
126 { 127 {
127 id: id?.id || buildUUID(), 128 id: id?.id || buildUUID(),
  1 +import { GraphEdge, GraphNode, VueFlowStore } from '@vue-flow/core';
  2 +import { getRuleNodeCache, setRuleNodeCache } from './useRuleCopyPaste';
  3 +import { RuleContextMenuEnum } from './useRuleChainContextMenu';
  4 +import { useAddNodes } from './useAddNodes';
  5 +import { buildUUID } from '/@/utils/uuid';
  6 +import { toRaw, unref } from 'vue';
  7 +import { useSaveAndRedo } from './useSaveAndRedo';
  8 +import { isUnDef } from '/@/utils/is';
  9 +import { useAddEdges } from './useAddEdges';
  10 +import { EdgeData } from '../types/node';
  11 +
  12 +interface HandleContextMenuActionParamsType {
  13 + menuType: RuleContextMenuEnum;
  14 + event?: Event;
  15 + flowActionType?: VueFlowStore;
  16 + node?: GraphNode;
  17 + edge?: GraphEdge;
  18 + useSaveAndRedoActionType?: ReturnType<typeof useSaveAndRedo>;
  19 +}
  20 +
  21 +export const NODE_WIDTH = 176;
  22 +export const NODE_HEIGHT = 48;
  23 +
  24 +function getElementsCenter(nodes: GraphNode[]) {
  25 + let leftTopX: number | undefined;
  26 + let leftTopY: number | undefined;
  27 + let rightBottomX: number | undefined;
  28 + let rightBottomY: number | undefined;
  29 +
  30 + for (const node of nodes) {
  31 + const { position } = node;
  32 + const { x, y } = position;
  33 + if (isUnDef(leftTopX)) {
  34 + leftTopX = x;
  35 + leftTopY = y;
  36 + rightBottomX = x + NODE_WIDTH;
  37 + rightBottomY = y + NODE_HEIGHT;
  38 +
  39 + continue;
  40 + }
  41 +
  42 + if (x < leftTopX!) {
  43 + leftTopX = x;
  44 + if (y < leftTopY!) {
  45 + leftTopY = y;
  46 + }
  47 + continue;
  48 + }
  49 +
  50 + if (x + NODE_WIDTH > rightBottomX!) {
  51 + rightBottomX = x + NODE_WIDTH;
  52 + if (y + NODE_HEIGHT > rightBottomY!) {
  53 + rightBottomY = y + NODE_HEIGHT;
  54 + }
  55 + }
  56 + }
  57 +
  58 + return {
  59 + originX: (rightBottomX! - leftTopX!) / 2 + leftTopX!,
  60 + originY: (rightBottomY! - leftTopY!) / 2 + leftTopY!,
  61 + };
  62 +}
  63 +
  64 +export function useContextMenuAction() {
  65 + const copy = (params: HandleContextMenuActionParamsType) => {
  66 + const { node } = params;
  67 + if (!node) return;
  68 + const { position, data } = node;
  69 + const { getAddNodesParams } = useAddNodes();
  70 + const { x, y } = position;
  71 +
  72 + const value = getAddNodesParams(position, data, { id: buildUUID() });
  73 +
  74 + setRuleNodeCache({
  75 + nodes: [value],
  76 + originX: x + NODE_WIDTH / 2 + x,
  77 + originY: y + NODE_HEIGHT / 2,
  78 + });
  79 + };
  80 +
  81 + const paste = (params: HandleContextMenuActionParamsType) => {
  82 + const { event, flowActionType, useSaveAndRedoActionType } = params;
  83 + const { triggerChange } = useSaveAndRedoActionType || {};
  84 + const { getAddNodesParams } = useAddNodes();
  85 + const { getAddedgesParams } = useAddEdges();
  86 + const clientX = (event as MouseEvent).offsetX;
  87 + const clientY = (event as MouseEvent).offsetY;
  88 +
  89 + const { edges = [], nodes = [], originX, originY } = getRuleNodeCache();
  90 +
  91 + const newNode = nodes.map((node) => {
  92 + const { position, data, id } = node;
  93 + const { x, y } = position;
  94 +
  95 + const newX = clientX - originX! + x + NODE_WIDTH / 2;
  96 + const newY = clientY - originY! + y + NODE_HEIGHT / 2;
  97 +
  98 + return getAddNodesParams({ x: newX, y: newY }, { ...data, created: false }, { id });
  99 + });
  100 +
  101 + const newEdges = edges.map((edge) => getAddedgesParams(edge, edge.data));
  102 +
  103 + flowActionType?.addNodes(newNode);
  104 + flowActionType?.addEdges(newEdges);
  105 +
  106 + triggerChange?.();
  107 +
  108 + flowActionType?.removeSelectedElements();
  109 + };
  110 +
  111 + const selectAll = (params: HandleContextMenuActionParamsType) => {
  112 + const { flowActionType } = params;
  113 + flowActionType?.addSelectedElements(unref(flowActionType.getElements));
  114 + };
  115 +
  116 + const unselect = (params: HandleContextMenuActionParamsType) => {
  117 + const { flowActionType } = params;
  118 + flowActionType?.removeSelectedElements();
  119 + };
  120 +
  121 + const deleteElement = (parmas: HandleContextMenuActionParamsType) => {
  122 + const { useSaveAndRedoActionType, flowActionType, node, edge } = parmas;
  123 + const { triggerChange } = useSaveAndRedoActionType || {};
  124 +
  125 + node && flowActionType?.removeNodes(node);
  126 + edge && flowActionType?.removeEdges(edge);
  127 +
  128 + triggerChange?.();
  129 + };
  130 +
  131 + const deleteElements = (params: HandleContextMenuActionParamsType) => {
  132 + const { flowActionType, useSaveAndRedoActionType } = params;
  133 + flowActionType?.removeNodes(unref(flowActionType.getSelectedNodes));
  134 +
  135 + useSaveAndRedoActionType?.triggerChange?.();
  136 + };
  137 +
  138 + const selectCopy = (params: HandleContextMenuActionParamsType) => {
  139 + const { flowActionType } = params;
  140 + const { getAddNodesParams } = useAddNodes();
  141 + const { getAddedgesParams } = useAddEdges();
  142 +
  143 + const edges = unref(flowActionType?.getSelectedEdges)?.map((edge) =>
  144 + getAddedgesParams(
  145 + {
  146 + source: edge.source,
  147 + target: edge.target,
  148 + sourceHandle: edge.sourceHandle,
  149 + targetHandle: edge.targetHandle,
  150 + },
  151 + toRaw(unref(edge.data as EdgeData)?.data)
  152 + )
  153 + );
  154 +
  155 + const nodes = unref(flowActionType?.getSelectedNodes)?.map((node) => {
  156 + const { id: oldId } = node;
  157 + const newId = buildUUID();
  158 +
  159 + for (const connection of edges || []) {
  160 + if (connection.source.includes(oldId)) {
  161 + connection.source = newId;
  162 + connection.sourceHandle = connection.sourceHandle?.replaceAll(oldId, newId);
  163 + continue;
  164 + }
  165 +
  166 + if (connection.target.includes(oldId)) {
  167 + connection.target = newId;
  168 + connection.targetHandle = connection.targetHandle?.replaceAll(oldId, newId);
  169 + }
  170 + }
  171 +
  172 + return getAddNodesParams(node.position, toRaw(unref(node.data)), { id: newId });
  173 + });
  174 +
  175 + const originRect = getElementsCenter(unref(flowActionType?.getSelectedNodes) || []);
  176 +
  177 + setRuleNodeCache({ nodes, edges, ...originRect });
  178 + };
  179 +
  180 + const applyChange = (params: HandleContextMenuActionParamsType) => {
  181 + const { useSaveAndRedoActionType, flowActionType } = params;
  182 +
  183 + useSaveAndRedoActionType?.handleApplyChange(flowActionType!);
  184 + };
  185 +
  186 + const undoChange = (params: HandleContextMenuActionParamsType) => {
  187 + const { useSaveAndRedoActionType, flowActionType } = params;
  188 +
  189 + useSaveAndRedoActionType?.handleRedoChange(flowActionType!);
  190 + };
  191 +
  192 + const handleContextMenuAction = (params: HandleContextMenuActionParamsType) => {
  193 + const { menuType } = params;
  194 +
  195 + const handlerMapping = {
  196 + [RuleContextMenuEnum.COPY]: copy,
  197 + [RuleContextMenuEnum.PASTE]: paste,
  198 + [RuleContextMenuEnum.SELECT_ALL]: selectAll,
  199 + [RuleContextMenuEnum.UNSELECTED]: unselect,
  200 + [RuleContextMenuEnum.DELETE]: deleteElement,
  201 + [RuleContextMenuEnum.DELETE_SELECT]: deleteElements,
  202 + [RuleContextMenuEnum.SELECT_COPY]: selectCopy,
  203 + [RuleContextMenuEnum.APPLY_CHANGE]: applyChange,
  204 + [RuleContextMenuEnum.UNDO_CHANGE]: undoChange,
  205 + };
  206 +
  207 + if (handlerMapping[menuType]) {
  208 + handlerMapping[menuType]?.(params);
  209 + }
  210 + };
  211 +
  212 + return {
  213 + handleContextMenuAction,
  214 + };
  215 +}
  1 +import { NodeMouseEvent } from '@vue-flow/core';
  2 +import { useContextMenu } from '../src/components/RuleChainContextMenu';
  3 +import { RuleChainContextMenuItemType } from '../src/components/RuleChainContextMenu/index.type';
  4 +import { ElementsTypeEnum } from '../enum';
  5 +import { checkHasCacheRuleNode } from './useRuleCopyPaste';
  6 +
  7 +export enum RuleContextMenuEnum {
  8 + DETAIL = 'DETAIL',
  9 + COPY = 'COPY',
  10 + DELETE = 'DELETE',
  11 +
  12 + SELECT_COPY = 'SELECT_COPY',
  13 + PASTE = 'PASTE',
  14 + UNSELECTED = 'UNSELECTED',
  15 + DELETE_SELECT = 'DELETE_SELECT',
  16 + APPLY_CHANGE = 'APPLY_CHANGE',
  17 + UNDO_CHANGE = 'UNDO_CHANGE',
  18 +
  19 + SELECT_ALL = 'SELECT_ALL',
  20 +}
  21 +
  22 +export enum RuleContextMenuNameEnum {
  23 + DETAIL = '详情',
  24 + COPY = '复制',
  25 + DELETE = '删除',
  26 +
  27 + SELECT_COPY = '选择副本',
  28 + PASTE = '粘贴',
  29 + UNSELECTED = '取消选择',
  30 + DELETE_SELECT = '删除选定',
  31 + APPLY_CHANGE = '应用更改',
  32 + UNDO_CHANGE = '撤销更改',
  33 +
  34 + SELECT_ALL = '选择全部',
  35 +}
  36 +
  37 +export enum RuleChainContextMenuIconEnum {
  38 + DETAIL = 'material-symbols:menu',
  39 + COPY = 'material-symbols:content-copy',
  40 + DELETE = 'material-symbols:delete',
  41 +
  42 + SELECT_COPY = 'material-symbols:content-copy',
  43 + PASTE = 'material-symbols:content-paste',
  44 + UNSELECTED = 'material-symbols:tab-unselected',
  45 + DELETE_SELECT = 'material-symbols:close',
  46 + APPLY_CHANGE = 'material-symbols:done',
  47 + UNDO_CHANGE = 'material-symbols:close',
  48 +
  49 + SELECT_ALL = 'material-symbols:select-all',
  50 +
  51 + // LINK = 'material-symbols:trending-flat',
  52 +}
  53 +
  54 +export enum RuleChainContextMenuShortcutKeyEnum {
  55 + DELETE = 'Ctrl(⌘) X',
  56 +
  57 + SELECT_COPY = 'Ctrl(⌘) C',
  58 + PASTE = 'Ctrl(⌘) V',
  59 + UNSELECTED = 'Esc',
  60 + DELETE_SELECT = 'Del',
  61 + APPLY_CHANGE = 'Ctrl(⌘) S',
  62 + UNDO_CHANGE = 'Ctrl(⌘) Z',
  63 +
  64 + SELECT_ALL = 'Ctrl(⌘) A',
  65 +}
  66 +
  67 +const getMenuItem = (key: RuleContextMenuEnum, handler: Fn, disabled = false) => {
  68 + return {
  69 + key,
  70 + label: RuleContextMenuNameEnum[key],
  71 + icon: RuleChainContextMenuIconEnum[key],
  72 + shortcutKey: RuleChainContextMenuShortcutKeyEnum[key],
  73 + handler: () => handler?.(key),
  74 + disabled,
  75 + } as RuleChainContextMenuItemType;
  76 +};
  77 +
  78 +const getDivider = (): RuleChainContextMenuItemType => ({ divider: true });
  79 +
  80 +export function useCreateRuleChainContextMenu() {
  81 + const [createContextMenu] = useContextMenu();
  82 +
  83 + const createNodeContextMenu = (params: NodeMouseEvent): Promise<RuleContextMenuEnum | ''> => {
  84 + return new Promise(async (resolve) => {
  85 + await createContextMenu(params, {
  86 + items: [
  87 + getMenuItem(RuleContextMenuEnum.DETAIL, resolve),
  88 + getMenuItem(RuleContextMenuEnum.COPY, resolve),
  89 + getMenuItem(RuleContextMenuEnum.DELETE, resolve),
  90 + ],
  91 + });
  92 + resolve('');
  93 + });
  94 + };
  95 +
  96 + const createElementsSelectedContextMenu = (
  97 + params: NodeMouseEvent,
  98 + changeMarker: boolean,
  99 + elementsType: ElementsTypeEnum.NODE = ElementsTypeEnum.NODE
  100 + ): Promise<RuleContextMenuEnum | ''> => {
  101 + return new Promise(async (resolve) => {
  102 + await createContextMenu(params, {
  103 + items: [
  104 + ...(elementsType === ElementsTypeEnum.NODE
  105 + ? [getMenuItem(RuleContextMenuEnum.SELECT_COPY, resolve)]
  106 + : []),
  107 + getMenuItem(RuleContextMenuEnum.PASTE, resolve, !checkHasCacheRuleNode()),
  108 + getDivider(),
  109 + getMenuItem(RuleContextMenuEnum.UNSELECTED, resolve),
  110 + getMenuItem(RuleContextMenuEnum.DELETE_SELECT, resolve),
  111 + getDivider(),
  112 + getMenuItem(RuleContextMenuEnum.APPLY_CHANGE, resolve, !changeMarker),
  113 + getMenuItem(RuleContextMenuEnum.UNDO_CHANGE, resolve, !changeMarker),
  114 + ],
  115 + });
  116 + resolve('');
  117 + });
  118 + };
  119 +
  120 + const createPanelContextMenu = (
  121 + params: NodeMouseEvent,
  122 + changeMarker: boolean
  123 + ): Promise<RuleContextMenuEnum | ''> => {
  124 + return new Promise(async (resolve) => {
  125 + await createContextMenu(params, {
  126 + items: [
  127 + getMenuItem(RuleContextMenuEnum.PASTE, resolve, !checkHasCacheRuleNode()),
  128 + getMenuItem(RuleContextMenuEnum.SELECT_ALL, resolve),
  129 + getMenuItem(RuleContextMenuEnum.APPLY_CHANGE, resolve, !changeMarker),
  130 + getMenuItem(RuleContextMenuEnum.UNDO_CHANGE, resolve, !changeMarker),
  131 + ],
  132 + });
  133 + resolve('');
  134 + });
  135 + };
  136 +
  137 + const createEdgeContextMenu = (
  138 + params: NodeMouseEvent,
  139 + isInput = false
  140 + ): Promise<RuleContextMenuEnum | ''> => {
  141 + return new Promise(async (resolve) => {
  142 + await createContextMenu(params, {
  143 + items: [
  144 + ...(isInput ? [] : [getMenuItem(RuleContextMenuEnum.DETAIL, resolve)]),
  145 + getMenuItem(RuleContextMenuEnum.DELETE, resolve),
  146 + ],
  147 + });
  148 + resolve('');
  149 + });
  150 + };
  151 + return {
  152 + createNodeContextMenu,
  153 + createElementsSelectedContextMenu,
  154 + createPanelContextMenu,
  155 + createEdgeContextMenu,
  156 + };
  157 +}
  1 +import { Edge, Node } from '@vue-flow/core';
  2 +import { RULE_NODE_KEY, RULE_NODE_LOCAL_CACHE_KEY } from '/@/enums/cacheEnum';
  3 +import { createLocalStorage } from '/@/utils/cache';
  4 +
  5 +const ruleNodeStorage = createLocalStorage({ prefixKey: RULE_NODE_LOCAL_CACHE_KEY });
  6 +
  7 +interface RuleNodeCacheType {
  8 + nodes?: Node[];
  9 + edges?: Edge[];
  10 + originX?: number;
  11 + originY?: number;
  12 +}
  13 +
  14 +export const setRuleNodeCache = ({
  15 + nodes = [],
  16 + edges = [],
  17 + originX,
  18 + originY,
  19 +}: RuleNodeCacheType) => {
  20 + ruleNodeStorage.set(RULE_NODE_KEY, {
  21 + nodes,
  22 + edges,
  23 + originX,
  24 + originY,
  25 + });
  26 +};
  27 +
  28 +export const getRuleNodeCache = (): RuleNodeCacheType => ruleNodeStorage.get(RULE_NODE_KEY);
  29 +
  30 +export const checkHasCacheRuleNode = () => !!getRuleNodeCache();
  31 +
  32 +function initRuleNodeStorage() {
  33 + const value = ruleNodeStorage.get(RULE_NODE_KEY);
  34 + value && ruleNodeStorage.set(RULE_NODE_KEY, value);
  35 +}
  36 +
  37 +initRuleNodeStorage();
@@ -4,29 +4,38 @@ import type { @@ -4,29 +4,38 @@ import type {
4 NodeComponent, 4 NodeComponent,
5 ValidConnectionFunc, 5 ValidConnectionFunc,
6 GraphNode, 6 GraphNode,
  7 + NodeMouseEvent,
  8 + GraphEdge,
  9 + EdgeMouseEvent,
7 } from '@vue-flow/core'; 10 } from '@vue-flow/core';
8 -import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core';  
9 import type { Ref } from 'vue'; 11 import type { Ref } from 'vue';
10 -import { markRaw, toRaw, unref } from 'vue';  
11 -import { isFunction } from 'lodash-es';  
12 import type { CreateNodeModal } from '../src/components/CreateNodeModal'; 12 import type { CreateNodeModal } from '../src/components/CreateNodeModal';
13 -import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum';  
14 -import { BasicEdge, BasicNode } from '../src/components';  
15 import type { EdgeData, NodeData } from '../types/node'; 13 import type { EdgeData, NodeData } from '../types/node';
16 import type { CreateEdgeModal } from '../src/components/CreateEdgeModal'; 14 import type { CreateEdgeModal } from '../src/components/CreateEdgeModal';
  15 +import { BasicEdge, BasicNode } from '../src/components';
  16 +import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum';
  17 +import { markRaw, toRaw, unref } from 'vue';
  18 +import { isFunction } from 'lodash-es';
  19 +import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core';
17 import { isInputHandle, isOutputHandle } from '../utils'; 20 import { isInputHandle, isOutputHandle } from '../utils';
18 import { useAddEdges } from './useAddEdges'; 21 import { useAddEdges } from './useAddEdges';
19 import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer'; 22 import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer';
20 import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer'; 23 import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer';
21 import { isNumber } from '/@/utils/is'; 24 import { isNumber } from '/@/utils/is';
  25 +import { RuleContextMenuEnum, useCreateRuleChainContextMenu } from './useRuleChainContextMenu';
  26 +import { RuleChainDetail } from '../types/ruleNode';
  27 +import { useContextMenuAction } from './useContextMenuAction';
  28 +import { useSaveAndRedo } from './useSaveAndRedo';
  29 +import { EntryCategoryComponentEnum } from '../enum/category';
22 30
23 interface UseRuleFlowOptionsType { 31 interface UseRuleFlowOptionsType {
24 id: string; 32 id: string;
  33 + ruleChainDetail: Ref<RuleChainDetail | undefined>;
25 createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>; 34 createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
26 createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>; 35 createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>;
27 updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>; 36 updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>;
28 updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>; 37 updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>;
29 - triggerChange: () => void; 38 + useSaveAndRedoActionType: ReturnType<typeof useSaveAndRedo>;
30 } 39 }
31 40
32 const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => { 41 const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => {
@@ -40,12 +49,15 @@ const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => @@ -40,12 +49,15 @@ const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) =>
40 export function useRuleFlow(options: UseRuleFlowOptionsType) { 49 export function useRuleFlow(options: UseRuleFlowOptionsType) {
41 const { 50 const {
42 id, 51 id,
  52 + ruleChainDetail,
43 createEdgeModalActionType, 53 createEdgeModalActionType,
44 updateEdgeDrawerActionType, 54 updateEdgeDrawerActionType,
45 updateNodeDrawerActionType, 55 updateNodeDrawerActionType,
46 - triggerChange, 56 + useSaveAndRedoActionType,
47 } = options; 57 } = options;
48 58
  59 + const { triggerChange } = useSaveAndRedoActionType;
  60 +
49 const flowActionType = useVueFlow({ 61 const flowActionType = useVueFlow({
50 id, 62 id,
51 maxZoom: 1, 63 maxZoom: 1,
@@ -96,6 +108,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -96,6 +108,9 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
96 onNodeDoubleClick, 108 onNodeDoubleClick,
97 onEdgeDoubleClick, 109 onEdgeDoubleClick,
98 onNodeDragStop, 110 onNodeDragStop,
  111 + onNodeContextMenu,
  112 + onEdgeContextMenu,
  113 + onPaneContextMenu,
99 } = flowActionType; 114 } = flowActionType;
100 115
101 const { getAddedgesParams } = useAddEdges(); 116 const { getAddedgesParams } = useAddEdges();
@@ -125,6 +140,119 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -125,6 +140,119 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
125 }); 140 });
126 141
127 onNodeDoubleClick(async ({ node }) => { 142 onNodeDoubleClick(async ({ node }) => {
  143 + handleUpdateNode(node);
  144 + });
  145 +
  146 + onEdgeDoubleClick(async ({ edge }) => {
  147 + handleUpdateEdge(edge);
  148 + });
  149 +
  150 + onNodeDragStop(() => {
  151 + triggerChange();
  152 + });
  153 +
  154 + const {
  155 + createNodeContextMenu,
  156 + createElementsSelectedContextMenu,
  157 + createEdgeContextMenu,
  158 + createPanelContextMenu,
  159 + } = useCreateRuleChainContextMenu();
  160 +
  161 + const { handleContextMenuAction } = useContextMenuAction();
  162 +
  163 + onNodeContextMenu(async (params) => {
  164 + const menuType = params.node.selected
  165 + ? await createElementsSelectedContextMenu(
  166 + params,
  167 + unref(useSaveAndRedoActionType.changeMarker)
  168 + )
  169 + : await createNodeContextMenu(params);
  170 +
  171 + if (menuType) {
  172 + if (menuType === RuleContextMenuEnum.DETAIL) {
  173 + handleUpdateNode(params.node);
  174 + return;
  175 + }
  176 +
  177 + handleContextMenuAction({
  178 + menuType,
  179 + flowActionType,
  180 + event: params.event,
  181 + node: params.node,
  182 + useSaveAndRedoActionType,
  183 + });
  184 + }
  185 + });
  186 +
  187 + onEdgeContextMenu(async (params) => {
  188 + const isInputNode =
  189 + (params.edge.sourceNode.data as NodeData).config?.key === EntryCategoryComponentEnum.INPUT;
  190 + const menuType = await createEdgeContextMenu(
  191 + getCreateEdgeContextMenuParams(params),
  192 + isInputNode
  193 + );
  194 +
  195 + if (menuType) {
  196 + if (menuType === RuleContextMenuEnum.DETAIL) {
  197 + handleUpdateEdge(params.edge);
  198 + return;
  199 + }
  200 +
  201 + handleContextMenuAction({
  202 + menuType,
  203 + flowActionType,
  204 + event: params.event,
  205 + useSaveAndRedoActionType,
  206 + edge: params.edge,
  207 + });
  208 + }
  209 + });
  210 +
  211 + onPaneContextMenu(async (params) => {
  212 + const menuType = unref(flowActionType.getSelectedElements).length
  213 + ? await createElementsSelectedContextMenu(
  214 + getCreatePanelContextMenuParams(params),
  215 + unref(useSaveAndRedoActionType.changeMarker)
  216 + )
  217 + : await createPanelContextMenu(
  218 + getCreatePanelContextMenuParams(params),
  219 + unref(useSaveAndRedoActionType.changeMarker)
  220 + );
  221 +
  222 + if (menuType) {
  223 + handleContextMenuAction({
  224 + menuType,
  225 + flowActionType,
  226 + event: params,
  227 + useSaveAndRedoActionType,
  228 + });
  229 + }
  230 + });
  231 +
  232 + /**
  233 + * @description 验证是否有连接label
  234 + * @param sourceData
  235 + * @returns
  236 + */
  237 + function validateHasLabelConnection(sourceData: NodeData) {
  238 + return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length;
  239 + }
  240 +
  241 + function handleMaxConnectionPoint(sourceNode?: GraphNode) {
  242 + if (!sourceNode) return;
  243 +
  244 + const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint;
  245 +
  246 + if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return;
  247 +
  248 + const sourceId = sourceNode.id;
  249 + const connectionPool = unref(getEdges).filter((item) => item.source === sourceId);
  250 + if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) {
  251 + removeEdges(connectionPool[0].id);
  252 + }
  253 + }
  254 +
  255 + async function handleUpdateNode(node: GraphNode) {
128 if ((node.data as NodeData).config?.disableAction) return; 256 if ((node.data as NodeData).config?.disableAction) return;
129 const { flag, data } = 257 const { flag, data } =
130 (await unref(updateNodeDrawerActionType)?.open( 258 (await unref(updateNodeDrawerActionType)?.open(
@@ -137,9 +265,10 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -137,9 +265,10 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
137 265
138 const currentNode = findNode(node.id); 266 const currentNode = findNode(node.id);
139 (currentNode!.data as NodeData).data = data; 267 (currentNode!.data as NodeData).data = data;
140 - }); 268 + triggerChange();
  269 + }
141 270
142 - onEdgeDoubleClick(async ({ edge }) => { 271 + async function handleUpdateEdge(edge: GraphEdge) {
143 if (!validateHasLabelConnection(edge.sourceNode.data)) return; 272 if (!validateHasLabelConnection(edge.sourceNode.data)) return;
144 273
145 if ((edge.sourceNode.data as NodeData).config?.disableAction) return; 274 if ((edge.sourceNode.data as NodeData).config?.disableAction) return;
@@ -156,33 +285,48 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) { @@ -156,33 +285,48 @@ export function useRuleFlow(options: UseRuleFlowOptionsType) {
156 const currentEdge = findEdge(edge.id); 285 const currentEdge = findEdge(edge.id);
157 286
158 (currentEdge!.data as EdgeData).data = toRaw(unref(data)); 287 (currentEdge!.data as EdgeData).data = toRaw(unref(data));
159 - });  
160 -  
161 - onNodeDragStop(() => {  
162 - triggerChange();  
163 - });  
164 288
165 - /**  
166 - * @description 验证是否有连接label  
167 - * @param sourceData  
168 - * @returns  
169 - */  
170 - function validateHasLabelConnection(sourceData: NodeData) {  
171 - return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length; 289 + triggerChange?.();
172 } 290 }
173 291
174 - function handleMaxConnectionPoint(sourceNode?: GraphNode) {  
175 - if (!sourceNode) return;  
176 -  
177 - const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint;  
178 -  
179 - if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return; 292 + function getCreatePanelContextMenuParams(params: Event) {
  293 + return {
  294 + event: params as MouseEvent,
  295 + node: {
  296 + data: {
  297 + data: { name: '规则链' },
  298 + config: {
  299 + name: unref(ruleChainDetail)?.name,
  300 + backgroundColor: '#aac7e4',
  301 + configurationDescriptor: {
  302 + nodeDefinition: {
  303 + icon: 'material-symbols:settings-ethernet',
  304 + },
  305 + },
  306 + },
  307 + },
  308 + },
  309 + } as NodeMouseEvent;
  310 + }
180 311
181 - const sourceId = sourceNode.id;  
182 - const connectionPool = unref(getEdges).filter((item) => item.source === sourceId);  
183 - if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) {  
184 - removeEdges(connectionPool[0].id);  
185 - } 312 + function getCreateEdgeContextMenuParams(params: EdgeMouseEvent) {
  313 + return {
  314 + event: params.event as MouseEvent,
  315 + node: {
  316 + data: {
  317 + data: { name: '链接' },
  318 + config: {
  319 + name: unref(params.edge.data as EdgeData)?.data?.type?.join(' / '),
  320 + backgroundColor: '#aac7e4',
  321 + configurationDescriptor: {
  322 + nodeDefinition: {
  323 + icon: 'material-symbols:trending-flat',
  324 + },
  325 + },
  326 + },
  327 + },
  328 + },
  329 + } as NodeMouseEvent;
186 } 330 }
187 331
188 return { flowActionType }; 332 return { flowActionType };
@@ -3,11 +3,12 @@ import { ComputedRef, computed, ref, unref } from 'vue'; @@ -3,11 +3,12 @@ import { ComputedRef, computed, ref, unref } from 'vue';
3 import { BasicNodeBindData, EdgeData, NodeData } from '../types/node'; 3 import { BasicNodeBindData, EdgeData, NodeData } from '../types/node';
4 import { EntryCategoryComponentEnum } from '../enum/category'; 4 import { EntryCategoryComponentEnum } from '../enum/category';
5 import { useBasicDataTransform } from './useBasicDataTransform'; 5 import { useBasicDataTransform } from './useBasicDataTransform';
6 -import { getRuleChainData, saveRuleChainData } from '/@/api/ruleDesigner';  
7 -import { ConnectionItemType, RuleChainType } from '../types/ruleNode'; 6 +import { getRuleChainData, getRuleChainDetail, saveRuleChainData } from '/@/api/ruleDesigner';
  7 +import { ConnectionItemType, RuleChainDetail, RuleChainType } from '../types/ruleNode';
8 import { useInputNode } from './useInputNode'; 8 import { useInputNode } from './useInputNode';
9 import { buildUUID } from '/@/utils/uuid'; 9 import { buildUUID } from '/@/utils/uuid';
10 import { useRoute } from 'vue-router'; 10 import { useRoute } from 'vue-router';
  11 +import { RuleChainEntityType } from '../enum/entity';
11 12
12 const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT]; 13 const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT];
13 14
@@ -20,8 +21,12 @@ export function useSaveAndRedo() { @@ -20,8 +21,12 @@ export function useSaveAndRedo() {
20 21
21 const route = useRoute(); 22 const route = useRoute();
22 23
  24 + const debugMarker = ref(false);
  25 +
23 const getRuleChainId = computed(() => (route.params as Record<'id', string>).id); 26 const getRuleChainId = computed(() => (route.params as Record<'id', string>).id);
24 27
  28 + const ruleChainDetail = ref<RuleChainDetail>();
  29 +
25 const { mergeData, deconstructionData } = useBasicDataTransform(); 30 const { mergeData, deconstructionData } = useBasicDataTransform();
26 31
27 const triggerChange = () => { 32 const triggerChange = () => {
@@ -71,7 +76,16 @@ export function useSaveAndRedo() { @@ -71,7 +76,16 @@ export function useSaveAndRedo() {
71 76
72 const data = nodeData.data; 77 const data = nodeData.data;
73 78
74 - nodes.push(mergeData(data, nodeData, node)); 79 + nodes.push(
  80 + Object.assign(
  81 + mergeData(data, nodeData, node),
  82 + nodeData.created
  83 + ? ({
  84 + id: { id: node.id, entityType: RuleChainEntityType.RULE_NODE },
  85 + } as BasicNodeBindData)
  86 + : {}
  87 + )
  88 + );
75 } 89 }
76 90
77 return nodes; 91 return nodes;
@@ -116,6 +130,10 @@ export function useSaveAndRedo() { @@ -116,6 +130,10 @@ export function useSaveAndRedo() {
116 resetChange(); 130 resetChange();
117 }; 131 };
118 132
  133 + async function getCurrentRuleChainDetail() {
  134 + ruleChainDetail.value = await getRuleChainDetail(unref(getRuleChainId));
  135 + }
  136 +
119 async function handleSaveRuleChain( 137 async function handleSaveRuleChain(
120 connections: ConnectionItemType[], 138 connections: ConnectionItemType[],
121 nodes: BasicNodeBindData[], 139 nodes: BasicNodeBindData[],
@@ -123,12 +141,13 @@ export function useSaveAndRedo() { @@ -123,12 +141,13 @@ export function useSaveAndRedo() {
123 ) { 141 ) {
124 try { 142 try {
125 loading.value = true; 143 loading.value = true;
  144 +
126 const data = await saveRuleChainData({ 145 const data = await saveRuleChainData({
127 connections, 146 connections,
128 nodes, 147 nodes,
129 firstNodeIndex, 148 firstNodeIndex,
130 ruleChainId: { 149 ruleChainId: {
131 - entityType: 'RULE_CHAIN', 150 + entityType: RuleChainEntityType.RULE_CHAIN,
132 id: unref(getRuleChainId), 151 id: unref(getRuleChainId),
133 }, 152 },
134 }); 153 });
@@ -175,12 +194,23 @@ export function useSaveAndRedo() { @@ -175,12 +194,23 @@ export function useSaveAndRedo() {
175 return elements; 194 return elements;
176 } 195 }
177 196
  197 + const handleRemoveDebug = (flowActionType: VueFlowStore) => {
  198 + for (const item of unref(flowActionType.getNodes)) {
  199 + (item.data as NodeData)!.data!.debugMode = false;
  200 + }
  201 + triggerChange();
  202 + };
  203 +
178 return { 204 return {
179 loading, 205 loading,
  206 + debugMarker,
180 changeMarker, 207 changeMarker,
  208 + ruleChainDetail,
181 triggerChange, 209 triggerChange,
182 handleApplyChange, 210 handleApplyChange,
183 handleRedoChange, 211 handleRedoChange,
  212 + handleRemoveDebug,
184 getCurrentPageMetaData, 213 getCurrentPageMetaData,
  214 + getCurrentRuleChainDetail,
185 }; 215 };
186 } 216 }
@@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
22 import { Icon } from '/@/components/Icon'; 22 import { Icon } from '/@/components/Icon';
23 import { UpdateNodeDrawer } from './src/components/UpdateNodeDrawer'; 23 import { UpdateNodeDrawer } from './src/components/UpdateNodeDrawer';
24 import { UpdateEdgeDrawer } from './src/components/UpdateEdgeDrawer'; 24 import { UpdateEdgeDrawer } from './src/components/UpdateEdgeDrawer';
  25 + import { NodeData } from './types/node';
25 26
26 const getId = Number(Math.random().toString().substring(2)).toString(16); 27 const getId = Number(Math.random().toString().substring(2)).toString(16);
27 28
@@ -39,22 +40,28 @@ @@ -39,22 +40,28 @@
39 40
40 const elements = ref([]); 41 const elements = ref([]);
41 42
  43 + const useSaveAndRedoActionType = useSaveAndRedo();
  44 +
42 const { 45 const {
43 loading, 46 loading,
44 changeMarker, 47 changeMarker,
  48 + ruleChainDetail,
45 getCurrentPageMetaData, 49 getCurrentPageMetaData,
46 triggerChange, 50 triggerChange,
47 handleApplyChange, 51 handleApplyChange,
48 handleRedoChange, 52 handleRedoChange,
49 - } = useSaveAndRedo(); 53 + handleRemoveDebug,
  54 + getCurrentRuleChainDetail,
  55 + } = useSaveAndRedoActionType;
50 56
51 const { flowActionType } = useRuleFlow({ 57 const { flowActionType } = useRuleFlow({
52 id: getId, 58 id: getId,
  59 + ruleChainDetail,
53 createNodeModalActionType, 60 createNodeModalActionType,
54 createEdgeModalActionType, 61 createEdgeModalActionType,
55 updateEdgeDrawerActionType, 62 updateEdgeDrawerActionType,
56 updateNodeDrawerActionType, 63 updateNodeDrawerActionType,
57 - triggerChange, 64 + useSaveAndRedoActionType,
58 }); 65 });
59 66
60 const { handleOnDragOver, handleOnDrop } = useDragCreate({ 67 const { handleOnDragOver, handleOnDrop } = useDragCreate({
@@ -72,13 +79,17 @@ @@ -72,13 +79,17 @@
72 79
73 const getDeleteDisplayState = computed(() => unref(flowActionType.getSelectedElements).length); 80 const getDeleteDisplayState = computed(() => unref(flowActionType.getSelectedElements).length);
74 81
  82 + const getDebugMarker = computed(() =>
  83 + flowActionType.getNodes.value.some((item) => (item.data as NodeData).data?.debugMode)
  84 + );
  85 +
75 const handleDeleteSelectionElements = () => { 86 const handleDeleteSelectionElements = () => {
76 - flowActionType.removeEdges(unref(flowActionType.getSelectedEdges));  
77 flowActionType.removeNodes(unref(flowActionType.getSelectedNodes)); 87 flowActionType.removeNodes(unref(flowActionType.getSelectedNodes));
78 }; 88 };
79 89
80 onMounted(() => { 90 onMounted(() => {
81 getCurrentPageMetaData(flowActionType); 91 getCurrentPageMetaData(flowActionType);
  92 + getCurrentRuleChainDetail();
82 }); 93 });
83 94
84 createFlowContext({ 95 createFlowContext({
@@ -127,7 +138,9 @@ @@ -127,7 +138,9 @@
127 <Icon class="!text-3xl !text-light-50" icon="mdi:delete" /> 138 <Icon class="!text-3xl !text-light-50" icon="mdi:delete" />
128 </button> 139 </button>
129 <button 140 <button
  141 + :class="getDebugMarker ? '!bg-orange-600 !opacity-100' : 'opacity-50'"
130 class="button-box-shadow w-14 h-14 flex justify-center items-center bg-gray-400 rounded-full opacity-50" 142 class="button-box-shadow w-14 h-14 flex justify-center items-center bg-gray-400 rounded-full opacity-50"
  143 + @click="handleRemoveDebug(flowActionType)"
131 > 144 >
132 <Icon class="!text-3xl !text-light-50" icon="carbon:debug" /> 145 <Icon class="!text-3xl !text-light-50" icon="carbon:debug" />
133 </button> 146 </button>
  1 +import { RouteLocationNormalizedLoaded } from 'vue-router';
1 import { RuleChainFieldsEnum, RuleChainFieldsNameEnum } from '../../../enum/formField/flow'; 2 import { RuleChainFieldsEnum, RuleChainFieldsNameEnum } from '../../../enum/formField/flow';
2 import { getRuleChains } from '/@/api/ruleDesigner'; 3 import { getRuleChains } from '/@/api/ruleDesigner';
3 import { FormSchema } from '/@/components/Form'; 4 import { FormSchema } from '/@/components/Form';
4 5
5 -const fetch = async (params: Recordable) => { 6 +const fetch = async (params: Recordable, ruleChainId: string) => {
6 try { 7 try {
7 const result = await getRuleChains(params); 8 const result = await getRuleChains(params);
8 - const data = result.data.map((item) => ({ label: item.name, value: item.id.id })); 9 + const data = result.data
  10 + .map((item) => ({ label: item.name, value: item.id.id }))
  11 + .filter((item) => item.value !== ruleChainId);
9 return data; 12 return data;
10 } catch (err) { 13 } catch (err) {
11 console.error(err); 14 console.error(err);
@@ -13,24 +16,27 @@ const fetch = async (params: Recordable) => { @@ -13,24 +16,27 @@ const fetch = async (params: Recordable) => {
13 } 16 }
14 }; 17 };
15 18
16 -export const formSchemas: FormSchema[] = [  
17 - {  
18 - field: RuleChainFieldsEnum.RULE_CHAIN_ID,  
19 - label: RuleChainFieldsNameEnum.RULE_CHAIN_ID,  
20 - component: 'ApiSearchSelect',  
21 - componentProps: () => {  
22 - return {  
23 - placeholder: '请选择所属产品',  
24 - showSearch: true,  
25 - params: {  
26 - pageSize: 50,  
27 - page: 0,  
28 - type: 'CORE',  
29 - },  
30 - api: fetch,  
31 - searchApi: fetch,  
32 - getPopupContainer: () => document.body,  
33 - }; 19 +export const getFormSchemas = (route: RouteLocationNormalizedLoaded): FormSchema[] => {
  20 + const ruleChainId = (route.params as Record<'id', string>).id;
  21 + return [
  22 + {
  23 + field: RuleChainFieldsEnum.RULE_CHAIN_ID,
  24 + label: RuleChainFieldsNameEnum.RULE_CHAIN_ID,
  25 + component: 'ApiSearchSelect',
  26 + componentProps: () => {
  27 + return {
  28 + placeholder: '请选择所属产品',
  29 + showSearch: true,
  30 + params: {
  31 + pageSize: 50,
  32 + page: 0,
  33 + type: 'CORE',
  34 + },
  35 + api: (params: Recordable) => fetch(params, ruleChainId),
  36 + searchApi: (params: Recordable) => fetch(params, ruleChainId),
  37 + getPopupContainer: () => document.body,
  38 + };
  39 + },
34 }, 40 },
35 - },  
36 -]; 41 + ];
  42 +};
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import type { CreateModalDefineExposeType } from '../../../types'; 2 import type { CreateModalDefineExposeType } from '../../../types';
3 import { BasicForm, useForm } from '/@/components/Form'; 3 import { BasicForm, useForm } from '/@/components/Form';
4 - import { formSchemas } from './create.config'; 4 + import { getFormSchemas } from './create.config';
5 import { NodeData } from '../../../types/node'; 5 import { NodeData } from '../../../types/node';
  6 + import { useRoute } from 'vue-router';
6 7
7 defineProps<{ 8 defineProps<{
8 config: NodeData; 9 config: NodeData;
9 }>(); 10 }>();
10 11
  12 + const ROUTE = useRoute();
  13 +
11 const [register, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({ 14 const [register, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({
12 - schemas: formSchemas, 15 + schemas: getFormSchemas(ROUTE),
13 showActionButtonGroup: false, 16 showActionButtonGroup: false,
14 }); 17 });
15 18
@@ -2,25 +2,32 @@ @@ -2,25 +2,32 @@
2 import { NodeProps } from '@vue-flow/core'; 2 import { NodeProps } from '@vue-flow/core';
3 import { Icon } from '/@/components/Icon'; 3 import { Icon } from '/@/components/Icon';
4 import { Tooltip } from 'ant-design-vue'; 4 import { Tooltip } from 'ant-design-vue';
  5 + import { useRouter } from 'vue-router';
  6 + import { NodeData } from '../../../types/node';
5 7
6 - defineProps<{  
7 - nodeProps?: NodeProps; 8 + const props = defineProps<{
  9 + nodeProps?: NodeProps<NodeData>;
8 }>(); 10 }>();
9 11
  12 + const ROUTER = useRouter();
  13 +
10 const handleClick = () => { 14 const handleClick = () => {
11 - // event.stopPropagation();  
12 - // event.preventDefault();  
13 - // console.log(props); 15 + const { data } = props.nodeProps?.data || ({} as NodeData);
  16 + const { configuration } = (data || {}) as { configuration: Record<'ruleChainId', string> };
  17 + if (configuration.ruleChainId) {
  18 + ROUTER.push(`/rule/chain/${configuration.ruleChainId}`);
  19 + }
14 }; 20 };
15 </script> 21 </script>
16 22
17 <template> 23 <template>
18 - <div class="w-full h-6 flex justify-end" @click="handleClick"> 24 + <div class="w-full h-6 flex justify-end">
19 <Tooltip color="#fff"> 25 <Tooltip color="#fff">
20 <template #title> 26 <template #title>
21 <span class="text-slate-500 italic">打开规则链</span> 27 <span class="text-slate-500 italic">打开规则链</span>
22 </template> 28 </template>
23 <Icon 29 <Icon
  30 + @click="handleClick"
24 icon="material-symbols:login" 31 icon="material-symbols:login"
25 class="cursor-pointer svg:text-lg svg:text-light-50 border-1 border-light-50 bg-purple-400 hover:bg-purple-500 rounded" 32 class="cursor-pointer svg:text-lg svg:text-light-50 border-1 border-light-50 bg-purple-400 hover:bg-purple-500 rounded"
26 /> 33 />
@@ -29,6 +29,11 @@ @@ -29,6 +29,11 @@
29 return config?.name; 29 return config?.name;
30 }); 30 });
31 31
  32 + const getIconUrl = computed(() => {
  33 + const { iconUrl } = unref(getNodeDefinition);
  34 + return iconUrl;
  35 + });
  36 +
32 const getIcon = computed(() => { 37 const getIcon = computed(() => {
33 const { icon } = unref(getNodeDefinition); 38 const { icon } = unref(getNodeDefinition);
34 const { category } = unref(getData); 39 const { category } = unref(getData);
@@ -64,10 +69,11 @@ @@ -64,10 +69,11 @@
64 </template> 69 </template>
65 <main 70 <main
66 class="basic-node-hover flex items-center w-44 h-12 rounded border px-4 py-2 border-gray-700 dark:text-light-50" 71 class="basic-node-hover flex items-center w-44 h-12 rounded border px-4 py-2 border-gray-700 dark:text-light-50"
67 - :style="{ backgroundColor: getBackgroundColor }" 72 + :style="{ backgroundColor: getBackgroundColor, outline: selected ? '3px solid red' : 'none' }"
68 > 73 >
69 <div> 74 <div>
70 - <Icon class="text-2xl dark:text-light-50" :icon="getIcon" /> 75 + <Icon v-if="!getIconUrl" class="text-2xl dark:text-light-50" :icon="getIcon" />
  76 + <img v-if="getIconUrl" :src="getIconUrl" class="w-4 h-4" />
71 </div> 77 </div>
72 <BasicToolbar v-if="!getData.config?.disableAction" v-bind="$props" /> 78 <BasicToolbar v-if="!getData.config?.disableAction" v-bind="$props" />
73 <div class="flex text-xs flex-col ml-2 text-left truncate"> 79 <div class="flex text-xs flex-col ml-2 text-left truncate">
  1 +import { NodeMouseEvent } from '@vue-flow/core';
  2 +import ContextMenuVue from './index.vue';
  3 +import { isClient } from '/@/utils/is';
  4 +import { createVNode, render, getCurrentInstance, onUnmounted, toRaw, unref } from 'vue';
  5 +
  6 +const menuManager: {
  7 + domList: Element[];
  8 + resolve: Fn;
  9 +} = {
  10 + domList: [],
  11 + resolve: () => {},
  12 +};
  13 +
  14 +const createContextMenu = function (
  15 + options: NodeMouseEvent,
  16 + params?: InstanceType<typeof ContextMenuVue>['$props']
  17 +) {
  18 + const { event } = options || {};
  19 +
  20 + if (!(params?.items && params?.items.length)) return;
  21 +
  22 + event && event?.preventDefault();
  23 +
  24 + if (!isClient) {
  25 + return;
  26 + }
  27 +
  28 + return new Promise((resolve) => {
  29 + const body = document.body;
  30 +
  31 + const container = document.createElement('div');
  32 + const propsData: Partial<InstanceType<typeof ContextMenuVue>['$props']> = {
  33 + ...params,
  34 + };
  35 +
  36 + if (options.event) {
  37 + const { clientX, clientY } = options.event as MouseEvent;
  38 + propsData.axis = { x: clientX, y: clientY };
  39 + }
  40 +
  41 + if (options.node) {
  42 + propsData.nodeData = toRaw(unref(options.node.data));
  43 + }
  44 +
  45 + const vm = createVNode(ContextMenuVue, propsData);
  46 + render(vm, container);
  47 +
  48 + const handleClick = function () {
  49 + menuManager.resolve('');
  50 + };
  51 +
  52 + menuManager.domList.push(container);
  53 +
  54 + const remove = function () {
  55 + menuManager.domList.forEach((dom: Element) => {
  56 + try {
  57 + dom && body.removeChild(dom);
  58 + } catch (error) {}
  59 + });
  60 + body.removeEventListener('click', handleClick);
  61 + body.removeEventListener('scroll', handleClick);
  62 + };
  63 +
  64 + menuManager.resolve = function (arg) {
  65 + remove();
  66 + resolve(arg);
  67 + };
  68 + remove();
  69 + body.appendChild(container);
  70 + body.addEventListener('click', handleClick);
  71 + body.addEventListener('scroll', handleClick);
  72 + });
  73 +};
  74 +
  75 +const destroyContextMenu = function () {
  76 + if (menuManager) {
  77 + menuManager.resolve('');
  78 + menuManager.domList = [];
  79 + }
  80 +};
  81 +
  82 +export function useContextMenu(authRemove = true) {
  83 + if (getCurrentInstance() && authRemove) {
  84 + onUnmounted(() => {
  85 + destroyContextMenu();
  86 + });
  87 + }
  88 + return [createContextMenu, destroyContextMenu];
  89 +}
  1 +export { default as RuleChainContextMenu } from './index.vue';
  2 +export { useContextMenu } from './createContextMenu';
  1 +export interface RuleChainContextMenuItemType {
  2 + icon?: string;
  3 + key?: string;
  4 + label?: string;
  5 + shortcutKey?: string;
  6 + divider?: boolean;
  7 + disabled?: boolean;
  8 + handler?: Fn;
  9 +}
  1 +<script setup lang="ts">
  2 + import { computed, CSSProperties, unref } from 'vue';
  3 + import { NodeData } from '../../../types/node';
  4 + import { RuleChainContextMenuItemType } from './index.type';
  5 + import { Icon } from '/@/components/Icon';
  6 + import { Divider } from 'ant-design-vue';
  7 +
  8 + const props = withDefaults(
  9 + defineProps<{
  10 + width?: number;
  11 + itemHeight?: number;
  12 + styles?: CSSProperties;
  13 + axis?: Record<'x' | 'y', number>;
  14 + items?: RuleChainContextMenuItemType[];
  15 + nodeData?: NodeData;
  16 + }>(),
  17 + {
  18 + width: 320,
  19 + itemHeight: 48,
  20 + axis: () => ({ x: 0, y: 0 }),
  21 + items: () => [],
  22 + nodeData: () => ({}),
  23 + }
  24 + );
  25 +
  26 + const getMenuHeight = computed(() => {
  27 + const { items, itemHeight } = props;
  28 + let dividerNumber = 0;
  29 + let menuNumber = 1;
  30 + for (const item of items) {
  31 + if (item.divider) dividerNumber++;
  32 + else menuNumber++;
  33 + }
  34 +
  35 + return itemHeight * menuNumber + dividerNumber + 8;
  36 + });
  37 +
  38 + const getStyle = computed((): CSSProperties => {
  39 + const { axis, styles, width } = props;
  40 + const { x, y } = axis;
  41 + const menuHeight = unref(getMenuHeight);
  42 + const menuWidth = width;
  43 + const body = document.body;
  44 +
  45 + const left = body.clientWidth < x + menuWidth ? x - menuWidth : x;
  46 + const top = body.clientHeight < y + menuHeight ? y - menuHeight : y;
  47 +
  48 + return {
  49 + ...styles,
  50 + position: 'absolute',
  51 + width: `${width}px`,
  52 + left: `${left + 1}px`,
  53 + top: `${top + 1}px`,
  54 + };
  55 + });
  56 +
  57 + const getNodeIconUrl = computed(() => {
  58 + const { config } = props.nodeData;
  59 + const { configurationDescriptor } = config || {};
  60 + const { nodeDefinition } = configurationDescriptor || {};
  61 + const { iconUrl } = nodeDefinition || {};
  62 + return iconUrl;
  63 + });
  64 +
  65 + const getNodeIcon = computed(() => {
  66 + const { nodeData } = props;
  67 + const { category, config } = nodeData;
  68 + const { icon: categoryIcon } = category || {};
  69 + const { configurationDescriptor } = config || {};
  70 + const { nodeDefinition } = configurationDescriptor || {};
  71 + const { icon } = nodeDefinition || {};
  72 +
  73 + return categoryIcon || icon;
  74 + });
  75 +
  76 + const getTitleBackgroundColor = computed(() => {
  77 + const { category, config } = props.nodeData;
  78 + const { backgroundColor: categoryBackgroundColor } = category || {};
  79 + const { backgroundColor } = config || {};
  80 + return categoryBackgroundColor || backgroundColor;
  81 + });
  82 +</script>
  83 +
  84 +<template>
  85 + <div
  86 + :style="getStyle"
  87 + class="bg-light-50 shadow-lg shadow-dark-50 z-50 rounded-md overflow-hidden pb-2"
  88 + >
  89 + <div
  90 + v-if="nodeData"
  91 + :style="{ backgroundColor: getTitleBackgroundColor, height: `${itemHeight}px` }"
  92 + class="flex items-center p-2"
  93 + >
  94 + <Icon v-if="!getNodeIconUrl" :icon="getNodeIcon" class="svg:text-2xl" />
  95 + <img v-if="getNodeIconUrl" :src="getNodeIconUrl" class="w-6 h-6" />
  96 + <div class="ml-4">
  97 + <div class="font-medium">{{ nodeData.config?.name }}</div>
  98 + <div class="text-xs">{{ nodeData.data?.name }}</div>
  99 + </div>
  100 + </div>
  101 + <div>
  102 + <template v-for="item in items" :key="item.key">
  103 + <div
  104 + v-if="!item.divider"
  105 + :style="{ height: `${itemHeight}px` }"
  106 + class="px-4 flex items-center cursor-pointer hover:bg-neutral-100"
  107 + :class="item.disabled && 'disables'"
  108 + @click="(event) => !item.disabled && item?.handler?.(event)"
  109 + >
  110 + <Icon :icon="item.icon" class="svg:text-2xl" />
  111 + <div class="flex-auto px-4">{{ item.label }}</div>
  112 + <div class="flex items-center"> {{ item.shortcutKey }} </div>
  113 + </div>
  114 + <Divider v-if="item.divider" class="!m-0" />
  115 + </template>
  116 + </div>
  117 + </div>
  118 +</template>
  119 +
  120 +<style lang="less" scoped>
  121 + .disables {
  122 + @apply pointer-events-none text-gray-300;
  123 + }
  124 +</style>
@@ -113,7 +113,11 @@ @@ -113,7 +113,11 @@
113 <Empty v-if="!shadowComponent" description="未找到链接组件" /> 113 <Empty v-if="!shadowComponent" description="未找到链接组件" />
114 </Spin> 114 </Spin>
115 </Tabs.TabPane> 115 </Tabs.TabPane>
116 - <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.EVENT]" :key="TabsPanelEnum.EVENT"> 116 + <Tabs.TabPane
  117 + v-if="nodeData?.created"
  118 + :tab="TabsPanelNameEnum[TabsPanelEnum.EVENT]"
  119 + :key="TabsPanelEnum.EVENT"
  120 + >
117 <BasicEvents :elementInfo="elementInfo" /> 121 <BasicEvents :elementInfo="elementInfo" />
118 </Tabs.TabPane> 122 </Tabs.TabPane>
119 <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.HELP]" :key="TabsPanelEnum.HELP"> 123 <Tabs.TabPane :tab="TabsPanelNameEnum[TabsPanelEnum.HELP]" :key="TabsPanelEnum.HELP">
@@ -58,3 +58,7 @@ @@ -58,3 +58,7 @@
58 .vue-flow__nodesselection-rect, .vue-flow__selection{ 58 .vue-flow__nodesselection-rect, .vue-flow__selection{
59 border-width: 3px; 59 border-width: 3px;
60 } 60 }
  61 +
  62 +.vue-flow__nodesselection-rect {
  63 + pointer-events: none;
  64 +}
@@ -137,6 +137,7 @@ export interface NodeData<T = BasicNodeFormData> { @@ -137,6 +137,7 @@ export interface NodeData<T = BasicNodeFormData> {
137 category?: CategoryConfigType; 137 category?: CategoryConfigType;
138 config?: NodeItemConfigType; 138 config?: NodeItemConfigType;
139 data?: T; 139 data?: T;
  140 + created?: boolean;
140 } 141 }
141 142
142 export interface EdgeData { 143 export interface EdgeData {
@@ -18,3 +18,20 @@ export interface ConnectionItemType { @@ -18,3 +18,20 @@ export interface ConnectionItemType {
18 toIndex: number; 18 toIndex: number;
19 type: string; 19 type: string;
20 } 20 }
  21 +
  22 +export interface RuleChainDetail {
  23 + id: Id;
  24 + createdTime: number;
  25 + additionalInfo: AdditionalInfo;
  26 + tenantId: Id;
  27 + name: Id;
  28 + type: string;
  29 + firstRuleNodeId: Id;
  30 + root: boolean;
  31 + debugMode: boolean;
  32 + configuration: any;
  33 +}
  34 +
  35 +export interface AdditionalInfo {
  36 + description: string;
  37 +}
@@ -47,12 +47,12 @@ @@ -47,12 +47,12 @@
47 {{ t('sys.login.loginButton') }} 47 {{ t('sys.login.loginButton') }}
48 </Button> 48 </Button>
49 </FormItem> 49 </FormItem>
50 - <ARow class="enter-x flex justify-between">  
51 - <ACol :md="11" :xs="24"> 50 + <ARow class="enter-x flex justify-center">
  51 + <!-- <ACol :md="11" :xs="24">
52 <Button block @click="setLoginState(LoginStateEnum.LOGIN)"> 52 <Button block @click="setLoginState(LoginStateEnum.LOGIN)">
53 {{ t('sys.login.userNameInFormTitle') }} 53 {{ t('sys.login.userNameInFormTitle') }}
54 </Button> 54 </Button>
55 - </ACol> 55 + </ACol> -->
56 <ACol :md="11" :xs="24"> 56 <ACol :md="11" :xs="24">
57 <Button block @click="setLoginState(LoginStateEnum.MOBILE)"> 57 <Button block @click="setLoginState(LoginStateEnum.MOBILE)">
58 {{ t('sys.login.mobileSignInFormTitle') }} 58 {{ t('sys.login.mobileSignInFormTitle') }}
@@ -81,6 +81,17 @@ @@ -81,6 +81,17 @@
81 confirm: handleDeleteOrBatchDelete.bind(null, record), 81 confirm: handleDeleteOrBatchDelete.bind(null, record),
82 }, 82 },
83 }, 83 },
  84 + {
  85 + label: '清除密码',
  86 + auth: 'api:yt:user:resetPassword',
  87 + icon: 'ant-design:delete-outlined',
  88 + color: 'error',
  89 + tooltip: '清除密码',
  90 + popConfirm: {
  91 + title: '是否确认清除密码',
  92 + confirm: handleClearPassword.bind(null, record),
  93 + },
  94 + },
84 ]" 95 ]"
85 /> 96 />
86 </template> 97 </template>
@@ -107,6 +118,8 @@ @@ -107,6 +118,8 @@
107 import { isAdmin } from '/@/enums/roleEnum'; 118 import { isAdmin } from '/@/enums/roleEnum';
108 import { TenantListItemRecord } from '/@/api/tenant/tenantInfo'; 119 import { TenantListItemRecord } from '/@/api/tenant/tenantInfo';
109 import { useFastEnter } from '/@/hooks/business/useFastEnter'; 120 import { useFastEnter } from '/@/hooks/business/useFastEnter';
  121 + import { clearUserPassword } from '/@/api/system/system';
  122 + import { useMessage } from '/@/hooks/web/useMessage';
110 123
111 export default defineComponent({ 124 export default defineComponent({
112 name: 'AccountManagement', 125 name: 'AccountManagement',
@@ -121,6 +134,7 @@ @@ -121,6 +134,7 @@
121 Popconfirm, 134 Popconfirm,
122 }, 135 },
123 setup() { 136 setup() {
  137 + const { createMessage } = useMessage();
124 const userInfo: any = getAuthCache(USER_INFO_KEY); 138 const userInfo: any = getAuthCache(USER_INFO_KEY);
125 const role: string = userInfo?.roles[0]; 139 const role: string = userInfo?.roles[0];
126 140
@@ -192,6 +206,13 @@ @@ -192,6 +206,13 @@
192 } 206 }
193 } 207 }
194 208
  209 + const handleClearPassword = async (record: Recordable) => {
  210 + const { id } = record;
  211 + if (!id) return;
  212 + const { message } = await clearUserPassword(id);
  213 + createMessage.success(message);
  214 + };
  215 +
195 return { 216 return {
196 handleLoginCustomAdmin, 217 handleLoginCustomAdmin,
197 registerTable, 218 registerTable,
@@ -206,6 +227,7 @@ @@ -206,6 +227,7 @@
206 handleDeleteOrBatchDelete, 227 handleDeleteOrBatchDelete,
207 isAdmin, 228 isAdmin,
208 role, 229 role,
  230 + handleClearPassword,
209 }; 231 };
210 }, 232 },
211 }); 233 });
@@ -5,14 +5,26 @@ export const formSchema: FormSchema[] = [ @@ -5,14 +5,26 @@ export const formSchema: FormSchema[] = [
5 field: 'passwordOld', 5 field: 'passwordOld',
6 label: '当前密码', 6 label: '当前密码',
7 component: 'InputPassword', 7 component: 'InputPassword',
  8 + componentProps: {
  9 + placeholder: '请输入当前密码',
  10 + },
8 required: true, 11 required: true,
9 }, 12 },
10 { 13 {
11 field: 'passwordNew', 14 field: 'passwordNew',
12 label: '新密码', 15 label: '新密码',
13 - component: 'StrengthMeter',  
14 - componentProps: {  
15 - placeholder: '新密码', 16 + component: 'InputPassword',
  17 + componentProps({ formModel, formActionType }) {
  18 + return {
  19 + placeholder: '请输入新密码',
  20 + onInput({ target }) {
  21 + const { value } = target;
  22 + const { confirmPassword } = formModel;
  23 + if (value === confirmPassword) {
  24 + formActionType.clearValidate('confirmPassword');
  25 + }
  26 + },
  27 + };
16 }, 28 },
17 rules: [ 29 rules: [
18 { 30 {
@@ -25,7 +37,9 @@ export const formSchema: FormSchema[] = [ @@ -25,7 +37,9 @@ export const formSchema: FormSchema[] = [
25 field: 'confirmPassword', 37 field: 'confirmPassword',
26 label: '确认密码', 38 label: '确认密码',
27 component: 'InputPassword', 39 component: 'InputPassword',
28 - 40 + componentProps: {
  41 + placeholder: '请输入确认密码',
  42 + },
29 dynamicRules: ({ values }) => { 43 dynamicRules: ({ values }) => {
30 return [ 44 return [
31 { 45 {
@@ -37,7 +51,6 @@ export const formSchema: FormSchema[] = [ @@ -37,7 +51,6 @@ export const formSchema: FormSchema[] = [
37 if (value !== values.passwordNew) { 51 if (value !== values.passwordNew) {
38 return Promise.reject('两次输入的密码不一致!'); 52 return Promise.reject('两次输入的密码不一致!');
39 } 53 }
40 -  
41 const pwdRegex = new RegExp(InputRegExp.PASSWORD_INPUT); 54 const pwdRegex = new RegExp(InputRegExp.PASSWORD_INPUT);
42 if (!pwdRegex.test(value)) { 55 if (!pwdRegex.test(value)) {
43 return Promise.reject( 56 return Promise.reject(
@@ -175,7 +175,7 @@ export const formSchema: FormSchema[] = [ @@ -175,7 +175,7 @@ export const formSchema: FormSchema[] = [
175 required: true, 175 required: true,
176 defaultValue: '0', 176 defaultValue: '0',
177 177
178 - label: '默认存储天数(0-无限制)', 178 + label: '默认存储TTL天数(0-无限制)',
179 colProps: { span: 12 }, 179 colProps: { span: 12 },
180 component: 'InputNumber', 180 component: 'InputNumber',
181 componentProps: { 181 componentProps: {
@@ -124,6 +124,8 @@ @@ -124,6 +124,8 @@
124 let ids = record?.id?.id; 124 let ids = record?.id?.id;
125 await deleteTenantProfileApi(ids); 125 await deleteTenantProfileApi(ids);
126 createMessage.success('删除成功'); 126 createMessage.success('删除成功');
  127 + selectedRowKeys.length = 0;
  128 + disabled.value = true;
127 reload(); 129 reload();
128 } else { 130 } else {
129 createMessage.warning(message); 131 createMessage.warning(message);
@@ -148,9 +150,6 @@ @@ -148,9 +150,6 @@
148 createMessage.success('删除成功'); 150 createMessage.success('删除成功');
149 reload(); 151 reload();
150 selectedRowKeys.length = 0; 152 selectedRowKeys.length = 0;
151 - setTimeout(() => {  
152 - disabled.value = false;  
153 - }, 3000);  
154 }); 153 });
155 }; 154 };
156 155
@@ -46,7 +46,7 @@ @@ -46,7 +46,7 @@
46 label: '账号', 46 label: '账号',
47 component: 'Input', 47 component: 'Input',
48 componentProps: { 48 componentProps: {
49 - maxLength: 255, 49 + maxLength: 64,
50 placeholder: '请输入账号', 50 placeholder: '请输入账号',
51 }, 51 },
52 dynamicRules: ({ values }) => { 52 dynamicRules: ({ values }) => {
@@ -87,7 +87,7 @@ @@ -87,7 +87,7 @@
87 required: true, 87 required: true,
88 component: 'Input', 88 component: 'Input',
89 componentProps: { 89 componentProps: {
90 - maxLength: 255, 90 + maxLength: 64,
91 placeholder: '请输入真实名字', 91 placeholder: '请输入真实名字',
92 }, 92 },
93 rules: chineseAndEnlishRule, 93 rules: chineseAndEnlishRule,
@@ -97,12 +97,20 @@ @@ -97,12 +97,20 @@
97 label: '电话号码', 97 label: '电话号码',
98 required: true, 98 required: true,
99 component: 'Input', 99 component: 'Input',
  100 + componentProps: {
  101 + maxLength: 11,
  102 + placeholder: '请输入电话号码',
  103 + },
100 rules: phoneRule, 104 rules: phoneRule,
101 }, 105 },
102 { 106 {
103 field: 'email', 107 field: 'email',
104 label: '邮件', 108 label: '邮件',
105 component: 'Input', 109 component: 'Input',
  110 + componentProps: {
  111 + maxLength: 64,
  112 + placeholder: '请输入邮件',
  113 + },
106 rules: emailRule, 114 rules: emailRule,
107 }, 115 },
108 { 116 {
@@ -44,6 +44,7 @@ @@ -44,6 +44,7 @@
44 icon: 'ant-design:usergroup-add-outlined', 44 icon: 'ant-design:usergroup-add-outlined',
45 label: '租户管理员', 45 label: '租户管理员',
46 onClick: handleTenantAdminDrawer.bind(null, record), 46 onClick: handleTenantAdminDrawer.bind(null, record),
  47 + disabled: record.enabled === false,
47 }, 48 },
48 { 49 {
49 icon: 'clarity:note-edit-line', 50 icon: 'clarity:note-edit-line',
@@ -110,14 +110,20 @@ export const tenantFormSchema: FormSchema[] = [ @@ -110,14 +110,20 @@ export const tenantFormSchema: FormSchema[] = [
110 label: '租户角色', 110 label: '租户角色',
111 component: 'ApiSelect', 111 component: 'ApiSelect',
112 required: true, 112 required: true,
113 - componentProps: {  
114 - mode: 'multiple',  
115 - api: getAllRoleList,  
116 - params: {  
117 - roleType: RoleEnum.TENANT_ADMIN,  
118 - },  
119 - labelField: 'name',  
120 - valueField: 'id', 113 + componentProps: () => {
  114 + return {
  115 + api: async () => {
  116 + const res = await getAllRoleList({ roleType: RoleEnum.TENANT_ADMIN });
  117 + return res;
  118 + },
  119 + mode: 'multiple',
  120 + showSearch: true,
  121 + labelField: 'name',
  122 + valueField: 'id',
  123 + filterOption: (inputValue: string, options: Record<'label' | 'value', string>) => {
  124 + return options.label.toLowerCase().includes(inputValue.toLowerCase());
  125 + },
  126 + };
121 }, 127 },
122 }, 128 },
123 { 129 {
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 import { BasicForm, useForm } from '/@/components/Form/index'; 32 import { BasicForm, useForm } from '/@/components/Form/index';
33 import { formSchema } from './role.data'; 33 import { formSchema } from './role.data';
34 import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; 34 import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
35 - import { BasicTree, CheckEvent, TreeActionType, TreeItem } from '/@/components/Tree'; 35 + import { BasicTree, CheckEvent, TreeActionType, TreeItem, CheckKeys } from '/@/components/Tree';
36 const { t } = useI18n(); //加载国际化 36 const { t } = useI18n(); //加载国际化
37 // 加载菜单数据 37 // 加载菜单数据
38 import { getAdminMenuList, getMenuList, getMenusIdsByRoleId } from '/@/api/sys/menu'; 38 import { getAdminMenuList, getMenuList, getMenusIdsByRoleId } from '/@/api/sys/menu';
@@ -60,7 +60,7 @@ @@ -60,7 +60,7 @@
60 const treeRef = ref<Nullable<TreeActionType>>(null); 60 const treeRef = ref<Nullable<TreeActionType>>(null);
61 const checked = ref<string[]>([]); //需要选中的节点 61 const checked = ref<string[]>([]); //需要选中的节点
62 const spinning = ref(false); 62 const spinning = ref(false);
63 - const checkedKeysWithHalfChecked = ref<string[]>([]); 63 + const checkedKeysWithHalfChecked = ref<(string | number)[]>([]);
64 64
65 const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({ 65 const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
66 labelWidth: 90, 66 labelWidth: 90,
@@ -137,7 +137,12 @@ @@ -137,7 +137,12 @@
137 async function handleSubmit() { 137 async function handleSubmit() {
138 setDrawerProps({ loading: true, confirmLoading: true }); 138 setDrawerProps({ loading: true, confirmLoading: true });
139 const { createMessage } = useMessage(); 139 const { createMessage } = useMessage();
140 - const treeCheckedKeys: string[] = (unref(treeRef)?.getCheckedKeys() as string[]) || []; 140 + let treeCheckedKeys: string[] | CheckKeys =
  141 + (unref(treeRef)?.getCheckedKeys() as string[] | CheckKeys) || [];
  142 + //fix 取消层级独立后(unref(treeRef)?.getCheckedKeys() as string[])的数据不是数组,是{checked:[],halfChecked:[]}对象,迭代报错
  143 + if (!Array.isArray(treeCheckedKeys)) {
  144 + treeCheckedKeys = treeCheckedKeys?.checked;
  145 + }
141 const menu = [...new Set([...unref(checkedKeysWithHalfChecked), ...treeCheckedKeys])]; 146 const menu = [...new Set([...unref(checkedKeysWithHalfChecked), ...treeCheckedKeys])];
142 try { 147 try {
143 const values = await validate(); 148 const values = await validate();
@@ -237,7 +242,12 @@ @@ -237,7 +242,12 @@
237 return needExcludeKeys; 242 return needExcludeKeys;
238 }; 243 };
239 244
240 - const handleCheckClick = (selectedKeys: string[], event: CheckEvent) => { 245 + const handleCheckClick = (selectedKeys: CheckKeys, event: CheckEvent) => {
  246 + //fix 取消层级独立后selectedKeys不是数组,是{checked:[],halfChecked:[]}对象 迭代报错
  247 + if (!Array.isArray(selectedKeys)) {
  248 + selectedKeys = selectedKeys?.checked;
  249 + event.halfCheckedKeys = [];
  250 + }
241 checkedKeysWithHalfChecked.value = [ 251 checkedKeysWithHalfChecked.value = [
242 ...selectedKeys, 252 ...selectedKeys,
243 ...(event.halfCheckedKeys as string[]), 253 ...(event.halfCheckedKeys as string[]),