فهرست منبع

子节点画布调整

ye-zhaojia 1 روز پیش
والد
کامیت
f41080ab8a
3فایلهای تغییر یافته به همراه116 افزوده شده و 40 حذف شده
  1. 2 0
      src/views/flow/designer/DesignerCanvas.vue
  2. 102 40
      src/views/flow/designer/SubNodeDrawer.vue
  3. 12 0
      src/views/flow/designer/index.vue

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

@@ -34,6 +34,7 @@
         @update:visible="$emit('update:drawerVisible', $event)"
         @select="$emit('selectSubNode', $event)"
         @add="$emit('addSubNode')"
+        @remove="$emit('removeSubNode', $event)"
       />
     </div>
   </div>
@@ -63,6 +64,7 @@ defineEmits<{
   'update:drawerVisible': [visible: boolean]
   selectSubNode: [index: number]
   addSubNode: []
+  removeSubNode: [index: number]
 }>()
 
 const nodeTypes = [

+ 102 - 40
src/views/flow/designer/SubNodeDrawer.vue

@@ -3,46 +3,57 @@
     <Transition name="drawer">
       <div
         v-show="visible"
-        class="sub-node-drawer"
-        :style="drawerStyle"
+        class="drawer-position"
+        :style="drawerPositionStyle"
       >
-        <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"
-            class="sub-node-card"
-            :class="{ active: selectedIndex === index }"
-            @click="emit('select', index)"
-          >
-            <div class="card-index">{{ index + 1 }}</div>
-            <div class="card-info">
-              <div class="card-name">{{ sub.name || `子节点 ${index + 1}` }}</div>
-              <div class="card-role">{{ getRoleName(sub.assigneeValue) || '未设置审批角色' }}</div>
-            </div>
+        <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="sub-node-card add-card" @click="emit('add')">
-            <el-icon class="add-icon"><Plus /></el-icon>
-            <span>添加子节点</span>
+          <div class="drawer-body">
+            <div
+              v-for="(sub, index) in subNodes"
+              :key="sub.id"
+              class="sub-node-card"
+              :class="{ active: selectedIndex === index }"
+              @click="emit('select', index)"
+            >
+              <div class="card-index">{{ index + 1 }}</div>
+              <div class="card-info">
+                <div class="card-name">{{ sub.name || `子节点 ${index + 1}` }}</div>
+                <div class="card-role">{{ getRoleName(sub.assigneeValue) || '未设置审批角色' }}</div>
+              </div>
+              <div class="card-actions" @click.stop>
+                <el-icon class="delete-icon" @click="emit('remove', index)">
+                  <Delete />
+                </el-icon>
+              </div>
+            </div>
+            <div class="sub-node-card add-card" @click="emit('add')">
+              <el-icon class="add-icon"><Plus /></el-icon>
+              <span>添加子节点</span>
+            </div>
           </div>
         </div>
       </div>
     </Transition>
 
-    <div class="sub-node-badge" :style="badgeStyle">
-      {{ subNodes.length }} 子节点
+    <div class="sub-node-badge" :style="badgeStyle" @click="toggleVisible">
+      <span>{{ subNodes.length }} 子节点</span>
+      <el-icon class="badge-arrow" :class="{ open: visible }">
+        <CaretBottom v-if="visible" />
+        <CaretTop v-else />
+      </el-icon>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
-import { Close, Plus } from '@element-plus/icons-vue'
+import { Close, Plus, Delete, CaretTop, CaretBottom } from '@element-plus/icons-vue'
 import type { Role } from '@/types/system'
 
 const props = defineProps<{
@@ -58,6 +69,7 @@ const emit = defineEmits<{
   (e: 'update:visible', visible: boolean): void
   (e: 'select', index: number): void
   (e: 'add'): void
+  (e: 'remove', index: number): void
 }>()
 
 const showOverlay = computed(() => {
@@ -68,21 +80,38 @@ const subNodes = computed(() => {
   return props.nodeProps.subNodes || []
 })
 
+const NODE_HALF_HEIGHT = 30
+const VERTICAL_GAP = 4
+const BADGE_HEIGHT = 22
+
 const position = ref({ x: 0, y: 0 })
-const drawerStyle = computed(() => ({
+
+const badgeTop = computed(() => {
+  return position.value.y - NODE_HALF_HEIGHT - BADGE_HEIGHT - VERTICAL_GAP
+})
+
+const drawerBottom = computed(() => {
+  return badgeTop.value - VERTICAL_GAP
+})
+
+const drawerPositionStyle = computed(() => ({
   position: 'absolute' as const,
   left: `${position.value.x}px`,
-  top: `${position.value.y}px`,
-  transform: 'translate(-50%, -100%) translateY(-12px)',
+  top: `${drawerBottom.value}px`,
+  transform: 'translate(-50%, -100%)',
 }))
 
 const badgeStyle = computed(() => ({
   position: 'absolute' as const,
   left: `${position.value.x}px`,
-  top: `${position.value.y + 42}px`,
+  top: `${badgeTop.value}px`,
   transform: 'translate(-50%, 0)',
 }))
 
+function toggleVisible() {
+  emit('update:visible', !props.visible)
+}
+
 function getRoleName(roleCode?: string): string {
   if (!roleCode) return ''
   const role = props.roleList.find((r) => r.roleCode === roleCode)
@@ -161,11 +190,15 @@ onUnmounted(() => {
   width: 100%;
   height: 100%;
   pointer-events: none;
-  z-index: 20;
+  z-index: 100;
+}
+
+.drawer-position {
+  z-index: 101;
 }
 
 .sub-node-drawer {
-  width: 220px;
+  width: 240px;
   background: #fff;
   border-radius: 8px;
   box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
@@ -199,7 +232,7 @@ onUnmounted(() => {
 }
 
 .drawer-body {
-  padding: 10px;
+  padding: 12px;
   max-height: 320px;
   overflow-y: auto;
 }
@@ -207,9 +240,9 @@ onUnmounted(() => {
 .sub-node-card {
   display: flex;
   align-items: center;
-  padding: 10px;
+  padding: 12px;
   margin-bottom: 8px;
-  border-radius: 6px;
+  border-radius: 8px;
   border: 1px solid #ebeef5;
   background: #fff;
   cursor: pointer;
@@ -232,8 +265,8 @@ onUnmounted(() => {
 }
 
 .card-index {
-  width: 22px;
-  height: 22px;
+  width: 24px;
+  height: 24px;
   border-radius: 50%;
   background: #409eff;
   color: #fff;
@@ -251,7 +284,7 @@ onUnmounted(() => {
 }
 
 .card-name {
-  font-size: 13px;
+  font-size: 14px;
   font-weight: 500;
   color: #303133;
   overflow: hidden;
@@ -268,6 +301,21 @@ onUnmounted(() => {
   white-space: nowrap;
 }
 
+.card-actions {
+  flex-shrink: 0;
+  margin-left: 8px;
+}
+
+.delete-icon {
+  color: #f56c6c;
+  font-size: 14px;
+  cursor: pointer;
+}
+
+.delete-icon:hover {
+  color: #ff4d4f;
+}
+
 .add-card {
   justify-content: center;
   color: #409eff;
@@ -281,13 +329,27 @@ onUnmounted(() => {
 }
 
 .sub-node-badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
   padding: 2px 10px;
   border-radius: 12px;
   background: #409eff;
   color: #fff;
   font-size: 12px;
   pointer-events: auto;
+  cursor: pointer;
   box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+  z-index: 101;
+}
+
+.badge-arrow {
+  transition: transform 0.2s;
+  font-size: 10px;
+}
+
+.badge-arrow.open {
+  transform: rotate(180deg);
 }
 
 .drawer-enter-active,
@@ -298,6 +360,6 @@ onUnmounted(() => {
 .drawer-enter-from,
 .drawer-leave-to {
   opacity: 0;
-  transform: translate(-50%, -100%) translateY(-12px) scale(0.95);
+  transform: translate(-50%, -100%) scale(0.95);
 }
 </style>

+ 12 - 0
src/views/flow/designer/index.vue

@@ -24,6 +24,7 @@
       @update:drawer-visible="drawerVisible = $event"
       @select-sub-node="selectedSubNodeIndex = $event"
       @add-sub-node="addSubNodeFromCanvas"
+      @remove-sub-node="removeSubNodeFromCanvas"
     />
 
     <DesignerPropertyPanel
@@ -139,6 +140,17 @@ function addSubNodeFromCanvas() {
   saveCurrentNodeProps()
 }
 
+function removeSubNodeFromCanvas(index: number) {
+  if (!Array.isArray(nodeProps.subNodes)) return
+  nodeProps.subNodes.splice(index, 1)
+  if (selectedSubNodeIndex.value === index) {
+    selectedSubNodeIndex.value = null
+  } else if (selectedSubNodeIndex.value !== null && selectedSubNodeIndex.value > index) {
+    selectedSubNodeIndex.value = selectedSubNodeIndex.value - 1
+  }
+  saveCurrentNodeProps()
+}
+
 // 选中节点时,若已启用子节点则自动展开画布抽屉
 watch(
   () => selectedNode.value,