|
@@ -3,46 +3,57 @@
|
|
|
<Transition name="drawer">
|
|
<Transition name="drawer">
|
|
|
<div
|
|
<div
|
|
|
v-show="visible"
|
|
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>
|
|
|
- <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>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</Transition>
|
|
</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>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
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'
|
|
import type { Role } from '@/types/system'
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
const props = defineProps<{
|
|
@@ -58,6 +69,7 @@ const emit = defineEmits<{
|
|
|
(e: 'update:visible', visible: boolean): void
|
|
(e: 'update:visible', visible: boolean): void
|
|
|
(e: 'select', index: number): void
|
|
(e: 'select', index: number): void
|
|
|
(e: 'add'): void
|
|
(e: 'add'): void
|
|
|
|
|
+ (e: 'remove', index: number): void
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
const showOverlay = computed(() => {
|
|
const showOverlay = computed(() => {
|
|
@@ -68,21 +80,38 @@ const subNodes = computed(() => {
|
|
|
return props.nodeProps.subNodes || []
|
|
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 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,
|
|
position: 'absolute' as const,
|
|
|
left: `${position.value.x}px`,
|
|
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(() => ({
|
|
const badgeStyle = computed(() => ({
|
|
|
position: 'absolute' as const,
|
|
position: 'absolute' as const,
|
|
|
left: `${position.value.x}px`,
|
|
left: `${position.value.x}px`,
|
|
|
- top: `${position.value.y + 42}px`,
|
|
|
|
|
|
|
+ top: `${badgeTop.value}px`,
|
|
|
transform: 'translate(-50%, 0)',
|
|
transform: 'translate(-50%, 0)',
|
|
|
}))
|
|
}))
|
|
|
|
|
|
|
|
|
|
+function toggleVisible() {
|
|
|
|
|
+ emit('update:visible', !props.visible)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function getRoleName(roleCode?: string): string {
|
|
function getRoleName(roleCode?: string): string {
|
|
|
if (!roleCode) return ''
|
|
if (!roleCode) return ''
|
|
|
const role = props.roleList.find((r) => r.roleCode === roleCode)
|
|
const role = props.roleList.find((r) => r.roleCode === roleCode)
|
|
@@ -161,11 +190,15 @@ onUnmounted(() => {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|
|
|
- z-index: 20;
|
|
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-position {
|
|
|
|
|
+ z-index: 101;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.sub-node-drawer {
|
|
.sub-node-drawer {
|
|
|
- width: 220px;
|
|
|
|
|
|
|
+ width: 240px;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
@@ -199,7 +232,7 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.drawer-body {
|
|
.drawer-body {
|
|
|
- padding: 10px;
|
|
|
|
|
|
|
+ padding: 12px;
|
|
|
max-height: 320px;
|
|
max-height: 320px;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
}
|
|
}
|
|
@@ -207,9 +240,9 @@ onUnmounted(() => {
|
|
|
.sub-node-card {
|
|
.sub-node-card {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 10px;
|
|
|
|
|
|
|
+ padding: 12px;
|
|
|
margin-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
|
- border-radius: 6px;
|
|
|
|
|
|
|
+ border-radius: 8px;
|
|
|
border: 1px solid #ebeef5;
|
|
border: 1px solid #ebeef5;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
@@ -232,8 +265,8 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.card-index {
|
|
.card-index {
|
|
|
- width: 22px;
|
|
|
|
|
- height: 22px;
|
|
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ height: 24px;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
background: #409eff;
|
|
background: #409eff;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
@@ -251,7 +284,7 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.card-name {
|
|
.card-name {
|
|
|
- font-size: 13px;
|
|
|
|
|
|
|
+ font-size: 14px;
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
@@ -268,6 +301,21 @@ onUnmounted(() => {
|
|
|
white-space: nowrap;
|
|
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 {
|
|
.add-card {
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
color: #409eff;
|
|
color: #409eff;
|
|
@@ -281,13 +329,27 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.sub-node-badge {
|
|
.sub-node-badge {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
padding: 2px 10px;
|
|
padding: 2px 10px;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
background: #409eff;
|
|
background: #409eff;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
pointer-events: auto;
|
|
pointer-events: auto;
|
|
|
|
|
+ cursor: pointer;
|
|
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
|
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,
|
|
.drawer-enter-active,
|
|
@@ -298,6 +360,6 @@ onUnmounted(() => {
|
|
|
.drawer-enter-from,
|
|
.drawer-enter-from,
|
|
|
.drawer-leave-to {
|
|
.drawer-leave-to {
|
|
|
opacity: 0;
|
|
opacity: 0;
|
|
|
- transform: translate(-50%, -100%) translateY(-12px) scale(0.95);
|
|
|
|
|
|
|
+ transform: translate(-50%, -100%) scale(0.95);
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|