|
|
@@ -77,7 +77,7 @@
|
|
|
|
|
|
<!-- 子节点配置 -->
|
|
|
<el-form-item label="启用子节点">
|
|
|
- <el-switch v-model="nodeProps.subNodesEnabled" active-text="是" inactive-text="否" />
|
|
|
+ <el-switch v-model="nodeProps.subNodesEnabled" active-text="是" inactive-text="否" @change="onSubNodesEnabledChange" />
|
|
|
</el-form-item>
|
|
|
<template v-if="nodeProps.subNodesEnabled">
|
|
|
<el-form-item label="执行方式">
|
|
|
@@ -86,38 +86,117 @@
|
|
|
<el-radio label="parallel">并行执行</el-radio>
|
|
|
</el-radio-group>
|
|
|
</el-form-item>
|
|
|
- <div class="panel-title">子节点配置</div>
|
|
|
- <el-table :data="nodeProps.subNodes" size="small" border class="sub-node-table">
|
|
|
- <el-table-column label="子节点名称" min-width="110">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-input v-model="row.name" size="small" placeholder="如:初审" />
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="审批角色" min-width="110">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-select v-model="row.assigneeValue" filterable clearable size="small" placeholder="请选择" style="width: 100%">
|
|
|
- <el-option v-for="role in roleList" :key="role.id" :label="role.roleName" :value="role.roleCode" />
|
|
|
+
|
|
|
+ <!-- 子节点列表 -->
|
|
|
+ <div v-if="selectedSubNodeIndex === null" class="sub-node-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <span class="section-title">子节点配置</span>
|
|
|
+ <span class="section-count">{{ nodeProps.subNodes.length }} 个</span>
|
|
|
+ </div>
|
|
|
+ <div class="sub-node-list">
|
|
|
+ <div
|
|
|
+ v-for="(sub, index) in nodeProps.subNodes"
|
|
|
+ :key="sub.id"
|
|
|
+ class="sub-node-row"
|
|
|
+ :class="{
|
|
|
+ 'drag-over': dragOverIndex === index,
|
|
|
+ sequential: isSequential,
|
|
|
+ }"
|
|
|
+ :draggable="isSequential"
|
|
|
+ @click="selectSubNode(index)"
|
|
|
+ @dragstart="handleDragStart(index, $event)"
|
|
|
+ @dragover.prevent="handleDragOver(index, $event)"
|
|
|
+ @drop="handleDrop(index)"
|
|
|
+ @dragend="handleDragEnd"
|
|
|
+ >
|
|
|
+ <div v-if="isSequential" class="drag-handle" @click.stop>
|
|
|
+ <el-icon><Rank /></el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="row-index">{{ index + 1 }}</div>
|
|
|
+ <div class="row-info">
|
|
|
+ <div class="row-name">{{ sub.name || `子节点 ${index + 1}` }}</div>
|
|
|
+ <div class="row-role">
|
|
|
+ {{ getRoleName(sub.assigneeValue) || '未设置审批角色' }}
|
|
|
+ <span v-if="sub.approveMode === 'and'"> · 会签</span>
|
|
|
+ <span v-else> · 或签</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="row-actions">
|
|
|
+ <el-icon class="delete-icon" @click.stop="removeSubNode(index)">
|
|
|
+ <Delete />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="!nodeProps.subNodes.length" class="empty-tip">暂无子节点,点击下方按钮添加</div>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" size="small" link class="add-sub-node-btn" @click="addSubNode">
|
|
|
+ <el-icon><Plus /></el-icon> 添加子节点
|
|
|
+ </el-button>
|
|
|
+ <div class="form-tip">
|
|
|
+ 提示:点击表格行或画布抽屉中的子节点卡片,可进入该子节点的独立配置页。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 子节点独立配置页 -->
|
|
|
+ <div v-else class="sub-node-detail">
|
|
|
+ <div class="detail-header">
|
|
|
+ <el-button type="primary" size="small" link @click="backToSubNodeList">
|
|
|
+ <el-icon><ArrowLeft /></el-icon>
|
|
|
+ 返回
|
|
|
+ </el-button>
|
|
|
+ <span class="section-title">子节点属性</span>
|
|
|
+ </div>
|
|
|
+ <template v-if="currentSubNode">
|
|
|
+ <el-form-item label="子节点名称">
|
|
|
+ <el-input v-model="nodeProps.subNodes[selectedSubNodeIndex].name" placeholder="如:初审" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="审批角色">
|
|
|
+ <el-select
|
|
|
+ v-model="nodeProps.subNodes[selectedSubNodeIndex].assigneeValue"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ placeholder="请选择审批角色"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="role in roleList"
|
|
|
+ :key="role.id"
|
|
|
+ :label="role.roleName"
|
|
|
+ :value="role.roleCode"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="审批方式">
|
|
|
+ <el-select v-model="nodeProps.subNodes[selectedSubNodeIndex].approveMode" style="width: 100%">
|
|
|
+ <el-option label="或签(一人通过即可)" value="or" />
|
|
|
+ <el-option label="会签(全部通过)" value="and" />
|
|
|
</el-select>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="审批方式" width="100">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-select v-model="row.approveMode" size="small" style="width: 100%">
|
|
|
- <el-option label="或签" value="or" />
|
|
|
- <el-option label="会签" value="and" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <template #label>
|
|
|
+ 审批时限
|
|
|
+ <el-tooltip placement="top" content="单位:小时,为空则不计算超时">
|
|
|
+ <el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <el-input-number
|
|
|
+ v-model="nodeProps.subNodes[selectedSubNodeIndex].timeoutHours"
|
|
|
+ :min="1"
|
|
|
+ :max="720"
|
|
|
+ :precision="0"
|
|
|
+ placeholder="不填表示无限制"
|
|
|
+ controls-position="right"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="超时动作">
|
|
|
+ <el-select v-model="nodeProps.subNodes[selectedSubNodeIndex].timeoutAction" style="width: 100%">
|
|
|
+ <el-option label="提醒" value="remind" />
|
|
|
+ <el-option label="无" value="" />
|
|
|
</el-select>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="操作" width="70" align="center">
|
|
|
- <template #default="{ $index }">
|
|
|
- <el-button type="danger" size="small" text @click="removeSubNode($index)">删除</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- <el-button type="primary" size="small" link class="add-sub-node-btn" @click="addSubNode">
|
|
|
- <el-icon><Plus /></el-icon> 添加子节点
|
|
|
- </el-button>
|
|
|
- <div class="form-tip">启用子节点后,父节点本身不再产生审批任务,由子节点替代执行。</div>
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</template>
|
|
|
<template v-if="selectedNode.type === 'condition-node'">
|
|
|
@@ -165,6 +244,25 @@
|
|
|
</el-form-item>
|
|
|
</template>
|
|
|
</el-form>
|
|
|
+
|
|
|
+ <div class="panel-actions">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ style="width: 100%"
|
|
|
+ @click="emit('update:drawerVisible', !drawerVisible)"
|
|
|
+ >
|
|
|
+ {{ drawerVisible ? '收起画布抽屉' : '展开画布抽屉' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="danger"
|
|
|
+ plain
|
|
|
+ style="width: 100%"
|
|
|
+ @click="deleteNode"
|
|
|
+ >
|
|
|
+ 删除节点
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-else class="property-panel empty">
|
|
|
<div class="panel-title">节点属性</div>
|
|
|
@@ -173,8 +271,8 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, toRefs } from 'vue'
|
|
|
-import { QuestionFilled, Plus } from '@element-plus/icons-vue'
|
|
|
+import { computed, toRefs, ref } from 'vue'
|
|
|
+import { QuestionFilled, Plus, Delete, Rank, ArrowLeft } from '@element-plus/icons-vue'
|
|
|
import type { Role } from '@/types/system'
|
|
|
|
|
|
const props = defineProps<{
|
|
|
@@ -187,10 +285,19 @@ const props = defineProps<{
|
|
|
onDefaultChange: (edge: any) => void
|
|
|
onBranchBlur: (edge: any) => void
|
|
|
generateSubNodeId: () => string
|
|
|
+ selectedSubNodeIndex: number | null
|
|
|
+ drawerVisible: boolean
|
|
|
+ saveCurrentNodeProps?: () => void
|
|
|
}>()
|
|
|
|
|
|
-const { selectedNode, outgoingEdges, roleList, nodeProps } = toRefs(props)
|
|
|
-const { getEdgeTargetName, updateNodeName, onDefaultChange, onBranchBlur, generateSubNodeId } = props
|
|
|
+const emit = defineEmits<{
|
|
|
+ (e: 'update:selectedSubNodeIndex', index: number | null): void
|
|
|
+ (e: 'update:drawerVisible', visible: boolean): void
|
|
|
+ (e: 'deleteNode'): void
|
|
|
+}>()
|
|
|
+
|
|
|
+const { selectedNode, outgoingEdges, roleList, nodeProps, selectedSubNodeIndex, drawerVisible } = toRefs(props)
|
|
|
+const { getEdgeTargetName, updateNodeName, onDefaultChange, onBranchBlur, generateSubNodeId, saveCurrentNodeProps } = props
|
|
|
|
|
|
const nodeName = computed({
|
|
|
get: () => selectedNode.value?.text?.value || '',
|
|
|
@@ -201,6 +308,29 @@ const nodeName = computed({
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+const isSequential = computed(() => nodeProps.value.subNodeMode === 'sequential')
|
|
|
+const currentSubNode = computed(() => {
|
|
|
+ const idx = selectedSubNodeIndex.value
|
|
|
+ if (idx === null || !Array.isArray(nodeProps.value.subNodes)) return null
|
|
|
+ return nodeProps.value.subNodes[idx]
|
|
|
+})
|
|
|
+
|
|
|
+function getRoleName(roleCode?: string): string {
|
|
|
+ if (!roleCode) return ''
|
|
|
+ const role = roleList.value.find((r) => r.roleCode === roleCode)
|
|
|
+ return role ? role.roleName : roleCode
|
|
|
+}
|
|
|
+
|
|
|
+function onSubNodesEnabledChange(enabled: boolean | string | number) {
|
|
|
+ if (enabled && !Array.isArray(nodeProps.value.subNodes)) {
|
|
|
+ nodeProps.value.subNodes = []
|
|
|
+ }
|
|
|
+ if (enabled) {
|
|
|
+ emit('update:drawerVisible', true)
|
|
|
+ }
|
|
|
+ saveCurrentNodeProps?.()
|
|
|
+}
|
|
|
+
|
|
|
function addSubNode() {
|
|
|
if (!Array.isArray(nodeProps.value.subNodes)) {
|
|
|
nodeProps.value.subNodes = []
|
|
|
@@ -213,11 +343,79 @@ function addSubNode() {
|
|
|
timeoutHours: undefined,
|
|
|
timeoutAction: 'remind',
|
|
|
})
|
|
|
+ saveCurrentNodeProps?.()
|
|
|
}
|
|
|
|
|
|
function removeSubNode(index: number) {
|
|
|
if (!Array.isArray(nodeProps.value.subNodes)) return
|
|
|
nodeProps.value.subNodes.splice(index, 1)
|
|
|
+ if (selectedSubNodeIndex.value === index) {
|
|
|
+ emit('update:selectedSubNodeIndex', null)
|
|
|
+ } else if (selectedSubNodeIndex.value !== null && selectedSubNodeIndex.value > index) {
|
|
|
+ emit('update:selectedSubNodeIndex', selectedSubNodeIndex.value - 1)
|
|
|
+ }
|
|
|
+ saveCurrentNodeProps?.()
|
|
|
+}
|
|
|
+
|
|
|
+function selectSubNode(index: number) {
|
|
|
+ emit('update:selectedSubNodeIndex', index)
|
|
|
+}
|
|
|
+
|
|
|
+function backToSubNodeList() {
|
|
|
+ emit('update:selectedSubNodeIndex', null)
|
|
|
+}
|
|
|
+
|
|
|
+function deleteNode() {
|
|
|
+ emit('deleteNode')
|
|
|
+}
|
|
|
+
|
|
|
+// 顺序模式下拖动排序
|
|
|
+const dragIndex = ref<number | null>(null)
|
|
|
+const dragOverIndex = ref<number | null>(null)
|
|
|
+
|
|
|
+function handleDragStart(index: number, e: DragEvent) {
|
|
|
+ if (!isSequential.value) return
|
|
|
+ dragIndex.value = index
|
|
|
+ if (e.dataTransfer) {
|
|
|
+ e.dataTransfer.effectAllowed = 'move'
|
|
|
+ e.dataTransfer.setData('text/plain', String(index))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleDragOver(index: number, e: DragEvent) {
|
|
|
+ if (!isSequential.value || dragIndex.value === null) return
|
|
|
+ dragOverIndex.value = index
|
|
|
+ if (e.dataTransfer) {
|
|
|
+ e.dataTransfer.dropEffect = 'move'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleDrop(index: number) {
|
|
|
+ dragOverIndex.value = null
|
|
|
+ if (!isSequential.value || dragIndex.value === null) return
|
|
|
+ const from = dragIndex.value
|
|
|
+ const to = index
|
|
|
+ dragIndex.value = null
|
|
|
+ if (from === to) return
|
|
|
+ const list = nodeProps.value.subNodes
|
|
|
+ const item = list.splice(from, 1)[0]
|
|
|
+ list.splice(to, 0, item)
|
|
|
+ // 同步选中索引
|
|
|
+ if (selectedSubNodeIndex.value === from) {
|
|
|
+ emit('update:selectedSubNodeIndex', to)
|
|
|
+ } else if (selectedSubNodeIndex.value !== null) {
|
|
|
+ if (from < selectedSubNodeIndex.value && to >= selectedSubNodeIndex.value) {
|
|
|
+ emit('update:selectedSubNodeIndex', selectedSubNodeIndex.value - 1)
|
|
|
+ } else if (from > selectedSubNodeIndex.value && to <= selectedSubNodeIndex.value) {
|
|
|
+ emit('update:selectedSubNodeIndex', selectedSubNodeIndex.value + 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ saveCurrentNodeProps?.()
|
|
|
+}
|
|
|
+
|
|
|
+function handleDragEnd() {
|
|
|
+ dragIndex.value = null
|
|
|
+ dragOverIndex.value = null
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
@@ -228,6 +426,8 @@ function removeSubNode(index: number) {
|
|
|
padding: 16px;
|
|
|
background: #fafafa;
|
|
|
overflow-y: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
.property-panel.empty {
|
|
|
display: flex;
|
|
|
@@ -246,13 +446,112 @@ function removeSubNode(index: number) {
|
|
|
padding-left: 4px;
|
|
|
padding-right: 4px;
|
|
|
}
|
|
|
-.sub-node-table {
|
|
|
- margin-top: 8px;
|
|
|
+
|
|
|
+.sub-node-section {
|
|
|
+ margin-bottom: 16px;
|
|
|
}
|
|
|
-.sub-node-table :deep(.el-input__wrapper) {
|
|
|
- padding-left: 4px;
|
|
|
- padding-right: 4px;
|
|
|
+.section-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 8px;
|
|
|
}
|
|
|
+.section-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.section-count {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-node-list {
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.sub-node-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 10px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+.sub-node-row:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+}
|
|
|
+.sub-node-row:hover {
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+.sub-node-row.active {
|
|
|
+ background: #ecf5ff;
|
|
|
+}
|
|
|
+.sub-node-row.drag-over {
|
|
|
+ background: #e6f2ff;
|
|
|
+ border: 1px dashed #409eff;
|
|
|
+}
|
|
|
+.sub-node-row.sequential {
|
|
|
+ cursor: move;
|
|
|
+}
|
|
|
+
|
|
|
+.drag-handle {
|
|
|
+ margin-right: 8px;
|
|
|
+ color: #c0c4cc;
|
|
|
+ cursor: move;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.row-index {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #409eff;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 11px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.row-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+.row-name {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #303133;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.row-role {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+.row-actions {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+.delete-icon {
|
|
|
+ color: #f56c6c;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.delete-icon:hover {
|
|
|
+ color: #ff4d4f;
|
|
|
+}
|
|
|
+.empty-tip {
|
|
|
+ padding: 16px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
.add-sub-node-btn {
|
|
|
margin-top: 8px;
|
|
|
}
|
|
|
@@ -262,4 +561,27 @@ function removeSubNode(index: number) {
|
|
|
margin-top: 8px;
|
|
|
line-height: 1.5;
|
|
|
}
|
|
|
+
|
|
|
+.sub-node-detail {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.detail-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.detail-header .section-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-actions {
|
|
|
+ margin-top: auto;
|
|
|
+ padding-top: 16px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
</style>
|