Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev

This commit is contained in:
puhui999
2024-12-05 17:48:01 +08:00
121 changed files with 13202 additions and 12518 deletions

View File

@ -185,7 +185,6 @@ export const useApiSelect = (option: ApiSelectProps) => {
</el-select>
)
}
debugger
return (
<el-select
class="w-1/1"

View File

@ -16,3 +16,46 @@ export const localeProps = (t, prefix, rules) => {
return rule
})
}
/**
* 解析表单组件的 field, title 等字段(递归,如果组件包含子组件)
*
* @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule
* @param fields 解析后表单组件字段
* @param parentTitle 如果是子表单,子表单的标题,默认为空
*/
export const parseFormFields = (
rule: Record<string, any>,
fields: Array<Record<string, any>> = [],
parentTitle: string = ''
) => {
const { type, field, $required, title: tempTitle, children } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
let required = false
if ($required) {
required = true
}
fields.push({
field,
title,
type,
required
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFields(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseFormFields(rule, fields)
})
}
}

View File

@ -1,11 +1,12 @@
<template>
<div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd">
<div class="node-handler">
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
v-if="!readonly"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
@ -27,11 +28,17 @@
<div class="handler-item-text">条件分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
<div class="handler-item-icon condition">
<div class="handler-item-icon parallel">
<span class="iconfont icon-size icon-parallel"></span>
</div>
<div class="handler-item-text">并行分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)">
<div class="handler-item-icon inclusive">
<span class="iconfont icon-size icon-inclusive"></span>
</div>
<div class="handler-item-text">包容分支</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
@ -56,23 +63,36 @@ import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
})
const popoverShow = ref(false)
const message = useMessage() // 消息弹窗
const popoverShow = ref(false)
const props = defineProps({
childNode: {
type: Object as () => SimpleFlowNode,
default: null
},
showAdd: {
// 是否显示添加节点
type: Boolean,
default: true
currentNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits(['update:childNode'])
const readonly = inject<Boolean>('readonly') // 是否只读
const addNode = (type: number) => {
// 校验:条件分支、包容分支后面,不允许直接添加并行分支
if (
type === NodeType.PARALLEL_BRANCH_NODE &&
[NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
props.currentNode?.type
)
) {
message.error('条件分支、包容分支后面,不允许直接添加并行分支')
return
}
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID()
@ -122,12 +142,11 @@ const addNode = (type: number) => {
childNode: undefined,
conditionType: 1,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '其它情况进入此流程',
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
@ -162,6 +181,33 @@ const addNode = (type: number) => {
}
emits('update:childNode', data)
}
if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '包容分支',
type: NodeType.INCLUSIVE_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '包容条件1',
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: true
}
]
}
emits('update:childNode', data)
}
}
</script>

View File

@ -31,6 +31,13 @@
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 包容分支节点 -->
<InclusiveNode
v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
@ -40,7 +47,10 @@
/>
<!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
<EndEventNode
v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"
:flow-node="currentNode"
/>
</template>
<script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue'
@ -49,6 +59,7 @@ import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import InclusiveNode from './nodes/InclusiveNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({

View File

