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[]), | ... | ... |