|
@@ -44,20 +44,12 @@
|
|
|
<el-input v-model="nodeName" @blur="updateNodeName" />
|
|
<el-input v-model="nodeName" @blur="updateNodeName" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<template v-if="selectedNode.type === 'approval-node'">
|
|
<template v-if="selectedNode.type === 'approval-node'">
|
|
|
- <el-form-item label="分配方式">
|
|
|
|
|
- <el-select v-model="nodeProps.assigneeType" placeholder="请选择" style="width: 100%">
|
|
|
|
|
- <el-option label="角色" value="ROLE" />
|
|
|
|
|
- <el-option label="指定用户" value="USER" />
|
|
|
|
|
- <el-option label="发起人" value="SELF" />
|
|
|
|
|
- <el-option label="部门主管" value="LEADER" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item v-if="nodeProps.assigneeType === 'ROLE'" label="角色">
|
|
|
|
|
|
|
+ <el-form-item label="审批员工">
|
|
|
<el-select
|
|
<el-select
|
|
|
v-model="nodeProps.assigneeValue"
|
|
v-model="nodeProps.assigneeValue"
|
|
|
filterable
|
|
filterable
|
|
|
clearable
|
|
clearable
|
|
|
- placeholder="请选择角色"
|
|
|
|
|
|
|
+ placeholder="请选择员工管理中的成员"
|
|
|
style="width: 100%"
|
|
style="width: 100%"
|
|
|
>
|
|
>
|
|
|
<el-option
|
|
<el-option
|
|
@@ -68,30 +60,32 @@
|
|
|
/>
|
|
/>
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <el-form-item v-if="nodeProps.assigneeType === 'USER'" label="指定用户">
|
|
|
|
|
- <el-select
|
|
|
|
|
- v-model="nodeProps.assigneeValue"
|
|
|
|
|
- filterable
|
|
|
|
|
- clearable
|
|
|
|
|
- multiple
|
|
|
|
|
- collapse-tags
|
|
|
|
|
- placeholder="请选择用户"
|
|
|
|
|
- style="width: 100%"
|
|
|
|
|
- >
|
|
|
|
|
- <el-option
|
|
|
|
|
- v-for="user in userList"
|
|
|
|
|
- :key="user.id"
|
|
|
|
|
- :label="user.realName || user.username"
|
|
|
|
|
- :value="String(user.id)"
|
|
|
|
|
- />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
<el-form-item label="审批方式">
|
|
<el-form-item label="审批方式">
|
|
|
<el-select v-model="nodeProps.approveMode" placeholder="请选择" style="width: 100%">
|
|
<el-select v-model="nodeProps.approveMode" placeholder="请选择" style="width: 100%">
|
|
|
<el-option label="或签(一人通过即可)" value="or" />
|
|
<el-option label="或签(一人通过即可)" value="or" />
|
|
|
<el-option label="会签(全部通过)" value="and" />
|
|
<el-option label="会签(全部通过)" value="and" />
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
|
|
+ <el-form-item label="审批时限">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="nodeProps.timeoutHours"
|
|
|
|
|
+ :min="1"
|
|
|
|
|
+ :max="720"
|
|
|
|
|
+ :precision="0"
|
|
|
|
|
+ placeholder="不填表示无限制"
|
|
|
|
|
+ controls-position="right"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="form-tip">单位:小时,为空则不计算超时</div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="超时动作">
|
|
|
|
|
+ <el-select v-model="nodeProps.timeoutAction" placeholder="请选择" style="width: 100%">
|
|
|
|
|
+ <el-option label="提醒" value="remind" />
|
|
|
|
|
+ <el-option label="自动通过" value="pass" />
|
|
|
|
|
+ <el-option label="自动驳回" value="reject" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <div class="form-tip">目前仅实现「提醒」,自动通过/驳回预留</div>
|
|
|
|
|
+ </el-form-item>
|
|
|
</template>
|
|
</template>
|
|
|
<template v-if="selectedNode.type === 'condition-node'">
|
|
<template v-if="selectedNode.type === 'condition-node'">
|
|
|
<el-form-item label="条件表达式">
|
|
<el-form-item label="条件表达式">
|
|
@@ -124,7 +118,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 保存确认弹窗(无模式时) -->
|
|
<!-- 保存确认弹窗(无模式时) -->
|
|
|
- <el-dialog v-model="saveDialogVisible" title="保存流程" width="500px">
|
|
|
|
|
|
|
+ <el-dialog v-model="saveDialogVisible" title="保存流程" width="600px">
|
|
|
<el-form ref="saveFormRef" :model="saveForm" :rules="saveFormRules" label-width="100px">
|
|
<el-form ref="saveFormRef" :model="saveForm" :rules="saveFormRules" label-width="100px">
|
|
|
<el-form-item label="流程编码" prop="code">
|
|
<el-form-item label="流程编码" prop="code">
|
|
|
<el-input v-model="saveForm.code" />
|
|
<el-input v-model="saveForm.code" />
|
|
@@ -139,11 +133,100 @@
|
|
|
<el-input v-model="saveForm.description" type="textarea" />
|
|
<el-input v-model="saveForm.description" type="textarea" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="form-fields-section">
|
|
|
|
|
+ <div class="form-fields-header">
|
|
|
|
|
+ <span class="form-fields-title">流程表单字段</span>
|
|
|
|
|
+ <el-button type="primary" size="small" :icon="Delete" @click="addFormField">添加字段</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-table :data="formFields" size="small" border style="width: 100%">
|
|
|
|
|
+ <el-table-column label="字段名" min-width="120">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-input v-model="formFields[$index].name" placeholder="英文标识" size="small" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="显示名" min-width="120">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-input v-model="formFields[$index].label" placeholder="中文显示名" size="small" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="类型" width="120">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-select v-model="formFields[$index].type" placeholder="类型" size="small" style="width: 100%">
|
|
|
|
|
+ <el-option v-for="opt in fieldTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="必填" width="70" align="center">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-checkbox v-model="formFields[$index].required" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="多选" width="70" align="center">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-checkbox
|
|
|
|
|
+ v-model="formFields[$index].multiple"
|
|
|
|
|
+ :disabled="formFields[$index].type !== 'select'"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="选项" width="90" align="center">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ link
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :disabled="formFields[$index].type !== 'select'"
|
|
|
|
|
+ @click="openOptionDialog($index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ 配置选项
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="70" align="center">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-button link type="danger" size="small" @click="removeFormField($index)">删除</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-empty v-if="formFields.length === 0" description="暂无字段,点击上方按钮添加" :image-size="60" style="padding: 10px 0;" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<el-button @click="saveDialogVisible = false">取消</el-button>
|
|
<el-button @click="saveDialogVisible = false">取消</el-button>
|
|
|
<el-button type="primary" @click="submitSave">确认保存</el-button>
|
|
<el-button type="primary" @click="submitSave">确认保存</el-button>
|
|
|
</template>
|
|
</template>
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 下拉选项配置弹窗 -->
|
|
|
|
|
+ <el-dialog v-model="optionDialogVisible" title="配置下拉选项" width="500px">
|
|
|
|
|
+ <div v-if="optionEditingField" class="option-edit-tip">
|
|
|
|
|
+ 字段:{{ optionEditingField.label || optionEditingField.name }}
|
|
|
|
|
+ <el-checkbox v-model="optionEditingField.multiple" style="margin-left: 16px;">允许多选</el-checkbox>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-table :data="optionList" size="small" border>
|
|
|
|
|
+ <el-table-column label="显示文本" min-width="140">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-input v-model="optionList[$index].label" placeholder="显示文本" size="small" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="选项值" min-width="140">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-input v-model="optionList[$index].value" placeholder="选项值" size="small" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="70" align="center">
|
|
|
|
|
+ <template #default="{ $index }">
|
|
|
|
|
+ <el-button link type="danger" size="small" @click="removeOption($index)">删除</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <el-button type="primary" size="small" style="margin-top: 12px;" @click="addOption">添加选项</el-button>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="optionDialogVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="saveOptions">保存</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -158,10 +241,10 @@ import { VideoPlay, User, Message, Operation, CircleCheck } from '@element-plus/
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
|
|
import { listRole } from '@/api/system/role'
|
|
import { listRole } from '@/api/system/role'
|
|
|
-import { listUser } from '@/api/system/user'
|
|
|
|
|
import { addDefinition, updateDefinition, getDefinition } from '@/api/flow/definition'
|
|
import { addDefinition, updateDefinition, getDefinition } from '@/api/flow/definition'
|
|
|
import type { Role } from '@/types/system'
|
|
import type { Role } from '@/types/system'
|
|
|
-import type { FlowDefinition } from '@/types/flow'
|
|
|
|
|
|
|
+import type { FlowDefinition, FormField, FormFieldOption } from '@/types/flow'
|
|
|
|
|
+import { Delete } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
@@ -179,11 +262,12 @@ const flowInfoVisible = computed(() => mode.value === 'create' || mode.value ===
|
|
|
|
|
|
|
|
const selectedNode = ref<any>(null)
|
|
const selectedNode = ref<any>(null)
|
|
|
const roleList = ref<Role[]>([])
|
|
const roleList = ref<Role[]>([])
|
|
|
-const userList = ref<{id: number; username: string; realName?: string}[]>([])
|
|
|
|
|
const nodeProps = reactive<Record<string, any>>({
|
|
const nodeProps = reactive<Record<string, any>>({
|
|
|
assigneeType: 'ROLE',
|
|
assigneeType: 'ROLE',
|
|
|
assigneeValue: '',
|
|
assigneeValue: '',
|
|
|
approveMode: 'or',
|
|
approveMode: 'or',
|
|
|
|
|
+ timeoutHours: undefined as number | undefined,
|
|
|
|
|
+ timeoutAction: 'remind',
|
|
|
condition: '',
|
|
condition: '',
|
|
|
ccUsers: ''
|
|
ccUsers: ''
|
|
|
})
|
|
})
|
|
@@ -206,7 +290,7 @@ const nodeTypes = [
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
function getDefaultProps(type: string) {
|
|
function getDefaultProps(type: string) {
|
|
|
- if (type === 'approval-node') return { assigneeType: 'ROLE', assigneeValue: '', approveMode: 'or' }
|
|
|
|
|
|
|
+ if (type === 'approval-node') return { assigneeType: 'ROLE', assigneeValue: '', approveMode: 'or', timeoutHours: undefined, timeoutAction: 'remind' }
|
|
|
if (type === 'condition-node') return { condition: '' }
|
|
if (type === 'condition-node') return { condition: '' }
|
|
|
if (type === 'cc-node') return { ccUsers: '' }
|
|
if (type === 'cc-node') return { ccUsers: '' }
|
|
|
return {}
|
|
return {}
|
|
@@ -214,12 +298,21 @@ function getDefaultProps(type: string) {
|
|
|
|
|
|
|
|
function saveCurrentNodeProps() {
|
|
function saveCurrentNodeProps() {
|
|
|
if (selectedNode.value && lf) {
|
|
if (selectedNode.value && lf) {
|
|
|
- const props = JSON.parse(JSON.stringify(nodeProps))
|
|
|
|
|
- // USER 类型多选时,将数组转为逗号分隔字符串
|
|
|
|
|
- if (props.assigneeType === 'USER' && Array.isArray(props.assigneeValue)) {
|
|
|
|
|
- props.assigneeValue = props.assigneeValue.join(',')
|
|
|
|
|
|
|
+ const type = selectedNode.value.type
|
|
|
|
|
+ const defaultProps = getDefaultProps(type)
|
|
|
|
|
+ // 只保存当前节点类型相关的属性,避免不同类型节点属性互相污染
|
|
|
|
|
+ const props: Record<string, any> = JSON.parse(JSON.stringify(defaultProps))
|
|
|
|
|
+ if (type === 'approval-node') {
|
|
|
|
|
+ props.assigneeType = 'ROLE'
|
|
|
|
|
+ props.assigneeValue = nodeProps.assigneeValue ?? ''
|
|
|
|
|
+ props.approveMode = nodeProps.approveMode ?? 'or'
|
|
|
|
|
+ props.timeoutHours = nodeProps.timeoutHours
|
|
|
|
|
+ props.timeoutAction = nodeProps.timeoutAction ?? 'remind'
|
|
|
|
|
+ } else if (type === 'condition-node') {
|
|
|
|
|
+ props.condition = nodeProps.condition ?? ''
|
|
|
|
|
+ } else if (type === 'cc-node') {
|
|
|
|
|
+ props.ccUsers = nodeProps.ccUsers ?? ''
|
|
|
}
|
|
}
|
|
|
- // 只保存当前节点类型相关的属性
|
|
|
|
|
lf.setProperties(selectedNode.value.id, props)
|
|
lf.setProperties(selectedNode.value.id, props)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -227,25 +320,30 @@ function saveCurrentNodeProps() {
|
|
|
function loadNodeProps(data: any) {
|
|
function loadNodeProps(data: any) {
|
|
|
const props = data.properties || getDefaultProps(data.type)
|
|
const props = data.properties || getDefaultProps(data.type)
|
|
|
const cloned = JSON.parse(JSON.stringify(props))
|
|
const cloned = JSON.parse(JSON.stringify(props))
|
|
|
|
|
+ // 先重置为默认值,避免上一个节点的属性残留
|
|
|
|
|
+ Object.assign(nodeProps, getDefaultProps(data.type))
|
|
|
// 兼容旧格式:approver + approveType -> 新格式
|
|
// 兼容旧格式:approver + approveType -> 新格式
|
|
|
- let assigneeType = cloned.assigneeType ?? 'ROLE'
|
|
|
|
|
- let assigneeValue = cloned.assigneeValue ?? ''
|
|
|
|
|
- let approveMode = cloned.approveMode ?? cloned.approveType ?? 'or'
|
|
|
|
|
- if (!cloned.assigneeType && cloned.approver) {
|
|
|
|
|
- assigneeType = 'ROLE'
|
|
|
|
|
- assigneeValue = cloned.approver
|
|
|
|
|
- }
|
|
|
|
|
- // USER 类型:将逗号分隔字符串转为数组
|
|
|
|
|
- if (assigneeType === 'USER' && typeof assigneeValue === 'string' && assigneeValue) {
|
|
|
|
|
- assigneeValue = assigneeValue.split(',').filter(Boolean)
|
|
|
|
|
|
|
+ if (data.type === 'approval-node') {
|
|
|
|
|
+ let assigneeType = 'ROLE'
|
|
|
|
|
+ let assigneeValue = cloned.assigneeValue ?? ''
|
|
|
|
|
+ let approveMode = cloned.approveMode ?? cloned.approveType ?? 'or'
|
|
|
|
|
+ if (!cloned.assigneeValue && cloned.approver) {
|
|
|
|
|
+ assigneeValue = cloned.approver
|
|
|
|
|
+ }
|
|
|
|
|
+ // 旧数据中的 USER/SELF/LEADER 不再支持,统一清空让用户重新选择员工角色
|
|
|
|
|
+ if (cloned.assigneeType && cloned.assigneeType !== 'ROLE') {
|
|
|
|
|
+ assigneeValue = ''
|
|
|
|
|
+ }
|
|
|
|
|
+ nodeProps.assigneeType = assigneeType
|
|
|
|
|
+ nodeProps.assigneeValue = assigneeValue
|
|
|
|
|
+ nodeProps.approveMode = approveMode
|
|
|
|
|
+ nodeProps.timeoutHours = cloned.timeoutHours ?? undefined
|
|
|
|
|
+ nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind'
|
|
|
|
|
+ } else if (data.type === 'condition-node') {
|
|
|
|
|
+ nodeProps.condition = cloned.condition ?? ''
|
|
|
|
|
+ } else if (data.type === 'cc-node') {
|
|
|
|
|
+ nodeProps.ccUsers = cloned.ccUsers ?? ''
|
|
|
}
|
|
}
|
|
|
- Object.assign(nodeProps, {
|
|
|
|
|
- assigneeType,
|
|
|
|
|
- assigneeValue,
|
|
|
|
|
- approveMode,
|
|
|
|
|
- condition: cloned.condition ?? '',
|
|
|
|
|
- ccUsers: cloned.ccUsers ?? ''
|
|
|
|
|
- })
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function registerNodes() {
|
|
function registerNodes() {
|
|
@@ -371,6 +469,7 @@ function initLogicFlow() {
|
|
|
// 编辑模式:加载已有流程图
|
|
// 编辑模式:加载已有流程图
|
|
|
if (mode.value === 'edit' && flowId.value) {
|
|
if (mode.value === 'edit' && flowId.value) {
|
|
|
getDefinition(Number(flowId.value)).then((def: FlowDefinition) => {
|
|
getDefinition(Number(flowId.value)).then((def: FlowDefinition) => {
|
|
|
|
|
+ parseFormSchema(def.formSchema)
|
|
|
if (def.flowJson) {
|
|
if (def.flowJson) {
|
|
|
try {
|
|
try {
|
|
|
const graphData = convertToFrontendFormat(JSON.parse(def.flowJson))
|
|
const graphData = convertToFrontendFormat(JSON.parse(def.flowJson))
|
|
@@ -429,13 +528,7 @@ function convertToBackendFormat(graphData: any) {
|
|
|
return n
|
|
return n
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
- if (data.edges) {
|
|
|
|
|
- data.edges = data.edges.map((edge: any) => {
|
|
|
|
|
- const e = { ...edge }
|
|
|
|
|
- if (e.sourceNodeId === undefined && e.sourceNodeId !== undefined) { /* noop */ }
|
|
|
|
|
- return e
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // edges 保持原样,properties 中的 condition 会在后端解析
|
|
|
return data
|
|
return data
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -497,6 +590,75 @@ const saveFormRules: FormRules = {
|
|
|
name: [{ required: true, message: '请输入流程名称', trigger: 'blur' }]
|
|
name: [{ required: true, message: '请输入流程名称', trigger: 'blur' }]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const formFields = ref<FormField[]>([])
|
|
|
|
|
+const fieldTypeOptions = [
|
|
|
|
|
+ { label: '文本', value: 'text' },
|
|
|
|
|
+ { label: '多行文本', value: 'textarea' },
|
|
|
|
|
+ { label: '数字', value: 'number' },
|
|
|
|
|
+ { label: '日期', value: 'date' },
|
|
|
|
|
+ { label: '下拉选择', value: 'select' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+function addFormField() {
|
|
|
|
|
+ formFields.value.push({ name: '', label: '', type: 'text', required: false })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeFormField(index: number) {
|
|
|
|
|
+ formFields.value.splice(index, 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFormSchema(): string {
|
|
|
|
|
+ const validFields = formFields.value.filter(f => f.name.trim() && f.label.trim())
|
|
|
|
|
+ return validFields.length > 0 ? JSON.stringify(validFields) : ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseFormSchema(schema?: string) {
|
|
|
|
|
+ if (!schema) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsed = JSON.parse(schema)
|
|
|
|
|
+ if (Array.isArray(parsed)) {
|
|
|
|
|
+ formFields.value = parsed
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ formFields.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 下拉选项编辑
|
|
|
|
|
+const optionDialogVisible = ref(false)
|
|
|
|
|
+const optionEditingIndex = ref<number | null>(null)
|
|
|
|
|
+const optionEditingField = ref<FormField | null>(null)
|
|
|
|
|
+const optionList = ref<FormFieldOption[]>([])
|
|
|
|
|
+
|
|
|
|
|
+function openOptionDialog(index: number) {
|
|
|
|
|
+ optionEditingIndex.value = index
|
|
|
|
|
+ optionEditingField.value = formFields.value[index]
|
|
|
|
|
+ const rawOptions = optionEditingField.value.options || []
|
|
|
|
|
+ optionList.value = rawOptions.map((opt: any) => {
|
|
|
|
|
+ if (typeof opt === 'string') {
|
|
|
|
|
+ return { label: opt, value: opt }
|
|
|
|
|
+ }
|
|
|
|
|
+ return { label: String(opt.label ?? opt.value), value: opt.value }
|
|
|
|
|
+ })
|
|
|
|
|
+ optionDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function addOption() {
|
|
|
|
|
+ optionList.value.push({ label: '', value: '' })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeOption(index: number) {
|
|
|
|
|
+ optionList.value.splice(index, 1)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function saveOptions() {
|
|
|
|
|
+ if (optionEditingIndex.value === null || !optionEditingField.value) return
|
|
|
|
|
+ const validOptions = optionList.value.filter(opt => String(opt.label).trim() !== '' && String(opt.value).trim() !== '')
|
|
|
|
|
+ formFields.value[optionEditingIndex.value].options = validOptions
|
|
|
|
|
+ formFields.value[optionEditingIndex.value].multiple = optionEditingField.value.multiple
|
|
|
|
|
+ optionDialogVisible.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function validateFlow(graphData: any): string | null {
|
|
function validateFlow(graphData: any): string | null {
|
|
|
const nodes = graphData.nodes || []
|
|
const nodes = graphData.nodes || []
|
|
|
const edges = graphData.edges || []
|
|
const edges = graphData.edges || []
|
|
@@ -507,17 +669,13 @@ function validateFlow(graphData: any): string | null {
|
|
|
if (endNodes.length === 0) return '流程图缺少结束节点'
|
|
if (endNodes.length === 0) return '流程图缺少结束节点'
|
|
|
if (startNodes.length > 1) return '流程图只能有一个开始节点'
|
|
if (startNodes.length > 1) return '流程图只能有一个开始节点'
|
|
|
if (endNodes.length > 1) return '流程图只能有一个结束节点'
|
|
if (endNodes.length > 1) return '流程图只能有一个结束节点'
|
|
|
- // 检查审批节点是否配置了审批人
|
|
|
|
|
|
|
+ // 检查审批节点是否选择了员工管理中的成员(角色账号)
|
|
|
for (const node of nodes) {
|
|
for (const node of nodes) {
|
|
|
if (node.type === 'approval-node' || node.type === 'approval') {
|
|
if (node.type === 'approval-node' || node.type === 'approval') {
|
|
|
const props = node.properties || {}
|
|
const props = node.properties || {}
|
|
|
- const assigneeType = props.assigneeType || props.approver
|
|
|
|
|
- if (!assigneeType) {
|
|
|
|
|
- return `审批节点 "${node.text || node.name}" 未配置审批人`
|
|
|
|
|
- }
|
|
|
|
|
const assigneeValue = props.assigneeValue || props.approver
|
|
const assigneeValue = props.assigneeValue || props.approver
|
|
|
- if (!assigneeValue && assigneeType !== 'SELF' && assigneeType !== 'LEADER') {
|
|
|
|
|
- return `审批节点 "${node.text || node.name}" 未选择具体的审批人/角色`
|
|
|
|
|
|
|
+ if (!assigneeValue) {
|
|
|
|
|
+ return `审批节点 "${node.text || node.name}" 未选择员工管理中的审批成员`
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -555,34 +713,71 @@ function validateFlow(graphData: any): string | null {
|
|
|
async function handleSave() {
|
|
async function handleSave() {
|
|
|
if (!lf) return
|
|
if (!lf) return
|
|
|
saveCurrentNodeProps()
|
|
saveCurrentNodeProps()
|
|
|
- const graphData = convertToBackendFormat(lf.getGraphData())
|
|
|
|
|
- const error = validateFlow(lf.getGraphData())
|
|
|
|
|
|
|
+ const rawGraphData = lf.getGraphData()
|
|
|
|
|
+ const error = validateFlow(rawGraphData)
|
|
|
if (error) {
|
|
if (error) {
|
|
|
ElMessage.warning(error)
|
|
ElMessage.warning(error)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 统一弹出保存弹窗,方便配置流程表单字段
|
|
|
|
|
+ if (mode.value === 'create') {
|
|
|
|
|
+ saveForm.code = flowCode.value
|
|
|
|
|
+ saveForm.name = flowName.value
|
|
|
|
|
+ saveForm.category = flowCategory.value
|
|
|
|
|
+ saveForm.description = flowDescription.value
|
|
|
|
|
+ formFields.value = []
|
|
|
|
|
+ } else if (mode.value === 'edit' && flowId.value) {
|
|
|
|
|
+ saveForm.code = flowCode.value
|
|
|
|
|
+ saveForm.name = flowName.value
|
|
|
|
|
+ saveForm.category = flowCategory.value
|
|
|
|
|
+ saveForm.description = flowDescription.value
|
|
|
|
|
+ // formFields 在编辑模式加载流程时已通过 parseFormSchema 填充
|
|
|
|
|
+ } else {
|
|
|
|
|
+ saveForm.code = ''
|
|
|
|
|
+ saveForm.name = ''
|
|
|
|
|
+ saveForm.category = ''
|
|
|
|
|
+ saveForm.description = ''
|
|
|
|
|
+ formFields.value = []
|
|
|
|
|
+ }
|
|
|
|
|
+ saveDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSave() {
|
|
|
|
|
+ const valid = await saveFormRef.value?.validate().catch(() => false)
|
|
|
|
|
+ if (!valid) return
|
|
|
|
|
+ if (!lf) return
|
|
|
|
|
+ saveCurrentNodeProps()
|
|
|
|
|
+ const rawGraphData = lf.getGraphData()
|
|
|
|
|
+ const error = validateFlow(rawGraphData)
|
|
|
|
|
+ if (error) {
|
|
|
|
|
+ ElMessage.warning(error)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const graphData = convertToBackendFormat(rawGraphData)
|
|
|
|
|
+
|
|
|
if (mode.value === 'create') {
|
|
if (mode.value === 'create') {
|
|
|
- // 新增模式:直接保存
|
|
|
|
|
const data: Partial<FlowDefinition> = {
|
|
const data: Partial<FlowDefinition> = {
|
|
|
- code: flowCode.value,
|
|
|
|
|
- name: flowName.value,
|
|
|
|
|
- category: flowCategory.value,
|
|
|
|
|
- description: flowDescription.value,
|
|
|
|
|
|
|
+ code: saveForm.code,
|
|
|
|
|
+ name: saveForm.name,
|
|
|
|
|
+ category: saveForm.category,
|
|
|
|
|
+ description: saveForm.description,
|
|
|
|
|
+ formSchema: getFormSchema(),
|
|
|
flowJson: JSON.stringify(graphData),
|
|
flowJson: JSON.stringify(graphData),
|
|
|
status: 0
|
|
status: 0
|
|
|
}
|
|
}
|
|
|
await addDefinition(data)
|
|
await addDefinition(data)
|
|
|
ElMessage.success('流程新增成功')
|
|
ElMessage.success('流程新增成功')
|
|
|
|
|
+ saveDialogVisible.value = false
|
|
|
router.push('/flow/definition')
|
|
router.push('/flow/definition')
|
|
|
} else if (mode.value === 'edit' && flowId.value) {
|
|
} else if (mode.value === 'edit' && flowId.value) {
|
|
|
- // 编辑模式:更新
|
|
|
|
|
const data: Partial<FlowDefinition> = {
|
|
const data: Partial<FlowDefinition> = {
|
|
|
id: Number(flowId.value),
|
|
id: Number(flowId.value),
|
|
|
- code: flowCode.value,
|
|
|
|
|
- name: flowName.value,
|
|
|
|
|
- category: flowCategory.value,
|
|
|
|
|
- description: flowDescription.value,
|
|
|
|
|
|
|
+ code: saveForm.code,
|
|
|
|
|
+ name: saveForm.name,
|
|
|
|
|
+ category: saveForm.category,
|
|
|
|
|
+ description: saveForm.description,
|
|
|
|
|
+ formSchema: getFormSchema(),
|
|
|
flowJson: JSON.stringify(graphData)
|
|
flowJson: JSON.stringify(graphData)
|
|
|
}
|
|
}
|
|
|
const newId = await updateDefinition(data)
|
|
const newId = await updateDefinition(data)
|
|
@@ -591,34 +786,23 @@ async function handleSave() {
|
|
|
} else {
|
|
} else {
|
|
|
ElMessage.success('流程修改成功')
|
|
ElMessage.success('流程修改成功')
|
|
|
}
|
|
}
|
|
|
|
|
+ saveDialogVisible.value = false
|
|
|
router.push('/flow/definition')
|
|
router.push('/flow/definition')
|
|
|
} else {
|
|
} else {
|
|
|
- // 无模式:弹出保存表单
|
|
|
|
|
- saveForm.code = ''
|
|
|
|
|
- saveForm.name = ''
|
|
|
|
|
- saveForm.category = ''
|
|
|
|
|
- saveForm.description = ''
|
|
|
|
|
- saveDialogVisible.value = true
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function submitSave() {
|
|
|
|
|
- const valid = await saveFormRef.value?.validate().catch(() => false)
|
|
|
|
|
- if (!valid) return
|
|
|
|
|
- if (!lf) return
|
|
|
|
|
- const graphData = convertToBackendFormat(lf.getGraphData())
|
|
|
|
|
- const data: Partial<FlowDefinition> = {
|
|
|
|
|
- code: saveForm.code,
|
|
|
|
|
- name: saveForm.name,
|
|
|
|
|
- category: saveForm.category,
|
|
|
|
|
- description: saveForm.description,
|
|
|
|
|
- flowJson: JSON.stringify(graphData),
|
|
|
|
|
- status: 0
|
|
|
|
|
|
|
+ const data: Partial<FlowDefinition> = {
|
|
|
|
|
+ code: saveForm.code,
|
|
|
|
|
+ name: saveForm.name,
|
|
|
|
|
+ category: saveForm.category,
|
|
|
|
|
+ description: saveForm.description,
|
|
|
|
|
+ formSchema: getFormSchema(),
|
|
|
|
|
+ flowJson: JSON.stringify(graphData),
|
|
|
|
|
+ status: 0
|
|
|
|
|
+ }
|
|
|
|
|
+ await addDefinition(data)
|
|
|
|
|
+ ElMessage.success('流程保存成功')
|
|
|
|
|
+ saveDialogVisible.value = false
|
|
|
|
|
+ router.push('/flow/definition')
|
|
|
}
|
|
}
|
|
|
- await addDefinition(data)
|
|
|
|
|
- ElMessage.success('流程保存成功')
|
|
|
|
|
- saveDialogVisible.value = false
|
|
|
|
|
- router.push('/flow/definition')
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleClear() {
|
|
function handleClear() {
|
|
@@ -638,21 +822,20 @@ async function loadRoles() {
|
|
|
} catch { /* ignore */ }
|
|
} catch { /* ignore */ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function loadUsers() {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await listUser({ pageNum: 1, pageSize: 500 })
|
|
|
|
|
- userList.value = res.list
|
|
|
|
|
- } catch { /* ignore */ }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
|
|
+ // 防止快速进入/离开设计器时重复创建 LogicFlow 实例
|
|
|
|
|
+ if (lf) {
|
|
|
|
|
+ lf.destroy()
|
|
|
|
|
+ lf = null
|
|
|
|
|
+ }
|
|
|
nextTick(initLogicFlow)
|
|
nextTick(initLogicFlow)
|
|
|
loadRoles()
|
|
loadRoles()
|
|
|
- loadUsers()
|
|
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
lf?.destroy()
|
|
lf?.destroy()
|
|
|
|
|
+ lf = null
|
|
|
|
|
+ selectedNode.value = null
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -747,4 +930,19 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
}
|
|
}
|
|
|
|
|
+.form-fields-section {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
|
|
+ padding-top: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+.form-fields-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.form-fields-title {
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|