@ -1,23 +1,11 @@
<template>
<div class="simple-flow-canvas" v-loading="loading">
<div class="simple-flow-container">
<div class="top-area-container">
<div class="top-actions">
<div class="canvas-control">
<span class="control-scale-group">
<span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
<span class="control-scale-label">{{ scaleValue }}%</span>
<span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
</span>
</div>
<el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
<!-- <el-button type="primary">全局设置</el-button> -->
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<div v-loading="loading" class="overflow-auto">
<SimpleProcessModel
v-if="processNodeTree"
:flow-node="processNodeTree"
:readonly="false"
@save="saveSimpleFlowModel"
/>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善请修改后保存</div>
<div
@ -35,7 +23,7 @@
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import SimpleProcessModel from './SimpleProcessModel.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
@ -50,14 +38,16 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
defineOptions({
name: 'SimpleProcessDesigner'
})
const router = useRouter() // 路由
const emits = defineEmits(['success']) // 保存成功事件
const props = defineProps({
modelId: {
type: String,
required: true
}
})
const loading = ref(true)
const loading = ref(false)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
@ -79,28 +69,26 @@ const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
if (!props.modelId) {
message.error('缺少模型 modelId 编号')
const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
if (!simpleModelNode) {
message.error('模型数据为空')
return
}
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
const data = {
id: props.modelId,
simpleModel: processNodeTree.value
}
const result = await updateBpmSimpleModel(data)
if (result) {
message.success('修改成功')
close()
} else {
message.alert('修改失败')
try {
loading.value = true
const data = {
id: props.modelId,
simpleModel: simpleModelNode
}
const result = await updateBpmSimpleModel(data)
if (result) {
message.success('修改成功')
emits('success')
} else {
message.alert('修改失败')
}
} finally {
loading.value = false
}
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
@ -111,58 +99,37 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
return
}
if (type == NodeType.START_USER_NODE) {
// 发起人节点暂时不用校验,直接校验孩子节点
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.USER_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.COPY_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.CONDITION_NODE) {
if (
type === NodeType.USER_TASK_NODE ||
type === NodeType.COPY_TASK_NODE ||
type === NodeType.CONDITION_NODE
) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type == NodeType.CONDITION_BRANCH_NODE) {
if (
type == NodeType.CONDITION_BRANCH_NODE ||
type == NodeType.PARALLEL_BRANCH_NODE ||
type == NodeType.INCLUSIVE_BRANCH_NODE
) {
// 分支节点
// 1. 先校验各个分支
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
// 2. 校验孩子节点
validateNode(node.childNode, errorNodes)
}
}
}
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomOut = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
// 缩小
const zoomIn = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
onMounted(async () => {
try {
loading.value = true
@ -188,7 +155,7 @@ onMounted(async () => {
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 获取 SIMPLE 设计器模型
//获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result

View File

@ -0,0 +1,140 @@
<template>
<div class="simple-process-model-container position-relative">
<div class="position-absolute top-0px right-0px bg-#fff">
<el-row type="flex" justify="end">
<el-button-group key="scale-control" size="default">
<el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
<el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
<el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
<el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
</el-button-group>
<el-button
v-if="!readonly"
size="default"
class="ml-4px"
type="primary"
:icon="Select"
@click="saveSimpleFlowModel"
>保存模型</el-button
>
</el-row>
</div>
<div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善请修改后保存</div>
<div
class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
v-for="(item, index) in errorNodes"
:key="index"
>
{{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
</div>
<template #footer>
<el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { useWatchNode } from './node'
import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
defineOptions({
name: 'SimpleProcessModel'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
},
readonly: {
type: Boolean,
required: false,
default: true
}
})
const emits = defineEmits<{
'save': [node: SimpleFlowNode | undefined]
}>()
const processNodeTree = useWatchNode(props)
provide('readonly', props.readonly)
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomIn = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
// 缩小
const zoomOut = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
const processReZoom = () => {
scaleValue.value = 100
}
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
emits('save', processNodeTree.value)
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
const { type, showText, conditionNodes } = node
if (type == NodeType.END_EVENT_NODE) {
return
}
if (type == NodeType.START_USER_NODE) {
// 发起人节点暂时不用校验,直接校验孩子节点
validateNode(node.childNode, errorNodes)
}
if (
type === NodeType.USER_TASK_NODE ||
type === NodeType.COPY_TASK_NODE ||
type === NodeType.CONDITION_NODE
) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (
type == NodeType.CONDITION_BRANCH_NODE ||
type == NodeType.PARALLEL_BRANCH_NODE ||
type == NodeType.INCLUSIVE_BRANCH_NODE
) {
// 分支节点
// 1. 先校验各个分支
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
// 2. 校验孩子节点
validateNode(node.childNode, errorNodes)
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,48 @@
<template>
<SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
</template>
<script setup lang="ts">
import { useWatchNode } from './node'
import { SimpleFlowNode } from './consts'
defineOptions({
name: 'SimpleProcessViewer'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
},
// 流程任务
tasks: {
type: Array,
default: () => [] as any[]
},
// 流程实例
processInstance: {
type: Object,
default: () => undefined
}
})
const approveTasks = ref<any[]>(props.tasks)
const currentProcessInstance = ref(props.processInstance)
const simpleModel = useWatchNode(props)
watch(
() => props.tasks,
(newValue) => {
approveTasks.value = newValue
}
)
watch(
() => props.processInstance,
(newValue) => {
currentProcessInstance.value = newValue
}
)
provide('tasks', approveTasks)
provide('processInstance', currentProcessInstance)
</script>
p

View File

@ -1,6 +1,6 @@
// @ts-ignore
import { DictDataVO } from '@/api/system/dict/types'
import { TaskStatusEnum } from '@/api/bpm/task'
/**
* 节点类型
*/
@ -79,7 +79,7 @@ export interface SimpleFlowNode {
// 审批按钮设置
buttonsSetting?: any[]
// 表单权限
fieldsPermission?: Array<Record<string, string>>
fieldsPermission?: Array<Record<string, any>>
// 审批任务超时处理
timeoutHandler?: TimeoutHandler
// 审批任务拒绝处理
@ -96,7 +96,8 @@ export interface SimpleFlowNode {
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
// 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum
}
// 候选人策略枚举 用于审批节点。抄送节点 )
export enum CandidateStrategy {
@ -144,6 +145,14 @@ export enum CandidateStrategy {
* 指定用户组
*/
USER_GROUP = 40,
/**
* 表单内用户字段
*/
FORM_USER = 50,
/**
* 表单内部门负责人
*/
FORM_DEPT_LEADER = 51,
/**
* 流程表达式
*/
@ -178,7 +187,7 @@ export enum ApproveMethodType {
export type RejectHandler = {
// 审批拒绝类型
type: RejectHandlerType
// 退节点 Id
// 退节点 Id
returnNodeId?: string
}
@ -360,9 +369,13 @@ export enum OperationButtonType {
*/
ADD_SIGN = 5,
/**
* 退
* 退
*/
RETURN = 6
RETURN = 6,
/**
* 抄送
*/
COPY = 7
}
/**
@ -419,6 +432,8 @@ export const CANDIDATE_STRATEGY: DictDataVO[] = [
{ label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
{ label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
{ label: '用户组', value: CandidateStrategy.USER_GROUP },
{ label: '表单内用户字段', value: CandidateStrategy.FORM_USER },
{ label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER },
{ label: '流程表达式', value: CandidateStrategy.EXPRESSION }
]
// 审批节点 的审批类型
@ -503,16 +518,17 @@ OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退')
OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退')
OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送')
// 默认的按钮权限设置
export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '退', enable: false }
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: true },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: true },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true },
{ id: OperationButtonType.RETURN, displayName: '退', enable: true }
]
// 发起人的按钮权限。暂时定死,不可以编辑
@ -522,7 +538,7 @@ export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '退', enable: false }
{ id: OperationButtonType.RETURN, displayName: '退', enable: false }
]
export const MULTI_LEVEL_DEPT: DictDataVO = [
@ -542,3 +558,13 @@ export const MULTI_LEVEL_DEPT: DictDataVO = [
{ label: '第 14 级部门', value: 14 },
{ label: '第 15 级部门', value: 15 }
]
/**
* 流程实例的变量枚举
*/
export enum ProcessVariableEnum {
/**
* 发起用户 ID
*/
START_USER_ID = 'PROCESS_START_USER_ID'
}

View File

@ -1,4 +1,5 @@
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import SimpleProcessViewer from './SimpleProcessViewer.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner }
export { SimpleProcessDesigner, SimpleProcessViewer}

View File

@ -1,4 +1,5 @@
import { cloneDeep } from 'lodash-es'
import { TaskStatusEnum } from '@/api/bpm/task'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
@ -13,8 +14,10 @@ import {
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType,
FieldPermissionType
FieldPermissionType,
ProcessVariableEnum
} from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
watch(
@ -26,12 +29,30 @@ export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlo
return node
}
// 解析 formCreate 所有表单字段, 并返回
const parseFormCreateFields = (formFields?: string[]) => {
const result: Array<Record<string, any>> = []
if (formFields) {
formFields.forEach((fieldStr: string) => {
parseFormFields(JSON.parse(fieldStr), result)
})
}
// 固定添加发起人 ID 字段
result.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
type: 'UserSelect',
required: true
})
return result
}
/**
* @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
*/
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
// 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型
@ -44,49 +65,26 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
}
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
const getDefaultFieldsPermission = (formFields?: string[]) => {
const defaultFieldsPermission: Array<Record<string, string>> = []
let defaultFieldsPermission: Array<Record<string, any>> = []
if (formFields) {
formFields.forEach((fieldStr: string) => {
parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => {
return {
field: item.field,
title: item.title,
permission: defaultPermission
}
})
}
return defaultFieldsPermission
}
// 解析字段。赋给默认权限
const parseFieldsSetDefaultPermission = (
rule: Record<string, any>,
fieldsPermission: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { /**type,*/ field, title: tempTitle, children } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
fieldsPermission.push({
field,
title,
permission: defaultPermission
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseFieldsSetDefaultPermission(rule, fieldsPermission)
})
}
}
// 获取表单的所有字段,作为下拉框选项
const formFieldOptions = parseFormCreateFields(unref(formFields))
return {
formType,
fieldsPermissionConfig,
formFieldOptions,
getNodeConfigFormFields
}
}
@ -94,50 +92,8 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
* @description 获取表单的字段
*/
export function useFormFields() {
// 解析后的表单字段
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const parseFormFields = () => {
const parsedFormFields: Array<Record<string, string>> = []
if (formFields) {
formFields.value.forEach((fieldStr: string) => {
parseField(JSON.parse(fieldStr), parsedFormFields)
})
}
return parsedFormFields
}
// 解析字段。
const parseField = (
rule: Record<string, any>,
parsedFormFields: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { field, title: tempTitle, children, type } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
parsedFormFields.push({
field,
title,
type
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseField(rule, parsedFormFields)
})
}
}
return parseFormFields()
return parseFormCreateFields(unref(formFields))
}
export type UserTaskFormType = {
@ -151,6 +107,8 @@ export type UserTaskFormType = {
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
expression?: string // 流程表达式
formUser?: string // 表单内用户字段
formDept?: string // 表单内部门字段
approveRatio?: number
rejectHandlerType?: RejectHandlerType
returnNodeId?: string
@ -173,6 +131,8 @@ export type CopyTaskFormType = {
userIds?: number[] // 用户
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
formUser?: string // 表单内用户字段
formDept?: string // 表单内部门字段
expression?: string // 流程表达式
}
@ -186,6 +146,7 @@ export function useNodeForm(nodeType: NodeType) {
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
@ -281,6 +242,18 @@ export function useNodeForm(nodeType: NodeType) {
}
}
// 表单内用户字段
if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) {
const formFieldOptions = parseFormCreateFields(unref(formFields))
const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser)
showText = `表单用户:${item?.title}`
}
// 表单内部门负责人
if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) {
showText = `表单内部门负责人`
}
// 发起人自选
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
showText = `发起人自选`
@ -327,6 +300,9 @@ export function useNodeForm(nodeType: NodeType) {
case CandidateStrategy.USER_GROUP:
candidateParam = configForm.value.userGroups!.join(',')
break
case CandidateStrategy.FORM_USER:
candidateParam = configForm.value.formUser!
break
case CandidateStrategy.EXPRESSION:
candidateParam = configForm.value.expression!
break
@ -346,6 +322,13 @@ export function useNodeForm(nodeType: NodeType) {
candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
break
}
// 表单内部门的负责人
case CandidateStrategy.FORM_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
const deptFieldOnForm = configForm.value.formDept!
candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '')
break
}
default:
break
}
@ -375,6 +358,9 @@ export function useNodeForm(nodeType: NodeType) {
case CandidateStrategy.USER_GROUP:
configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.FORM_USER:
configForm.value.formUser = candidateParam
break
case CandidateStrategy.EXPRESSION:
configForm.value.expression = candidateParam
break
@ -395,6 +381,14 @@ export function useNodeForm(nodeType: NodeType) {
configForm.value.deptLevel = +paramArray[1]
break
}
// 表单内的部门负责人
case CandidateStrategy.FORM_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
const paramArray = candidateParam.split('|')
configForm.value.formDept = paramArray[0]
configForm.value.deptLevel = +paramArray[1]
break
}
default:
break
}
@ -476,3 +470,26 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
blurEvent
}
}
/**
* @description 根据节点任务状态,获取节点任务状态样式
*/
export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string {
if (!taskStatus) {
return ''
}
if (taskStatus === TaskStatusEnum.APPROVE) {
return 'status-pass'
}
if (taskStatus === TaskStatusEnum.RUNNING) {
return 'status-running'
}
if (taskStatus === TaskStatusEnum.REJECT) {
return 'status-reject'
}
if (taskStatus === TaskStatusEnum.CANCEL) {
return 'status-cancel'
}
return ''
}

