Jelajahi Sumber

子节点展示bug修复

ye-zhaojia 1 hari lalu
induk
melakukan
4dde756176

+ 6 - 6
package-lock.json

@@ -8068,9 +8068,9 @@
       }
     },
     "node_modules/ws": {
-      "version": "8.21.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
-      "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+      "version": "8.21.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+      "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
       "dev": true,
       "engines": {
         "node": ">=10.0.0"
@@ -13805,9 +13805,9 @@
       }
     },
     "ws": {
-      "version": "8.21.0",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
-      "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+      "version": "8.21.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+      "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
       "dev": true,
       "requires": {}
     },

+ 2 - 2
src/views/flow/designer/DesignerCanvas.vue

@@ -46,7 +46,7 @@ import { VideoPlay, User, Message, Operation, CircleCheck } from '@element-plus/
 import type { Role } from '@/types/system'
 import SubNodeDrawer from './SubNodeDrawer.vue'
 
-defineProps<{
+const props = defineProps<{
   setContainer: (el: Element | ComponentPublicInstance | null) => void
   lf: any
   selectedNode: any
@@ -56,7 +56,7 @@ defineProps<{
   drawerVisible: boolean
 }>()
 
-defineEmits<{
+const emit = defineEmits<{
   drop: [e: DragEvent]
   save: []
   clear: []

+ 49 - 56
src/views/flow/designer/SubNodeDrawer.vue

@@ -7,19 +7,13 @@
         :style="drawerPositionStyle"
       >
         <div class="sub-node-drawer">
-          <div class="drawer-header">
-            <span class="drawer-title">子节点配置</span>
-            <el-icon class="drawer-close" @click="emit('update:visible', false)">
-              <Close />
-            </el-icon>
-          </div>
           <div class="drawer-body">
             <div
               v-for="(sub, index) in subNodes"
-              :key="sub.id"
+              :key="sub.id || index"
               class="sub-node-card"
               :class="{ active: selectedIndex === index }"
-              @click="emit('select', index)"
+              @click="onSelectSubNode(index)"
             >
               <div class="card-index">{{ index + 1 }}</div>
               <div class="card-info">
@@ -27,12 +21,12 @@
                 <div class="card-role">{{ getSubNodeAssigneeDisplay(sub) }}</div>
               </div>
               <div class="card-actions" @click.stop>
-                <el-icon class="delete-icon" @click="emit('remove', index)">
+                <el-icon class="delete-icon" @click="onRemove(index)">
                   <Delete />
                 </el-icon>
               </div>
             </div>
-            <div class="sub-node-card add-card" @click="emit('add')">
+            <div class="sub-node-card add-card" @click="onAdd">
               <el-icon class="add-icon"><Plus /></el-icon>
               <span>添加子节点</span>
             </div>
@@ -42,7 +36,8 @@
     </Transition>
 
     <div class="sub-node-badge" :style="badgeStyle" @click="toggleVisible">
-      <span>{{ subNodes.length }} 子节点</span>
+      <el-icon><Document /></el-icon>
+      <span>子节点 {{ subNodes.length }}</span>
       <el-icon class="badge-arrow" :class="{ open: visible }">
         <CaretBottom v-if="visible" />
         <CaretTop v-else />
@@ -53,16 +48,16 @@
 
 <script setup lang="ts">
 import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
-import { Close, Plus, Delete, CaretTop, CaretBottom } from '@element-plus/icons-vue'
+import { Plus, Delete, CaretTop, CaretBottom, Document } from '@element-plus/icons-vue'
 import type { Role } from '@/types/system'
 
 const props = defineProps<{
   lf: any
   node: any
   nodeProps: Record<string, any>
+  roleList: Role[]
   selectedIndex: number | null
   visible: boolean
-  roleList: Role[]
 }>()
 
 const emit = defineEmits<{
@@ -80,25 +75,32 @@ const subNodes = computed(() => {
   return props.nodeProps.subNodes || []
 })
 
+const CARD_HEIGHT = 58
+const CARD_GAP = 8
+const BADGE_HEIGHT = 26
+const VERTICAL_GAP = 6
 const NODE_HALF_HEIGHT = 30
-const VERTICAL_GAP = 4
-const BADGE_HEIGHT = 22
 
 const position = ref({ x: 0, y: 0 })
 
+const drawerHeight = computed(() => {
+  const count = subNodes.value.length + 1 // +1 for add-card
+  return count * CARD_HEIGHT + Math.max(0, count - 1) * CARD_GAP + 16 // padding
+})
+
 const badgeTop = computed(() => {
   return position.value.y - NODE_HALF_HEIGHT - BADGE_HEIGHT - VERTICAL_GAP
 })
 
-const drawerBottom = computed(() => {
-  return badgeTop.value - VERTICAL_GAP
+const drawerTop = computed(() => {
+  return badgeTop.value - VERTICAL_GAP - drawerHeight.value
 })
 
 const drawerPositionStyle = computed(() => ({
   position: 'absolute' as const,
   left: `${position.value.x}px`,
-  top: `${drawerBottom.value}px`,
-  transform: 'translate(-50%, -100%)',
+  top: `${drawerTop.value}px`,
+  transform: 'translate(-50%, 0)',
 }))
 
 const badgeStyle = computed(() => ({
@@ -112,6 +114,18 @@ function toggleVisible() {
   emit('update:visible', !props.visible)
 }
 
+function onSelectSubNode(index: number) {
+  emit('select', index)
+}
+
+function onAdd() {
+  emit('add')
+}
+
+function onRemove(index: number) {
+  emit('remove', index)
+}
+
 function getRoleName(roleCode?: string): string {
   if (!roleCode) return ''
   const role = props.roleList.find((r) => r.roleCode === roleCode)
@@ -126,15 +140,19 @@ function getSubNodeAssigneeDisplay(sub: any): string {
 }
 
 function computePosition() {
-  if (!props.lf || !props.node?.id) return
+  if (!props.node?.id) return
   try {
-    const nodeData = props.lf.getNodeData(props.node.id)
-    if (!nodeData) return
-    const htmlPoint = props.lf.graphModel.transformModel.CanvasPointToHtmlPoint([
-      nodeData.x,
-      nodeData.y,
-    ])
-    position.value = { x: htmlPoint[0], y: htmlPoint[1] }
+    // 通过选中的 DOM 元素计算节点中心位置(最可靠)
+    const nodeEl = document.querySelector('.lf-node-selected')
+    if (!nodeEl) return
+    const shapeEl = nodeEl.querySelector('.lf-basic-shape') || nodeEl
+    const rect = shapeEl.getBoundingClientRect()
+    const canvasEl = document.querySelector('.canvas-area')
+    const canvasRect = canvasEl ? canvasEl.getBoundingClientRect() : { left: 0, top: 0 }
+    position.value = {
+      x: rect.left + rect.width / 2 - canvasRect.left,
+      y: rect.top + rect.height / 2 - canvasRect.top,
+    }
   } catch {
     // ignore
   }
@@ -205,7 +223,7 @@ onUnmounted(() => {
 }
 
 .sub-node-drawer {
-  width: 240px;
+  width: 220px;
   background: #fff;
   border-radius: 8px;
   box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
@@ -213,31 +231,6 @@ onUnmounted(() => {
   overflow: hidden;
 }
 
-.drawer-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 10px 12px;
-  border-bottom: 1px solid #ebeef5;
-  background: #fafafa;
-}
-
-.drawer-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-}
-
-.drawer-close {
-  font-size: 14px;
-  color: #909399;
-  cursor: pointer;
-}
-
-.drawer-close:hover {
-  color: #409eff;
-}
-
 .drawer-body {
   padding: 12px;
   max-height: 320px;
@@ -247,7 +240,7 @@ onUnmounted(() => {
 .sub-node-card {
   display: flex;
   align-items: center;
-  padding: 12px;
+  padding: 10px 12px;
   margin-bottom: 8px;
   border-radius: 8px;
   border: 1px solid #ebeef5;
@@ -339,7 +332,7 @@ onUnmounted(() => {
   display: inline-flex;
   align-items: center;
   gap: 4px;
-  padding: 2px 10px;
+  padding: 4px 10px;
   border-radius: 12px;
   background: #409eff;
   color: #fff;
@@ -367,6 +360,6 @@ onUnmounted(() => {
 .drawer-enter-from,
 .drawer-leave-to {
   opacity: 0;
-  transform: translate(-50%, -100%) scale(0.95);
+  transform: translate(-50%, 10px) scale(0.95);
 }
 </style>

+ 6 - 5
src/views/flow/designer/index.vue

@@ -22,7 +22,7 @@
       @clear="handleClear"
       @back="goBack"
       @update:drawer-visible="drawerVisible = $event"
-      @select-sub-node="selectedSubNodeIndex = $event"
+      @select-sub-node="selectedSubNodeIndex = $event; drawerVisible = true"
       @add-sub-node="addSubNodeFromCanvas"
       @remove-sub-node="removeSubNodeFromCanvas"
     />
@@ -99,6 +99,7 @@ const {
   generateSubNodeId,
   saveCurrentNodeProps,
   deleteSelectedNode,
+  selectNodeById,
   saveDialogVisible,
   optionDialogVisible,
   saveForm,
@@ -152,12 +153,12 @@ function removeSubNodeFromCanvas(index: number) {
   saveCurrentNodeProps()
 }
 
-// 选中节点时,若已启用子节点则自动展开画布抽屉
+// 选中节点或子节点开关变化时,自动展开/收起画布抽屉
 watch(
-  () => selectedNode.value,
-  (node) => {
+  () => [selectedNode.value, nodeProps.subNodesEnabled],
+  ([node]) => {
     selectedSubNodeIndex.value = null
-    if (node && node.type === 'approval-node' && nodeProps.subNodesEnabled && nodeProps.subNodes?.length > 0) {
+    if (node && node.type === 'approval-node' && nodeProps.subNodesEnabled) {
       drawerVisible.value = true
     } else {
       drawerVisible.value = false

+ 33 - 1
src/views/flow/designer/useFlowDesigner.ts

@@ -167,6 +167,8 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
       if (type === 'approval-node') {
         syncNodeTextWithSubNodeBadge(selectedNode.value.id)
       }
+      // 通知画布刷新子节点 overlay
+      lf.emit('properties:changed', { nodeId: selectedNode.value.id })
     }
   }
 
@@ -665,9 +667,23 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
         const props = node.properties || {}
         const assigneeType = props.assigneeType ?? 'ROLE'
         const assigneeValue = props.assigneeValue || props.approver
-        if (assigneeType === 'ROLE' && !assigneeValue) {
+        if (assigneeType === 'ROLE' && !assigneeValue && props.subNodesEnabled !== true) {
           return `审批节点 "${node.text || node.name}" 未选择审批角色`
         }
+        if (props.subNodesEnabled === true) {
+          const subNodes = props.subNodes || []
+          if (subNodes.length === 0) {
+            return `审批节点 "${node.text || node.name}" 已启用子节点,请至少配置一个子节点`
+          }
+          for (let i = 0; i < subNodes.length; i++) {
+            const sub = subNodes[i]
+            const subAssigneeType = sub.assigneeType ?? 'ROLE'
+            const subAssigneeValue = sub.assigneeValue
+            if (subAssigneeType === 'ROLE' && !subAssigneeValue) {
+              return `审批节点 "${node.text || node.name}" 的子节点 ${i + 1} 未选择审批角色`
+            }
+          }
+        }
       }
     }
     // 检查孤立节点
@@ -820,6 +836,21 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
     outgoingEdges.value = []
   }
 
+  function selectNodeById(nodeId: string) {
+    if (!lf) return
+    try {
+      const nodeData = lf.getNodeData(nodeId)
+      if (!nodeData) return
+      lf.selectElementById(nodeId)
+      saveCurrentNodeProps()
+      selectedNode.value = nodeData
+      loadNodeProps(nodeData)
+      loadOutgoingEdges(nodeData.id)
+    } catch {
+      // ignore
+    }
+  }
+
   function handleClear() {
     if (!lf) return
     lf.clearData()
@@ -886,6 +917,7 @@ export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
     updateNodeName,
     generateSubNodeId,
     deleteSelectedNode,
+    selectNodeById,
     // save dialog
     saveDialogVisible,
     saveForm,