|
@@ -7,19 +7,13 @@
|
|
|
:style="drawerPositionStyle"
|
|
:style="drawerPositionStyle"
|
|
|
>
|
|
>
|
|
|
<div class="sub-node-drawer">
|
|
<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 class="drawer-body">
|
|
|
<div
|
|
<div
|
|
|
v-for="(sub, index) in subNodes"
|
|
v-for="(sub, index) in subNodes"
|
|
|
- :key="sub.id"
|
|
|
|
|
|
|
+ :key="sub.id || index"
|
|
|
class="sub-node-card"
|
|
class="sub-node-card"
|
|
|
:class="{ active: selectedIndex === index }"
|
|
:class="{ active: selectedIndex === index }"
|
|
|
- @click="emit('select', index)"
|
|
|
|
|
|
|
+ @click="onSelectSubNode(index)"
|
|
|
>
|
|
>
|
|
|
<div class="card-index">{{ index + 1 }}</div>
|
|
<div class="card-index">{{ index + 1 }}</div>
|
|
|
<div class="card-info">
|
|
<div class="card-info">
|
|
@@ -27,12 +21,12 @@
|
|
|
<div class="card-role">{{ getSubNodeAssigneeDisplay(sub) }}</div>
|
|
<div class="card-role">{{ getSubNodeAssigneeDisplay(sub) }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="card-actions" @click.stop>
|
|
<div class="card-actions" @click.stop>
|
|
|
- <el-icon class="delete-icon" @click="emit('remove', index)">
|
|
|
|
|
|
|
+ <el-icon class="delete-icon" @click="onRemove(index)">
|
|
|
<Delete />
|
|
<Delete />
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
</div>
|
|
</div>
|
|
|
</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>
|
|
<el-icon class="add-icon"><Plus /></el-icon>
|
|
|
<span>添加子节点</span>
|
|
<span>添加子节点</span>
|
|
|
</div>
|
|
</div>
|
|
@@ -42,7 +36,8 @@
|
|
|
</Transition>
|
|
</Transition>
|
|
|
|
|
|
|
|
<div class="sub-node-badge" :style="badgeStyle" @click="toggleVisible">
|
|
<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 }">
|
|
<el-icon class="badge-arrow" :class="{ open: visible }">
|
|
|
<CaretBottom v-if="visible" />
|
|
<CaretBottom v-if="visible" />
|
|
|
<CaretTop v-else />
|
|
<CaretTop v-else />
|
|
@@ -53,16 +48,16 @@
|
|
|
|
|
|
|
|
<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, 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'
|
|
import type { Role } from '@/types/system'
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
const props = defineProps<{
|
|
|
lf: any
|
|
lf: any
|
|
|
node: any
|
|
node: any
|
|
|
nodeProps: Record<string, any>
|
|
nodeProps: Record<string, any>
|
|
|
|
|
+ roleList: Role[]
|
|
|
selectedIndex: number | null
|
|
selectedIndex: number | null
|
|
|
visible: boolean
|
|
visible: boolean
|
|
|
- roleList: Role[]
|
|
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
@@ -80,25 +75,32 @@ const subNodes = computed(() => {
|
|
|
return props.nodeProps.subNodes || []
|
|
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 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 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(() => {
|
|
const badgeTop = computed(() => {
|
|
|
return position.value.y - NODE_HALF_HEIGHT - BADGE_HEIGHT - VERTICAL_GAP
|
|
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(() => ({
|
|
const drawerPositionStyle = computed(() => ({
|
|
|
position: 'absolute' as const,
|
|
position: 'absolute' as const,
|
|
|
left: `${position.value.x}px`,
|
|
left: `${position.value.x}px`,
|
|
|
- top: `${drawerBottom.value}px`,
|
|
|
|
|
- transform: 'translate(-50%, -100%)',
|
|
|
|
|
|
|
+ top: `${drawerTop.value}px`,
|
|
|
|
|
+ transform: 'translate(-50%, 0)',
|
|
|
}))
|
|
}))
|
|
|
|
|
|
|
|
const badgeStyle = computed(() => ({
|
|
const badgeStyle = computed(() => ({
|
|
@@ -112,6 +114,18 @@ function toggleVisible() {
|
|
|
emit('update:visible', !props.visible)
|
|
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 {
|
|
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)
|
|
@@ -126,15 +140,19 @@ function getSubNodeAssigneeDisplay(sub: any): string {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function computePosition() {
|
|
function computePosition() {
|
|
|
- if (!props.lf || !props.node?.id) return
|
|
|
|
|
|
|
+ if (!props.node?.id) return
|
|
|
try {
|
|
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 {
|
|
} catch {
|
|
|
// ignore
|
|
// ignore
|
|
|
}
|
|
}
|
|
@@ -205,7 +223,7 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.sub-node-drawer {
|
|
.sub-node-drawer {
|
|
|
- width: 240px;
|
|
|
|
|
|
|
+ width: 220px;
|
|
|
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);
|
|
@@ -213,31 +231,6 @@ onUnmounted(() => {
|
|
|
overflow: hidden;
|
|
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 {
|
|
.drawer-body {
|
|
|
padding: 12px;
|
|
padding: 12px;
|
|
|
max-height: 320px;
|
|
max-height: 320px;
|
|
@@ -247,7 +240,7 @@ onUnmounted(() => {
|
|
|
.sub-node-card {
|
|
.sub-node-card {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 12px;
|
|
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
margin-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
border: 1px solid #ebeef5;
|
|
border: 1px solid #ebeef5;
|
|
@@ -339,7 +332,7 @@ onUnmounted(() => {
|
|
|
display: inline-flex;
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 4px;
|
|
gap: 4px;
|
|
|
- padding: 2px 10px;
|
|
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
background: #409eff;
|
|
background: #409eff;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
@@ -367,6 +360,6 @@ onUnmounted(() => {
|
|
|
.drawer-enter-from,
|
|
.drawer-enter-from,
|
|
|
.drawer-leave-to {
|
|
.drawer-leave-to {
|
|
|
opacity: 0;
|
|
opacity: 0;
|
|
|
- transform: translate(-50%, -100%) scale(0.95);
|
|
|
|
|
|
|
+ transform: translate(-50%, 10px) scale(0.95);
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|