View File

@ -26,7 +26,7 @@
</div>
</template>
<div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支该分支不可编辑和删除</div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件进入此分支该分支不可编辑和删除</div>
<div v-else>
<el-form
ref="formRef"

View File

@ -60,7 +60,8 @@
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
prop="deptIds"
@ -122,7 +123,57 @@
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
prop="formUser"
>
<el-select v-model="configForm.formUser" clearable style="width: 100%">
<el-option
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
label="表单内部门字段"
prop="formDept"
>
<el-select v-model="configForm.formDept" clearable style="width: 100%">
<el-option
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
@ -201,7 +252,8 @@ import {
CandidateStrategy,
NodeType,
CANDIDATE_STRATEGY,
FieldPermissionType
FieldPermissionType,
MULTI_LEVEL_DEPT
} from '../consts'
import {
useWatchNode,
@ -221,6 +273,15 @@ const props = defineProps({
required: true
}
})
const deptLevelLabel = computed(() => {
let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)'
} else {
label = label + '(发起人部门向上)'
}
return label
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
@ -230,9 +291,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限配置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
useFormFieldsPermission(FieldPermissionType.READ)
// 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect')
})
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect')
})
// 抄送人表单配置
const formRef = ref() // 表单 Ref
// 表单校验规则
@ -243,6 +311,8 @@ const formRules = reactive({
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
})
@ -260,11 +330,7 @@ const {
const configForm = tempConfigForm as Ref<CopyTaskFormType>
// 抄送人策略, 去掉发起人自选 和 发起人自己
const copyUserStrategies = computed(() => {
return CANDIDATE_STRATEGY.filter(
(item) =>
item.value !== CandidateStrategy.START_USER_SELECT &&
item.value !== CandidateStrategy.START_USER
)
return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER)
})
// 改变抄送人设置策略
const changeCandidateStrategy = () => {
@ -274,6 +340,7 @@ const changeCandidateStrategy = () => {
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
configForm.value.formUser = ''
}
// 保存配置
const saveConfig = async () => {

View File

@ -119,7 +119,6 @@ const saveConfig = async () => {
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置发起人的按钮权限
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
settingVisible.value = false
return true
}

View File

@ -56,7 +56,6 @@
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
@ -94,25 +93,6 @@
show-checkbox
/>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
@ -134,13 +114,7 @@
prop="userIds"
span="24"
>
<el-select
v-model="configForm.userIds"
clearable
multiple
style="width: 100%"
@change="changedCandidateUsers"
>
<el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
@ -163,6 +137,57 @@
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
label="表单内用户字段"
prop="formUser"
>
<el-select v-model="configForm.formUser" clearable style="width: 100%">
<el-option
v-for="(item, idx) in userFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
label="表单内部门字段"
prop="formDept"
>
<el-select v-model="configForm.formDept" clearable style="width: 100%">
<el-option
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy ==
CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<!-- TODO @jason后续要支持选择已经存好的表达式 -->
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
@ -184,14 +209,7 @@
:key="index"
class="flex items-center"
>
<el-radio
:value="item.value"
:label="item.value"
:disabled="
item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
notAllowedMultiApprovers
"
>
<el-radio :value="item.value" :label="item.value">
{{ item.label }}
</el-radio>
<el-form-item prop="approveRatio">
@ -481,6 +499,8 @@ const deptLevelLabel = computed(() => {
let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)'
} else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
label = label + '(表单内部门向上)'
} else {
label = label + '(发起人部门向上)'
}
@ -495,9 +515,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限设置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
useFormFieldsPermission(FieldPermissionType.READ)
// 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'UserSelect')
})
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
return formFieldOptions.filter((item) => item.type === 'DeptSelect')
})
// 操作按钮设置
const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting()
@ -511,6 +538,8 @@ const formRules = reactive({
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
@ -537,8 +566,7 @@ const {
getShowText
} = useNodeForm(NodeType.USER_TASK_NODE)
const configForm = tempConfigForm as Ref<UserTaskFormType>
// 不允许多人审批
const notAllowedMultiApprovers = ref(false)
// 改变审批人设置策略
const changeCandidateStrategy = () => {
configForm.value.userIds = []
@ -547,30 +575,11 @@ const changeCandidateStrategy = () => {
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
configForm.value.formUser = ''
configForm.value.formDept = ''
configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
if (
configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
// 改变审批候选人
const changedCandidateUsers = () => {
if (
configForm.value.userIds &&
configForm.value.userIds?.length <= 1 &&
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
// 审批方式改变
const approveMethodChanged = () => {
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
@ -579,7 +588,7 @@ const approveMethodChanged = () => {
}
formRef.value.clearValidate('approveRatio')
}
// 审批拒绝 可退的节点
// 审批拒绝 可退的节点
const returnTaskList = ref<SimpleFlowNode[]>([])
// 审批人超时未处理设置
const {
@ -666,11 +675,6 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
configForm.value.candidateStrategy = node.candidateStrategy!
// 解析候选人参数
parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
if (configForm.value.userIds && configForm.value.userIds.length > 1) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
// 2.2 设置审批方式
configForm.value.approveMethod = node.approveMethod!
if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {

View File

@ -1,11 +1,17 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="showInput"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
@ -24,9 +30,9 @@
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div class="node-toolbar">
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
@ -34,15 +40,23 @@
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
<CopyTaskNodeConfig
v-if="!readonly && currentNode"
ref="nodeSetting"
:flow-node="currentNode"
/>
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode } from '../node'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
defineOptions({
name: 'CopyTaskNode'
@ -57,7 +71,8 @@ const props = defineProps({
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
@ -66,6 +81,9 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}

View File

@ -1,13 +1,102 @@
<template>
<div class="end-node-wrapper">
<div class="end-node-box">
<div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick">
<span class="node-fixed-name" title="结束">结束</span>
</div>
</div>
<el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body>
<el-row>
<el-table
:data="processInstanceInfos"
size="small"
border
header-cell-class-name="table-header-gray"
>
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column
label="发起人"
prop="assigneeUser.nickname"
min-width="100"
align="center"
/>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import { SimpleFlowNode } from '../consts'
import { useWatchNode, useTaskStatusClass } from '../node'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
name: 'EndEventNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
// 监控节点变化
const currentNode = useWatchNode(props)
// 是否只读
const readonly = inject<Boolean>('readonly')
const processInstance = inject<Ref<any>>('processInstance')
// 审批信息的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
const nodeClick = () => {
if (readonly) {
if(processInstance && processInstance.value){
processInstanceInfos.value = [
{
assigneeUser: processInstance.value.startUser,
createTime: processInstance.value.startTime,
endTime: processInstance.value.endTime,
status: processInstance.value.status,
durationInMillis: processInstance.value.durationInMillis
}
]
dialogVisible.value = true
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -1,7 +1,17 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加条件</div>
<div
v-if="readonly"
class="branch-node-readonly"
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
>
<span class="iconfont icon-exclusive icon-size condition"></span>
</div>
<el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
>添加条件</el-button
>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
@ -17,9 +27,15 @@
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !item.showText }">
<div
class="node-box"
:class="[
{ 'node-config-error': !item.showText },
`${useTaskStatusClass(item.activityStatus)}`
]"
>
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<div v-if="!readonly && showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@ -39,7 +55,10 @@
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
<div
class="node-toolbar"
v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
>
<div class="toolbar-icon">
<Icon
color="#0089ff"
@ -65,7 +84,7 @@
<Icon icon="ep:arrow-right" />
</div>
</div>
<NodeHandler v-model:child-node="item.childNode" />
<NodeHandler v-model:child-node="item.childNode" :current-node="item" />
</div>
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
@ -78,7 +97,11 @@
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
</template>
@ -87,6 +110,7 @@ import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
@ -94,10 +118,6 @@ defineOptions({
name: 'ExclusiveNode'
})
const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
@ -113,10 +133,9 @@ const emits = defineEmits<{
nodeType: number
]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
const currentNode = ref<SimpleFlowNode>(props.flowNode)
// const conditionNodes = computed(() => currentNode.value.conditionNodes);
watch(
() => props.flowNode,
(newValue) => {
@ -139,6 +158,9 @@ const clickEvent = (index: number) => {
}
const conditionNodeConfig = (nodeId: string) => {
if (readonly) {
return
}
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
@ -193,7 +215,7 @@ const recursiveFindParentNode = (
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
if (!node || node.type === NodeType.START_USER_NODE) {
return
}
if (node.type === nodeType) {

View File

@ -0,0 +1,233 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div
v-if="readonly"
class="branch-node-readonly"
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
>
<span class="iconfont icon-inclusive icon-size inclusive"></span>
</div>
<el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain
>添加条件</el-button
>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"> </div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !item.showText },
`${useTaskStatusClass(item.activityStatus)}`
]"
>
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div
class="node-toolbar"
v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
>
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<div
class="branch-node-move move-node-left"
v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length"
@click="moveNode(index, -1)"
>
<Icon icon="ep:arrow-left" />
</div>
<div
class="branch-node-move move-node-right"
v-if="
!readonly &&
currentNode.conditionNodes &&
index < currentNode.conditionNodes.length - 2
"
@click="moveNode(index, 1)"
>
<Icon icon="ep:arrow-right" />
</div>
</div>
<NodeHandler v-model:child-node="item.childNode" :current-node="item" />
</div>
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useTaskStatusClass } from '../node'
import { getDefaultInclusiveConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'InclusiveNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
const currentNode = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
// 失去焦点
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
if (readonly) {
return
}
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
// 新增条件
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '包容条件' + len,
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
// 删除条件
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
// 更新此节点为后续孩子节点
emits('update:modelValue', childNode)
}
}
}
// 移动节点
const moveNode = (index: number, to: number) => {
// -1 :向左 1 向右
if (currentNode.value.conditionNodes) {
currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
index + to,
1,
currentNode.value.conditionNodes[index]
)[0]
}
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_USER_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点NodeType.INCLUSIVE_BRANCH_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>

View File

@ -1,7 +1,16 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加分支</div>
<div
v-if="readonly"
class="branch-node-readonly"
:class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
>
<span class="iconfont icon-parallel icon-size parallel"></span>
</div>
<el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain
>添加分支</el-button
>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
@ -17,7 +26,7 @@
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box">
<div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
@ -39,7 +48,7 @@
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar">
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon">
<Icon
color="#0089ff"
@ -49,20 +58,8 @@
/>
</div>
</div>
<!-- <div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
<Icon icon="ep:arrow-left" />
</div> -->
<!-- <div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)">
<Icon icon="ep:arrow-right" />
</div> -->
</div>
<NodeHandler v-model:child-node="item.childNode" />
<NodeHandler v-model:child-node="item.childNode" :current-node="item" />
</div>
</div>
<!-- 递归显示子节点 -->
@ -74,7 +71,11 @@
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
</template>
@ -82,8 +83,8 @@
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ParallelNode'
@ -106,6 +107,8 @@ const emits = defineEmits<{
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
// 是否只读
const readonly = inject<Boolean>('readonly')
watch(
() => props.flowNode,
@ -169,7 +172,7 @@ const recursiveFindParentNode = (
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
if (!node || node.type === NodeType.START_USER_NODE) {
return
}
if (node.type === nodeType) {

View File

@ -1,7 +1,13 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span
@ -19,27 +25,88 @@
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
<Icon icon="ep:arrow-right-bold" v-if="!readonly" />
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
</div>
<StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
<StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
<!-- 审批记录 -->
<el-dialog
:title="dialogTitle || '审批记录'"
v-model="dialogVisible"
width="1000px"
append-to-body
>
<el-row>
<el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column label="审批人" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2 } from '../node'
import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
name: 'StartEventNode'
})
@ -49,6 +116,8 @@ const props = defineProps({
default: () => null
}
})
const readonly = inject<Boolean>('readonly') // 是否只读
const tasks = inject<Ref<any[]>>('tasks')
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
@ -59,11 +128,27 @@ const currentNode = useWatchNode(props)
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
// 把当前节点传递给配置组件
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
//
const nodeClick = () => {
if (readonly) {
// 只读模式,弹窗显示任务信息
if (tasks && tasks.value) {
dialogTitle.value = currentNode.value.name
selectTasks.value = tasks.value.filter(
(item: any) => item?.taskDefinitionKey === currentNode.value.id
)
dialogVisible.value = true
}
} else {
// 编辑模式,打开节点配置、把当前节点传递给配置组件
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
}
// 任务的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
</script>
<style lang="scss" scoped></style>

View File

@ -1,11 +1,17 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
<input
v-if="showInput"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
@ -17,23 +23,27 @@
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
<Icon icon="ep:arrow-right-bold" v-if="!readonly" />
</div>
<div class="node-toolbar">
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
</div>
<UserTaskNodeConfig
@ -42,12 +52,69 @@
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
<!-- 审批记录 -->
<el-dialog
:title="dialogTitle || '审批记录'"
v-model="dialogVisible"
width="1000px"
append-to-body
>
<el-row>
<el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column label="审批人" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2 } from '../node'
import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
name: 'UserTaskNode'
})
@ -61,22 +128,36 @@ const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
const tasks = inject<Ref<any[]>>('tasks')
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
// 把当前节点传递给配置组件
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
const nodeClick = () => {
if (readonly) {
if (tasks && tasks.value) {
dialogTitle.value = currentNode.value.name
// 只读模式,弹窗显示任务信息
selectTasks.value = tasks.value.filter(
(item: any) => item?.taskDefinitionKey === currentNode.value.id
)
dialogVisible.value = true
}
} else {
// 编辑模式,打开节点配置、把当前节点传递给配置组件
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
}
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
// 查找可以驳回用户节点
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] // 匹配的节点
@ -84,5 +165,10 @@ const findReturnTaskNodes = (
// 从父节点查找
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
}
// 任务的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
</script>
<style lang="scss" scoped></style>

View File

@ -8,6 +8,14 @@ export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean
return '条件' + (index + 1)
}
// 获取包容分支条件节点默认的名称
export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
if (defaultFlow) {
return '其它情况'
}
return '包容条件' + (index + 1)
}
export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE

View File

@ -0,0 +1,152 @@
<template>
<Dialog v-model="dialogVisible" title="人员选择" width="800">
<el-row class="gap2" v-loading="formLoading">
<el-col :span="6">
<ContentWrap class="h-1/1">
<el-tree
ref="treeRef"
:data="deptTree"
:expand-on-click-node="false"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</ContentWrap>
</el-col>
<el-col :span="17">
<el-transfer
v-model="selectedUserIdList"
:titles="['未选', '已选']"
filterable
filter-placeholder="搜索成员"
:data="transferUserList"
:props="{ label: 'nickname', key: 'id' }"
/>
</el-col>
</el-row>
<template #footer>
<el-button
:disabled="formLoading || !selectedUserIdList?.length"
type="primary"
@click="submitForm"
>
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'UserSelectForm' })
const emit = defineEmits<{
confirm: [id: any, userList: any[]]
}>()
const { t } = useI18n() // 国际
const message = useMessage() // 消息弹窗
const deptTree = ref<Tree[]>([]) // 部门树形结构化
const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
const selectedUserIdList: any = ref([]) // 选中的用户列表
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const activityId = ref()
/** 计算属性:合并已选择的用户和当前部门过滤后的用户 */
const transferUserList = computed(() => {
// 1.1 获取所有已选择的用户
const selectedUsers = userList.value.filter((user: any) =>
selectedUserIdList.value.includes(user.id)
)
// 1.2 获取当前部门过滤后的未选择用户
const filteredUnselectedUsers = filteredUserList.value.filter(
(user: any) => !selectedUserIdList.value.includes(user.id)
)
// 2. 合并并去重
return [...selectedUsers, ...filteredUnselectedUsers]
})
/** 打开弹窗 */
const open = async (id: number, selectedList?: any[]) => {
activityId.value = id
resetForm()
// 加载部门、用户列表
deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
userList.value = await UserApi.getSimpleUserList()
// 初始状态下,过滤列表等于所有用户列表
filteredUserList.value = [...userList.value]
selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
dialogVisible.value = true
}
/** 获取部门过滤后的用户列表 */
const getUserList = async (deptId?: number) => {
formLoading.value = true
try {
// @ts-ignore
// TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
// TODO @Zqqq这个可以使用前端过滤么通过 deptList 获取到 deptId 子节点,然后去 userList
const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
// 更新过滤后的用户列表
filteredUserList.value = data.list
} finally {
formLoading.value = false
}
}
/** 提交选择 */
const submitForm = async () => {
try {
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 从所有用户列表中筛选出已选择的用户
const emitUserList = userList.value.filter((user: any) =>
selectedUserIdList.value.includes(user.id)
)
// 发送操作成功的事件
emit('confirm', activityId.value, emitUserList)
} finally {
}
}
/** 重置表单 */
const resetForm = () => {
deptTree.value = []
userList.value = []
filteredUserList.value = []
selectedUserIdList.value = []
}
/** 处理部门被点击 */
const handleNodeClick = (row: { [key: string]: any }) => {
getUserList(row.id)
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
</script>
<style lang="scss" scoped>
:deep() {
.el-transfer {
display: flex;
}
.el-transfer__buttons {
display: flex !important;
flex-direction: column-reverse;
justify-content: center;
gap: 20px;
.el-transfer__button:nth-child(2) {
margin: 0;
}
}
}
</style>

View File

@ -1211,6 +1211,76 @@
"isAttr": true
}
]
},
{
"name": "AssignStartUserHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "RejectHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "RejectReturnTaskId",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "String",
"isBody": true
}
]
},
{
"name": "AssignEmptyHandlerType",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Integer",
"isBody": true
}
]
},
{
"name": "AssignEmptyUserIds",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "String",
"isBody": true
}
]
}
],
"emumerations": []

