|
|
@@ -0,0 +1,1083 @@
|
|
|
+<template>
|
|
|
+ <div style="padding: 20px">
|
|
|
+ <!-- 顶部按钮栏 -->
|
|
|
+ <div style="margin-bottom: 10px; display: flex; gap: 10px">
|
|
|
+ <el-button type="primary" @click="handleAddNode" :disabled="DisableCheckView||props.isPreset">添加</el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ @click="addFromTemplate"
|
|
|
+ :disabled="DisableCheckView || !props.enableStepTable||props.isPreset"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <DocumentAdd />
|
|
|
+ </el-icon>
|
|
|
+ 从模板添加
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ @click="handleDeleteNode"
|
|
|
+ :disabled="!selectedNodeId || DisableCheckView||props.isPreset"
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- VueFlow 主画布 -->
|
|
|
+ <VueFlow style="width: 100%; height: 600px">
|
|
|
+ <template #node-default="{ id, data }">
|
|
|
+ <div class="custom-node">
|
|
|
+ <div class="node-content">
|
|
|
+ <!-- 图标显示 -->
|
|
|
+ <div style="font-size: 30px">
|
|
|
+ <img
|
|
|
+ v-if="data.stepIcon && data.stepIcon.startsWith('http')"
|
|
|
+ :src="data.stepIcon"
|
|
|
+ :alt="data.stepTitleShort"
|
|
|
+ style="width: 40px; height: 40px; object-fit: contain"
|
|
|
+ />
|
|
|
+ <span v-else>{{ data.stepIcon || '📋' }}</span>
|
|
|
+ </div>
|
|
|
+ <div style="font-weight: bold; font-size: 14px">
|
|
|
+ {{ data.stepTitleShort || '无标题' }}
|
|
|
+ </div>
|
|
|
+ <div style="font-size: 25px">
|
|
|
+ {{ String.fromCharCode(9311 + (data.stepIndex || 1)) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 四个连接点 -->
|
|
|
+ <Handle type="target" position="top" :id="`${id}-top`" class="handle handle-top" />
|
|
|
+ <Handle
|
|
|
+ type="source"
|
|
|
+ position="bottom"
|
|
|
+ :id="`${id}-bottom`"
|
|
|
+ class="handle handle-bottom"
|
|
|
+ />
|
|
|
+ <Handle type="target" position="left" :id="`${id}-left`" class="handle handle-left" />
|
|
|
+ <Handle type="source" position="right" :id="`${id}-right`" class="handle handle-right" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </VueFlow>
|
|
|
+
|
|
|
+ <!-- 节点表单内容 -->
|
|
|
+ <div
|
|
|
+ v-if="showForm && selectedNodeId"
|
|
|
+ style="margin-top: 20px; padding: 20px; border: 1px solid #ccc"
|
|
|
+ >
|
|
|
+ <h3>节点配置({{ selectedNodeId }})</h3>
|
|
|
+ <div style="display: flex; gap: 20px">
|
|
|
+ <el-form label-width="155px">
|
|
|
+ <el-form-item label="图标">
|
|
|
+ <el-popover placement="bottom" trigger="click" width="230">
|
|
|
+ <!-- 图标选择面板 -->
|
|
|
+ <div style="display: flex; flex-wrap: wrap; gap: 8px">
|
|
|
+ <div
|
|
|
+ v-for="icon in iconOptions"
|
|
|
+ :key="icon.value"
|
|
|
+ @click="() => selectIcon(formData, icon.value)"
|
|
|
+ :style="{
|
|
|
+ border:
|
|
|
+ formData.stepIcon === icon.value ? '2px solid #409EFF' : '1px solid #ccc',
|
|
|
+ borderRadius: '4px',
|
|
|
+ padding: '2px',
|
|
|
+ cursor: 'pointer'
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ :src="icon.value"
|
|
|
+ :alt="icon.name"
|
|
|
+ style="width: 28px; height: 28px; object-fit: contain"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 触发元素:当前图标或按钮 -->
|
|
|
+ <template #reference>
|
|
|
+ <div style="cursor: pointer; display: inline-block">
|
|
|
+ <img
|
|
|
+ v-if="formData.stepIcon"
|
|
|
+ :src="formData.stepIcon"
|
|
|
+ style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid #ccc"
|
|
|
+ />
|
|
|
+ <el-button v-else type="primary" size="small">选择图标</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-popover>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="标题">
|
|
|
+ <el-input
|
|
|
+ :disabled="DisableCheckView"
|
|
|
+ v-model="formData.stepTitle"
|
|
|
+ placeholder="标题"
|
|
|
+ size="small"
|
|
|
+ @blur="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="标题缩写">
|
|
|
+ <el-input
|
|
|
+ :disabled="DisableCheckView"
|
|
|
+ v-model="formData.stepTitleShort"
|
|
|
+ placeholder="标题缩写"
|
|
|
+ size="small"
|
|
|
+ @blur="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="确认方式">
|
|
|
+ <el-select
|
|
|
+ :disabled="DisableCheckView"
|
|
|
+ v-model="formData.confirmType"
|
|
|
+ placeholder="请选择确认方式"
|
|
|
+ @change="handleFormChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYS_STEP_CONFIRMTYPE)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="确认角色" v-if="formData.confirmType == '2'">
|
|
|
+ <el-select
|
|
|
+ :disabled="DisableCheckView"
|
|
|
+ v-model="formData.confirmRoleCode"
|
|
|
+ placeholder="请选择确认角色"
|
|
|
+ @change="handleFormChange"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ @clear="handleRoleClear(formData)"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in RoleOptions"
|
|
|
+ :key="dict.code"
|
|
|
+ :label="dict.name"
|
|
|
+ :value="dict.code"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="确认人员" v-if="formData.confirmType == '2'">
|
|
|
+ <el-select
|
|
|
+ :disabled="DisableCheckView"
|
|
|
+ v-model="formData.confirmUser"
|
|
|
+ placeholder="请选择确认人员"
|
|
|
+ @change="handleFormChange"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in UserOptions"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="取消作业">
|
|
|
+ <el-checkbox
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ v-model="formData.enableCancelJob"
|
|
|
+ @change="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="设置锁定人">
|
|
|
+ <el-checkbox
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ v-model="formData.enableSetLocker"
|
|
|
+ @change="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="设置共锁人">
|
|
|
+ <el-checkbox
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ v-model="formData.enableSetColocker"
|
|
|
+ @change="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="添加共锁人">
|
|
|
+ <el-checkbox
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ v-model="formData.enableAddColocker"
|
|
|
+ @change="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="添加共锁人后跳转步骤">
|
|
|
+ <el-input-number
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ v-model="formData.gotoStepAfterAddingColocker"
|
|
|
+ :min="1"
|
|
|
+ size="small"
|
|
|
+ placeholder="步骤号"
|
|
|
+ @change="handleFormChange"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="减少共锁人">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="formData.enableReduceColocker"
|
|
|
+ @change="handleFormChange"
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="上锁">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="formData.enableLock"
|
|
|
+ @change="handleFormChange"
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="共锁">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="formData.enableColock"
|
|
|
+ @change="handleFormChange"
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="解除共锁">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="formData.enableReleaseColock"
|
|
|
+ @change="handleFormChange"
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="解锁">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="formData.enableUnlock"
|
|
|
+ @change="handleFormChange"
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="结束作业">
|
|
|
+ <el-checkbox
|
|
|
+ v-model="formData.enableEndJob"
|
|
|
+ @change="handleFormChange"
|
|
|
+ :disabled="DisableCheckView||props.isPreset"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 右侧富文本 -->
|
|
|
+ <div style="flex: 1">
|
|
|
+ <label style="font-weight: bold; display: block; margin-bottom: 8px">步骤操作说明</label>
|
|
|
+ <TinyMCE
|
|
|
+ :disabled="DisableCheckView"
|
|
|
+ v-model:value="formData.stepDescription"
|
|
|
+ :height="700"
|
|
|
+ placeholder="请输入内容..."
|
|
|
+ @change="handleFormChange"
|
|
|
+ @update:value="handleContentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 从模板添加-->
|
|
|
+ <TemplateAddDialog
|
|
|
+ v-model:visible="templateDialogVisible"
|
|
|
+ @select-template="handleTemplateSelect"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, watch, onMounted, nextTick } from 'vue'
|
|
|
+import { VueFlow, useVueFlow, Position, addEdge, Handle } from '@vue-flow/core'
|
|
|
+import '@vue-flow/core/dist/style.css'
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+import TinyMCE from '@/components/TinyMCE/index.vue'
|
|
|
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
|
|
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
|
|
|
+import {
|
|
|
+ getWorkflowStepPage,
|
|
|
+ insertWorkflowStep,
|
|
|
+ updateWorkflowStep,
|
|
|
+ deleteWorkflowStepList
|
|
|
+} from '@/api/custonWorkflow/step'
|
|
|
+
|
|
|
+import { getRolePage } from '@/api/system/role'
|
|
|
+import { getRoleUser } from '@/api/system/user'
|
|
|
+import { DocumentAdd } from '@element-plus/icons-vue'
|
|
|
+import TemplateAddDialog from '@/views/CustomWorkflow/CW/TemplateAdd.vue'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+const {
|
|
|
+ addNodes,
|
|
|
+ addEdges,
|
|
|
+ onNodeClick,
|
|
|
+ onNodeContextMenu,
|
|
|
+ removeNodes,
|
|
|
+ findNode,
|
|
|
+ zoomTo,
|
|
|
+ updateNode,
|
|
|
+ onConnect
|
|
|
+} = useVueFlow()
|
|
|
+const props = defineProps({
|
|
|
+ enableStepTable: {
|
|
|
+ type: Boolean,
|
|
|
+ required: true
|
|
|
+ },
|
|
|
+ modeId: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: null
|
|
|
+ },
|
|
|
+ isPreset:{
|
|
|
+ type: Boolean,
|
|
|
+ required: true
|
|
|
+ }
|
|
|
+
|
|
|
+})
|
|
|
+const selectedNodeId = ref<string | null>(null)
|
|
|
+const showForm = ref(false)
|
|
|
+const nodeIdCounter = ref(1)
|
|
|
+const nodes = ref([])
|
|
|
+const edges = ref([]) // 存储连接线
|
|
|
+const iconOptions = ref([])
|
|
|
+const RoleOptions = ref([])
|
|
|
+const UserOptions = ref([])
|
|
|
+
|
|
|
+// 默认数据结构
|
|
|
+const formData = reactive({
|
|
|
+ modeId: route.query.id || props.modeId,
|
|
|
+ stepTemplateId: undefined,
|
|
|
+ stepIndex: 0,
|
|
|
+ stepName: undefined,
|
|
|
+ stepTitle: undefined,
|
|
|
+ stepTitleShort: undefined,
|
|
|
+ stepDescription: undefined,
|
|
|
+ confirmType: undefined,
|
|
|
+ confirmRoleCode: undefined,
|
|
|
+ confirmUser: undefined,
|
|
|
+ enableCancelJob: undefined,
|
|
|
+ enableSetLocker: undefined,
|
|
|
+ enableSetColocker: undefined,
|
|
|
+ enableAddColocker: undefined,
|
|
|
+ gotoStepAfterAddingColocker: undefined,
|
|
|
+ enableReduceColocker: undefined,
|
|
|
+ enableLock: undefined,
|
|
|
+ enableColock: undefined,
|
|
|
+ enableReleaseColock: undefined,
|
|
|
+ enableUnlock: undefined,
|
|
|
+ enableEndJob: undefined,
|
|
|
+ id: undefined,
|
|
|
+ stepIcon: undefined
|
|
|
+})
|
|
|
+
|
|
|
+// 从模板添加
|
|
|
+const templateDialogVisible = ref(false)
|
|
|
+const addFromTemplate = () => {
|
|
|
+ templateDialogVisible.value = true
|
|
|
+}
|
|
|
+// 接收模板选择结果
|
|
|
+const handleTemplateSelect = (templateNodeData) => {
|
|
|
+ const { template } = templateNodeData
|
|
|
+
|
|
|
+ const id = `node-${nodeIdCounter.value++}`
|
|
|
+ const stepIndex = getNextAvailableIndex()
|
|
|
+
|
|
|
+ const newNode = {
|
|
|
+ id,
|
|
|
+ position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
|
|
|
+ width: 100,
|
|
|
+ height: 150,
|
|
|
+ data: {
|
|
|
+ modeId: route.query.id || props.modeId,
|
|
|
+ stepIcon: template.stepIcon || '',
|
|
|
+ stepTitleShort: template.stepTitleShort || template.stepName || '',
|
|
|
+ stepTitle: template.stepTitle || template.stepName || '',
|
|
|
+ stepDescription: template.stepDescription || '',
|
|
|
+ stepIndex: stepIndex,
|
|
|
+ index: stepIndex,
|
|
|
+ // 其他模板字段
|
|
|
+ confirmType: template.confirmType || 1,
|
|
|
+ confirmRoleCode: template.confirmRoleCode || '',
|
|
|
+ confirmUser: template.confirmUser || null,
|
|
|
+ enableCancelJob: template.enableCancelJob || false,
|
|
|
+ enableSetLocker: template.enableSetLocker || false,
|
|
|
+ enableSetColocker: template.enableSetColocker || false,
|
|
|
+ enableAddColocker: template.enableAddColocker || false,
|
|
|
+ gotoStepAfterAddingColocker: template.gotoStepAfterAddingColocker || null,
|
|
|
+ enableReduceColocker: template.enableReduceColocker || false,
|
|
|
+ enableLock: template.enableLock || false,
|
|
|
+ enableColock: template.enableColock || false,
|
|
|
+ enableReleaseColock: template.enableReleaseColock || false,
|
|
|
+ enableUnlock: template.enableUnlock || false,
|
|
|
+ enableEndJob: template.enableEndJob || false
|
|
|
+ },
|
|
|
+ style: {
|
|
|
+ width: '130px',
|
|
|
+ height: '180px',
|
|
|
+ borderRadius: '12px',
|
|
|
+ border: '1px solid #999',
|
|
|
+ textAlign: 'center',
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ padding: '10px',
|
|
|
+ backgroundColor: '#fff'
|
|
|
+ },
|
|
|
+ draggable: true
|
|
|
+ }
|
|
|
+
|
|
|
+ addNodes(newNode)
|
|
|
+ nodes.value.push(newNode)
|
|
|
+
|
|
|
+ console.log('从模板创建的新节点:', newNode)
|
|
|
+}
|
|
|
+// 防抖函数
|
|
|
+const debounce = (func, wait) => {
|
|
|
+ let timeout
|
|
|
+ return function executedFunction(...args) {
|
|
|
+ const later = () => {
|
|
|
+ clearTimeout(timeout)
|
|
|
+ func(...args)
|
|
|
+ }
|
|
|
+ clearTimeout(timeout)
|
|
|
+ timeout = setTimeout(later, wait)
|
|
|
+ }
|
|
|
+}
|
|
|
+// 表格图标切换
|
|
|
+const selectIcon = (formData, iconUrl) => {
|
|
|
+ formData.stepIcon = iconUrl
|
|
|
+ saveFormData(formData) // 你已有的接口保存方法
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化步骤数据并渲染节点
|
|
|
+const initTableData = async () => {
|
|
|
+ try {
|
|
|
+ if (props.modeId || route.query.id) {
|
|
|
+ const data = await getWorkflowStepPage({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: -1,
|
|
|
+ modeId: route.query.id || props.modeId
|
|
|
+ })
|
|
|
+
|
|
|
+ if (Array.isArray(data.list) && data.list.length > 0) {
|
|
|
+ // 按 stepIndex 从小到大排序
|
|
|
+ const sortedData = data.list.sort((a, b) => {
|
|
|
+ const aIndex = a.stepIndex || 0
|
|
|
+ const bIndex = b.stepIndex || 0
|
|
|
+ return aIndex - bIndex
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log(
|
|
|
+ '初始化数据完成,已按 stepIndex 排序:',
|
|
|
+ sortedData.map((row) => row.stepIndex)
|
|
|
+ )
|
|
|
+
|
|
|
+ // 渲染节点
|
|
|
+ renderNodesFromData(sortedData)
|
|
|
+
|
|
|
+ // 渲染连接线
|
|
|
+ renderEdgesFromData(sortedData)
|
|
|
+ } else {
|
|
|
+ console.log('没有找到步骤数据')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('初始化表格数据失败:', error)
|
|
|
+ ElMessage.error('加载步骤数据失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化数据渲染节点 - 修正版本
|
|
|
+const renderNodesFromData = (data) => {
|
|
|
+ // 清空现有节点
|
|
|
+ nodes.value = []
|
|
|
+
|
|
|
+ data.forEach((item, index) => {
|
|
|
+ const nodeId = `node-${item.id || Date.now() + index}`
|
|
|
+
|
|
|
+ const newNode = {
|
|
|
+ id: nodeId,
|
|
|
+ position: {
|
|
|
+ x: 100 + index * 200,
|
|
|
+ y: 100
|
|
|
+ },
|
|
|
+ width: 100,
|
|
|
+ height: 150,
|
|
|
+ data: {
|
|
|
+ stepIcon: item.stepIcon,
|
|
|
+ stepTitleShort: item.stepTitleShort,
|
|
|
+ stepIndex: item.stepIndex || index + 1, // 使用 stepIndex
|
|
|
+ index: item.stepIndex || index + 1, // 保持兼容性
|
|
|
+ // 保存完整的数据用于表单编辑
|
|
|
+ stepData: item
|
|
|
+ },
|
|
|
+ style: {
|
|
|
+ width: '130px',
|
|
|
+ height: '180px',
|
|
|
+ borderRadius: '12px',
|
|
|
+ border: '1px solid #999',
|
|
|
+ textAlign: 'center',
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ padding: '10px',
|
|
|
+ backgroundColor: '#fff'
|
|
|
+ },
|
|
|
+ draggable: true
|
|
|
+ }
|
|
|
+
|
|
|
+ addNodes(newNode)
|
|
|
+ nodes.value.push(newNode)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 渲染连接线 - 新增函数
|
|
|
+const renderEdgesFromData = (data) => {
|
|
|
+ // 清空现有连接线
|
|
|
+ edges.value = []
|
|
|
+
|
|
|
+ // 根据 stepIndex 顺序创建连接线
|
|
|
+ for (let i = 0; i < data.length - 1; i++) {
|
|
|
+ const currentStep = data[i]
|
|
|
+ const nextStep = data[i + 1]
|
|
|
+
|
|
|
+ const sourceNodeId = `node-${currentStep.id}`
|
|
|
+ const targetNodeId = `node-${nextStep.id}`
|
|
|
+
|
|
|
+ // 创建连接线,从右侧连接到左侧
|
|
|
+ const edge = {
|
|
|
+ id: `edge-${currentStep.id}-${nextStep.id}`,
|
|
|
+ source: sourceNodeId,
|
|
|
+ target: targetNodeId,
|
|
|
+ sourceHandle: `${sourceNodeId}-right`, // 从右侧连接点出发
|
|
|
+ targetHandle: `${targetNodeId}-left`, // 连接到左侧连接点
|
|
|
+ type: 'smoothstep',
|
|
|
+ style: { stroke: '#333', strokeWidth: 2 },
|
|
|
+ markerEnd: {
|
|
|
+ type: 'arrowclosed',
|
|
|
+ width: 20,
|
|
|
+ height: 20,
|
|
|
+ color: '#333'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ addEdges(edge)
|
|
|
+ edges.value.push(edge)
|
|
|
+
|
|
|
+ console.log('创建连接线:', edge)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取下一个可用的索引 - 修正版本
|
|
|
+function getNextAvailableIndex() {
|
|
|
+ if (nodes.value.length === 0) {
|
|
|
+ return 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用 stepIndex 而不是 index
|
|
|
+ const usedIndexes = nodes.value.map((node) => node.data.stepIndex || 1)
|
|
|
+ let nextIndex = 1
|
|
|
+ while (usedIndexes.includes(nextIndex)) {
|
|
|
+ nextIndex++
|
|
|
+ }
|
|
|
+
|
|
|
+ return nextIndex
|
|
|
+}
|
|
|
+
|
|
|
+// 新增节点
|
|
|
+function handleAddNode() {
|
|
|
+ const id = `node-${nodeIdCounter.value++}`
|
|
|
+ const stepIndex = getNextAvailableIndex() // 获取下一个 stepIndex
|
|
|
+
|
|
|
+ const newNode = {
|
|
|
+ id,
|
|
|
+ position: { x: 100 + Math.random() * 400, y: 100 + Math.random() * 300 },
|
|
|
+ width: 100,
|
|
|
+ height: 150,
|
|
|
+ data: {
|
|
|
+ stepIcon: '',
|
|
|
+ stepTitleShort: '',
|
|
|
+ stepIndex: stepIndex, // 使用 stepIndex
|
|
|
+ index: stepIndex // 保持兼容性
|
|
|
+ },
|
|
|
+ style: {
|
|
|
+ width: '130px',
|
|
|
+ height: '180px',
|
|
|
+ borderRadius: '12px',
|
|
|
+ border: '1px solid #999',
|
|
|
+ textAlign: 'center',
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ padding: '10px',
|
|
|
+ backgroundColor: '#fff'
|
|
|
+ },
|
|
|
+ draggable: true
|
|
|
+ }
|
|
|
+
|
|
|
+ addNodes(newNode)
|
|
|
+ nodes.value.push(newNode)
|
|
|
+
|
|
|
+ selectedNodeId.value = id
|
|
|
+ showForm.value = true
|
|
|
+
|
|
|
+ // 重置表单数据,并设置 stepIndex
|
|
|
+ resetFormData()
|
|
|
+ formData.stepIndex = stepIndex
|
|
|
+}
|
|
|
+
|
|
|
+// 删除操作
|
|
|
+function handleDeleteNode() {
|
|
|
+ if (selectedNodeId.value) {
|
|
|
+ // 获取要删除的节点数据
|
|
|
+ const nodeToDelete = nodes.value.find((node) => node.id === selectedNodeId.value)
|
|
|
+
|
|
|
+ if (nodeToDelete && nodeToDelete.data.stepData && nodeToDelete.data.stepData.id) {
|
|
|
+ // 显示确认对话框
|
|
|
+ ElMessageBox.confirm(
|
|
|
+ `确定要删除步骤"${nodeToDelete.data.stepTitleShort || '未命名'}"吗?`,
|
|
|
+ '确认删除',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(() => {
|
|
|
+ // 用户确认删除
|
|
|
+ deleteWorkflowStepList([nodeToDelete.data.stepData.id])
|
|
|
+ .then(() => {
|
|
|
+ // 删除相关的连接线
|
|
|
+ const relatedEdges = edges.value.filter(
|
|
|
+ (edge) =>
|
|
|
+ edge.source === selectedNodeId.value || edge.target === selectedNodeId.value
|
|
|
+ )
|
|
|
+
|
|
|
+ relatedEdges.forEach((edge) => {
|
|
|
+ edges.value = edges.value.filter((e) => e.id !== edge.id)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 删除成功,从界面移除节点
|
|
|
+ removeNodes([selectedNodeId.value])
|
|
|
+
|
|
|
+ const index = nodes.value.findIndex((node) => node.id === selectedNodeId.value)
|
|
|
+ if (index !== -1) {
|
|
|
+ nodes.value.splice(index, 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedNodeId.value = null
|
|
|
+ showForm.value = false
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ reorderNodeIndexes()
|
|
|
+ }, 100)
|
|
|
+
|
|
|
+ ElMessage.success('删除成功')
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error('删除失败:', error)
|
|
|
+ ElMessage.error('删除失败,请稍后重试')
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 用户取消删除
|
|
|
+ ElMessage.info('已取消删除')
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 如果节点没有对应的数据库ID,直接从界面删除
|
|
|
+ removeNodes([selectedNodeId.value])
|
|
|
+
|
|
|
+ const index = nodes.value.findIndex((node) => node.id === selectedNodeId.value)
|
|
|
+ if (index !== -1) {
|
|
|
+ nodes.value.splice(index, 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedNodeId.value = null
|
|
|
+ showForm.value = false
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ reorderNodeIndexes()
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 重置表单数据
|
|
|
+const resetFormData = () => {
|
|
|
+ Object.assign(formData, {
|
|
|
+ modeId: route.query.id,
|
|
|
+ stepTemplateId: undefined,
|
|
|
+ stepIndex: 0,
|
|
|
+ stepName: undefined,
|
|
|
+ stepTitle: undefined,
|
|
|
+ stepTitleShort: undefined,
|
|
|
+ stepDescription: undefined,
|
|
|
+ confirmType: undefined,
|
|
|
+ confirmRoleCode: undefined,
|
|
|
+ confirmUser: undefined,
|
|
|
+ enableCancelJob: undefined,
|
|
|
+ enableSetLocker: undefined,
|
|
|
+ enableSetColocker: undefined,
|
|
|
+ enableAddColocker: undefined,
|
|
|
+ gotoStepAfterAddingColocker: undefined,
|
|
|
+ enableReduceColocker: undefined,
|
|
|
+ enableLock: undefined,
|
|
|
+ enableColock: undefined,
|
|
|
+ enableReleaseColock: undefined,
|
|
|
+ enableUnlock: undefined,
|
|
|
+ enableEndJob: undefined,
|
|
|
+ id: undefined,
|
|
|
+ stepIcon: undefined
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 表单变化处理(防抖) - 修正版本
|
|
|
+const handleFormChange = debounce(async () => {
|
|
|
+ if (selectedNodeId.value) {
|
|
|
+ // 更新节点显示
|
|
|
+ updateNode(selectedNodeId.value, (node) => {
|
|
|
+ node.data.stepIcon = formData.stepIcon
|
|
|
+ node.data.stepTitleShort = formData.stepTitleShort
|
|
|
+ node.data.stepIndex = formData.stepIndex // 同步 stepIndex
|
|
|
+ })
|
|
|
+
|
|
|
+ // 更新本地节点数组
|
|
|
+ const localNode = nodes.value.find((n) => n.id === selectedNodeId.value)
|
|
|
+ if (localNode) {
|
|
|
+ localNode.data.stepIcon = formData.stepIcon
|
|
|
+ localNode.data.stepTitleShort = formData.stepTitleShort
|
|
|
+ localNode.data.stepIndex = formData.stepIndex // 同步 stepIndex
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存数据
|
|
|
+ await saveFormData(formData)
|
|
|
+ }
|
|
|
+}, 1000)
|
|
|
+
|
|
|
+// 自动保存单行数据
|
|
|
+const saveFormData = async (row) => {
|
|
|
+ try {
|
|
|
+ if (!row) return
|
|
|
+ // 如果没有 id,则执行新增
|
|
|
+ if (!row.id) {
|
|
|
+ const res = await insertWorkflowStep(row)
|
|
|
+ if (res && res) {
|
|
|
+ row.id = res
|
|
|
+ console.log('新增步骤成功:', res)
|
|
|
+ ElMessage.success(`新增步骤成功`)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 有 id 则执行更新
|
|
|
+ console.log('准备更新步骤:', row)
|
|
|
+
|
|
|
+ await updateWorkflowStep(row)
|
|
|
+ ElMessage.success(`更新步骤成功`)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存步骤失败', error)
|
|
|
+ ElMessage.error('保存失败,请稍后重试')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 节点点击处理 - 修正版本
|
|
|
+onNodeClick(({ node }) => {
|
|
|
+ if (selectedNodeId.value === node.id) {
|
|
|
+ showForm.value = false
|
|
|
+ selectedNodeId.value = null
|
|
|
+ } else {
|
|
|
+ selectedNodeId.value = node.id
|
|
|
+ showForm.value = true
|
|
|
+
|
|
|
+ // 更新表单数据
|
|
|
+ const nodeData = node.data
|
|
|
+ if (nodeData.stepData) {
|
|
|
+ // 如果有完整数据,使用完整数据
|
|
|
+ Object.assign(formData, nodeData.stepData)
|
|
|
+ } else {
|
|
|
+ // 否则使用节点显示数据
|
|
|
+ formData.stepIcon = nodeData.stepIcon || ''
|
|
|
+ formData.stepTitleShort = nodeData.stepTitleShort || ''
|
|
|
+ formData.stepIndex = nodeData.stepIndex || 1 // 同步 stepIndex
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onNodeContextMenu(({ node, event }) => {
|
|
|
+ event.preventDefault()
|
|
|
+ selectedNodeId.value = node.id
|
|
|
+ showForm.value = true
|
|
|
+})
|
|
|
+
|
|
|
+// 获取当前所有节点并重新排序索引 - 修正版本
|
|
|
+function reorderNodeIndexes() {
|
|
|
+ console.log('重新排序前的节点:', nodes.value)
|
|
|
+
|
|
|
+ const sortedNodes = [...nodes.value].sort((a, b) => {
|
|
|
+ if (Math.abs(a.position.y - b.position.y) < 50) {
|
|
|
+ return a.position.x - b.position.x
|
|
|
+ }
|
|
|
+ return a.position.y - b.position.y
|
|
|
+ })
|
|
|
+
|
|
|
+ sortedNodes.forEach((node, arrayIndex) => {
|
|
|
+ const newStepIndex = arrayIndex + 1
|
|
|
+ updateNode(node.id, (nodeData) => {
|
|
|
+ nodeData.data.stepIndex = newStepIndex
|
|
|
+ nodeData.data.index = newStepIndex // 保持兼容性
|
|
|
+ })
|
|
|
+
|
|
|
+ const localNode = nodes.value.find((n) => n.id === node.id)
|
|
|
+ if (localNode) {
|
|
|
+ localNode.data.stepIndex = newStepIndex
|
|
|
+ localNode.data.index = newStepIndex // 保持兼容性
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log('重新排序后的节点:', nodes.value)
|
|
|
+}
|
|
|
+
|
|
|
+// 角色获取
|
|
|
+const InitRole = async () => {
|
|
|
+ try {
|
|
|
+ const data = await getRolePage({ pageNo: 1, pageSize: -1 })
|
|
|
+ RoleOptions.value = data.list
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取角色数据失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+// 人员获取
|
|
|
+const InitUser = async () => {
|
|
|
+ try {
|
|
|
+ const data = await getRoleUser(formData.confirmRoleCode)
|
|
|
+ UserOptions.value = data.map((item) => {
|
|
|
+ return {
|
|
|
+ label: item.nickname,
|
|
|
+ value: item.id
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取角色数据失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+// 获取图标选项
|
|
|
+const loadIconOptions = async () => {
|
|
|
+ try {
|
|
|
+ const icons = await getIcons()
|
|
|
+ if (icons && icons.length > 0) {
|
|
|
+ iconOptions.value = icons
|
|
|
+ console.log('图标选项已加载:', iconOptions.value)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载图标选项失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取步骤基础图标
|
|
|
+const getIcons = async () => {
|
|
|
+ try {
|
|
|
+ const sysAttrKey1 = 'icon.step.all'
|
|
|
+ const iconRes = await getIsSystemAttributeByKey(sysAttrKey1)
|
|
|
+ console.log(iconRes, '获取到的图标配置')
|
|
|
+
|
|
|
+ if (iconRes && iconRes.sysAttrValue) {
|
|
|
+ const iconKeys = iconRes.sysAttrValue.split(',')
|
|
|
+ console.log('图标键值列表:', iconKeys)
|
|
|
+
|
|
|
+ const iconValues = await getIconValues(iconKeys)
|
|
|
+ console.log('所有图标值:', iconValues)
|
|
|
+
|
|
|
+ return iconValues
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取图标失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 批量获取图标值
|
|
|
+const getIconValues = async (iconKeys) => {
|
|
|
+ const iconValues = []
|
|
|
+
|
|
|
+ for (const key of iconKeys) {
|
|
|
+ try {
|
|
|
+ const iconData = await getIsSystemAttributeByKey(key.trim())
|
|
|
+ if (iconData && iconData.sysAttrValue) {
|
|
|
+ iconValues.push({
|
|
|
+ id: iconData.id,
|
|
|
+ key: key.trim(),
|
|
|
+ value: iconData.sysAttrValue,
|
|
|
+ name: iconData.sysAttrName || key.trim()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`获取图标 ${key} 失败:`, error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return iconValues
|
|
|
+}
|
|
|
+
|
|
|
+// 内容变化
|
|
|
+const handleContentChange = (content: string) => {
|
|
|
+ console.log('内容变化:', content)
|
|
|
+ handleFormChange() // 触发保存
|
|
|
+}
|
|
|
+
|
|
|
+// 节点连接线 - 修正版本
|
|
|
+onConnect((params) => {
|
|
|
+ console.log('连接参数:', params)
|
|
|
+
|
|
|
+ // 创建新的连接线,确保使用正确的 sourceHandle 和 targetHandle
|
|
|
+ const newEdge = {
|
|
|
+ id: `e-${params.source}-${params.target}`,
|
|
|
+ source: params.source,
|
|
|
+ target: params.target,
|
|
|
+ sourceHandle: params.sourceHandle, // 使用实际的 sourceHandle
|
|
|
+ targetHandle: params.targetHandle, // 使用实际的 targetHandle
|
|
|
+ type: 'smoothstep',
|
|
|
+ style: { stroke: '#333', strokeWidth: 2 },
|
|
|
+ markerEnd: {
|
|
|
+ type: 'arrowclosed',
|
|
|
+ width: 20,
|
|
|
+ height: 20,
|
|
|
+ color: '#333'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('创建连接线:', newEdge)
|
|
|
+ addEdges(newEdge)
|
|
|
+ edges.value.push(newEdge)
|
|
|
+})
|
|
|
+
|
|
|
+const DisableCheckView = ref()
|
|
|
+// 组件挂载时初始化
|
|
|
+onMounted(async () => {
|
|
|
+ if (route.query.type == 'view') {
|
|
|
+ DisableCheckView.value = true
|
|
|
+ } else {
|
|
|
+ DisableCheckView.value = false
|
|
|
+ }
|
|
|
+
|
|
|
+ zoomTo(1.1)
|
|
|
+
|
|
|
+ // 并行执行初始化
|
|
|
+ await Promise.all([loadIconOptions(), InitRole(), initTableData()])
|
|
|
+})
|
|
|
+// 确认角色清空操作
|
|
|
+const handleRoleClear = (formData) => {
|
|
|
+ formData.confirmRoleCode = ''
|
|
|
+ formData.confirmUser = ''
|
|
|
+}
|
|
|
+// 监听 confirmRoleCode 的变化
|
|
|
+watch(
|
|
|
+ () => formData.confirmRoleCode,
|
|
|
+ async (newRoleCode) => {
|
|
|
+ if (newRoleCode) {
|
|
|
+ await InitUser(newRoleCode)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true } // immediate: true 表示在组件挂载时立即执行一次
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.custom-node {
|
|
|
+ position: relative;
|
|
|
+ width: 125px;
|
|
|
+ height: 180px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.node-content {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+// 连接点样式
|
|
|
+.handle {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ background-color: #1a192b;
|
|
|
+ border: 2px solid #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: crosshair;
|
|
|
+ position: absolute;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.handle:hover {
|
|
|
+ background-color: #555;
|
|
|
+ transform: scale(1.2);
|
|
|
+}
|
|
|
+
|
|
|
+.handle-top {
|
|
|
+ top: -8px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.handle-bottom {
|
|
|
+ bottom: -8px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.handle-left {
|
|
|
+ left: -8px;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+.handle-right {
|
|
|
+ right: -8px;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+}
|
|
|
+
|
|
|
+// 全局样式,确保连接点可见
|
|
|
+:deep(.vue-flow__handle) {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ background-color: #1a192b;
|
|
|
+ border: 2px solid #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: crosshair;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.vue-flow__handle:hover) {
|
|
|
+ background-color: #555;
|
|
|
+ transform: scale(1.2);
|
|
|
+}
|
|
|
+
|
|
|
+// 连接线样式
|
|
|
+:deep(.vue-flow__edge-path) {
|
|
|
+ stroke: #333;
|
|
|
+ stroke-width: 2;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.vue-flow__edge) {
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+// 箭头样式
|
|
|
+:deep(.vue-flow__edge-marker) {
|
|
|
+ fill: #333;
|
|
|
+}
|
|
|
+</style>
|