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