View File

@ -1,5 +1,5 @@
<template>
<div class="process-panel__container" :style="{ width: `${width}px` }">
<div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }">
<el-collapse v-model="activeTab">
<el-collapse-item name="base">
<!-- class="panel-tab__title" -->
@ -54,6 +54,10 @@
<template #title><Icon icon="ep:promotion" />其他</template>
<element-other-config :id="elementId" />
</el-collapse-item>
<el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
<template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
<element-custom-config :id="elementId" :type="elementType" />
</el-collapse-item>
</el-collapse>
</div>
</template>

View File

@ -0,0 +1,283 @@
<!-- UserTask 自定义配置
1. 审批人与提交人为同一人时
2. 审批人拒绝时
3. 审批人为空时
-->
<template>
<div class="panel-tab__content">
<el-divider content-position="left">审批人拒绝时</el-divider>
<el-form-item prop="rejectHandlerType">
<el-radio-group
v-model="rejectHandlerType"
:disabled="returnTaskList.length === 0"
@change="updateRejectHandlerType"
>
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
label="驳回节点"
prop="returnNodeId"
>
<el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
<el-option
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人为空时</el-divider>
<el-form-item prop="assignEmptyHandlerType">
<el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
span="24"
>
<el-select
v-model="assignEmptyUserIds"
clearable
multiple
style="width: 100%"
@change="updateAssignEmptyUserIds"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人与提交人为同一人时</el-divider>
<el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</div>
</template>
<script lang="ts" setup>
import {
ASSIGN_START_USER_HANDLER_TYPES,
RejectHandlerType,
REJECT_HANDLER_TYPES,
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType
} from '@/components/SimpleProcessDesignerV2/src/consts'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'ElementCustomConfig' })
const props = defineProps({
id: String,
type: String
})
const prefix = inject('prefix')
// 审批人与提交人为同一人时
const assignStartUserHandlerTypeEl = ref()
const assignStartUserHandlerType = ref()
// 审批人拒绝时
const rejectHandlerTypeEl = ref()
const rejectHandlerType = ref()
const returnNodeIdEl = ref()
const returnNodeId = ref()
const returnTaskList = ref([])
// 审批人为空时
const assignEmptyHandlerTypeEl = ref()
const assignEmptyHandlerType = ref()
const assignEmptyUserIdsEl = ref()
const assignEmptyUserIds = ref()
const elExtensionElements = ref()
const otherExtensions = ref()
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
const resetCustomConfigList = () => {
bpmnElement.value = bpmnInstances().bpmnElement
// 获取可回退的列表
returnTaskList.value = findAllPredecessorsExcludingStart(
bpmnElement.value.id,
bpmnInstances().modeler
)
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
// 审批人与提交人为同一人时
assignStartUserHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
// 审批人拒绝时
rejectHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:RejectHandlerType`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
rejectHandlerType.value = rejectHandlerTypeEl.value.value
returnNodeIdEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:RejectReturnTaskId`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
returnNodeId.value = returnNodeIdEl.value.value
// 审批人为空时
assignEmptyHandlerTypeEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
assignEmptyUserIdsEl.value =
elExtensionElements.value.values?.filter(
(ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
)?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
// 如果数字超出了最大安全整数范围,则将其作为字符串处理
let num = Number(item)
return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
})
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
(ex) =>
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
ex.$type !== `${prefix}:RejectHandlerType` &&
ex.$type !== `${prefix}:RejectReturnTaskId` &&
ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
ex.$type !== `${prefix}:AssignEmptyUserIds`
) ?? []
// 更新元素扩展属性,避免后续报错
updateElementExtensions()
}
const updateAssignStartUserHandlerType = () => {
assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
updateElementExtensions()
}
const updateRejectHandlerType = () => {
rejectHandlerTypeEl.value.value = rejectHandlerType.value
returnNodeId.value = returnTaskList.value[0].id
returnNodeIdEl.value.value = returnNodeId.value
updateElementExtensions()
}
const updateReturnNodeId = () => {
returnNodeIdEl.value.value = returnNodeId.value
updateElementExtensions()
}
const updateAssignEmptyHandlerType = () => {
assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
updateElementExtensions()
}
const updateAssignEmptyUserIds = () => {
assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
updateElementExtensions()
}
const updateElementExtensions = () => {
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
values: [
...otherExtensions.value,
assignStartUserHandlerTypeEl.value,
rejectHandlerTypeEl.value,
returnNodeIdEl.value,
assignEmptyHandlerTypeEl.value,
assignEmptyUserIdsEl.value
]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
extensionElements: extensions
})
}
watch(
() => props.id,
(val) => {
val &&
val.length &&
nextTick(() => {
resetCustomConfigList()
})
},
{ immediate: true }
)
function findAllPredecessorsExcludingStart(elementId, modeler) {
const elementRegistry = modeler.get('elementRegistry')
const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
const predecessors = new Set() // 使用 Set 来避免重复节点
// 检查是否是开始事件节点
function isStartEvent(element) {
return element.type === 'bpmn:StartEvent'
}
function findPredecessorsRecursively(element) {
// 获取与当前节点相连的所有连接
const incomingConnections = allConnections.filter((connection) => connection.target === element)
incomingConnections.forEach((connection) => {
const source = connection.source // 获取前置节点
// 只添加不是开始事件的前置节点
if (!isStartEvent(source)) {
predecessors.add(source.businessObject)
// 递归查找前置节点
findPredecessorsRecursively(source)
}
})
}
const targetElement = elementRegistry.get(elementId)
if (targetElement) {
findPredecessorsRecursively(targetElement)
}
return Array.from(predecessors) // 返回前置节点数组
}
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
})
</script>

View File

@ -268,9 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
const resetFormList = () => {
bpmnELement.value = bpmnInstances().bpmnElement
formKey.value = bpmnELement.value.businessObject.formKey
if (formKey.value?.length > 0) {
formKey.value = parseInt(formKey.value)
}
// if (formKey.value?.length > 0) {
// formKey.value = parseInt(formKey.value)
// }
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnELement.value.businessObject.get('extensionElements') ||

View File

@ -80,7 +80,7 @@ const resetAttributesList = () => {
otherExtensionList.value = [] // 其他扩展配置
bpmnElementProperties.value =
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => {
bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => {
if (ex.$type !== `${prefix}:Properties`) {
otherExtensionList.value.push(ex)
}

View File

@ -5,7 +5,7 @@ $--color-danger: #ff4d4f;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';
@use '~element-ui/packages/theme-chalk/src/index';
.el-table td,
.el-table th {

View File

@ -1,2 +1,117 @@
@import './process-designer.scss';
@import './process-panel.scss';
@use './process-designer.scss';
@use './process-panel.scss';
$success-color: #4eb819;
$primary-color: #409EFF;
$danger-color: #F56C6C;
$cancel-color: #909399;
.process-viewer {
position: relative;
border: 1px solid #EFEFEF;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
.success-arrow {
fill: $success-color;
stroke: $success-color;
}
.success-conditional {
fill: white;
stroke: $success-color;
}
.success.djs-connection {
.djs-visual path {
stroke: $success-color!important;
//marker-end: url(#sequenceflow-end-white-success)!important;
}
}
.success.djs-connection.condition-expression {
.djs-visual path {
//marker-start: url(#conditional-flow-marker-white-success)!important;
}
}
.success.djs-shape {
.djs-visual rect {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $success-color!important;
}
.djs-visual path:nth-child(2) {
stroke: $success-color!important;
fill: $success-color!important;
}
.djs-visual circle {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
}
}
.primary.djs-shape {
.djs-visual rect {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $primary-color!important;
}
.djs-visual circle {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
}
}
.danger.djs-shape {
.djs-visual rect {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $danger-color!important;
}
.djs-visual circle {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
}
}
.cancel.djs-shape {
.djs-visual rect {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $cancel-color!important;
}
.djs-visual circle {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
}
}
}
.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
display: none;
}

View File

@ -1,6 +1,6 @@
@import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
@import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
@import 'bpmn-js-token-simulation/assets/css/normalize.css';
@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
@use 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
@use 'bpmn-js-token-simulation/assets/css/normalize.css';
// 边框被 token-simulation 样式覆盖了
.djs-palette {