Commit b94235ad by 芋道源码 Committed by Gitee

!452 BPM 的草稿 PR(非 BPM 开发成员,不用关注)

Merge pull request !452 from 芋道源码/feature/bpm
parents 2d245d1f 619491b4
......@@ -30,7 +30,7 @@ export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', params })
}
export const getModel = async (id: number) => {
export const getModel = async (id: string) => {
return await request.get({ url: '/bpm/model/get?id=' + id })
}
......@@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data })
}
export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data })
}
// 任务状态修改
export const updateModelState = async (id: number, state: number) => {
const data = {
......
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
id: string
name: string
......@@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
processDefinition?: ProcessDefinitionVO
}
// 用户信息
export type User = {
id: number,
nickname: string,
avatar: string
}
// 审批任务信息
export type ApprovalTaskInfo = {
id: number,
ownerUser: User,
assigneeUser: User,
status: number,
reason: string
}
// 审批节点信息
export type ApprovalNodeInfo = {
id : number
name: string
nodeType: NodeType
status: number
startTime?: Date
endTime?: Date
candidateUserList?: User[]
tasks: ApprovalTaskInfo[]
}
export const getProcessInstanceMyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/my-page', params })
}
......@@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params })
}
// 获取审批详情
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
}
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
}
import request from '@/config/axios'
export const updateBpmSimpleModel = async (data) => {
return await request.post({
url: '/bpm/model/simple/update',
data: data
})
}
export const getBpmSimpleModel = async (id) => {
return await request.get({
url: '/bpm/model/simple/get?id=' + id
})
}
import request from '@/config/axios'
/**
* 任务状态枚举
*/
export enum TaskStatusEnum {
/**
* 未开始
*/
NOT_START = -1,
/**
* 待审批
*/
WAIT = 0,
/**
* 审批中
*/
RUNNING = 1,
/**
* 审批通过
*/
APPROVE = 2,
/**
* 审批不通过
*/
REJECT = 3,
/**
* 已取消
*/
CANCEL = 4,
/**
* 已退回
*/
RETURN = 5,
/**
* 委派中
*/
DELEGATE = 6,
/**
* 审批通过中
*/
APPROVING = 7,
}
export type TaskVO = {
id: number
}
......
/* stylelint-disable order/properties-order */
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" v-model="visible" width="auto">
<div class="add-node-popover-body">
<a class="add-node-popover-item approver" @click="addType(1)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>审批人</p>
</a>
<a class="add-node-popover-item notifier" @click="addType(2)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>抄送人</p>
</a>
<a class="add-node-popover-item condition" @click="addType(4)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>条件分支</p>
</a>
</div>
<template #reference>
<button class="btn" type="button">
<span class="iconfont"></span>
</button>
</template>
</el-popover>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
let props = defineProps({
childNodeP: {
type: Object,
default: () => ({})
}
})
let emits = defineEmits(['update:childNodeP'])
let visible = ref(false)
const addType = (type) => {
visible.value = false
if (type != 4) {
var data
if (type == 1) {
data = {
nodeName: '审核人',
error: true,
type: 1,
settype: 1,
selectMode: 0,
selectRange: 0,
directorLevel: 1,
examineMode: 1,
noHanderAction: 1,
examineEndDirectorLevel: 0,
childNode: props.childNodeP,
nodeUserList: []
}
} else if (type == 2) {
data = {
nodeName: '抄送人',
type: 2,
ccSelfSelectFlag: 1,
childNode: props.childNodeP,
nodeUserList: []
}
}
emits('update:childNodeP', data)
} else {
emits('update:childNodeP', {
nodeName: '路由',
type: 4,
childNode: null,
conditionNodes: [
{
nodeName: '条件1',
error: true,
type: 3,
priorityLevel: 1,
conditionList: [],
nodeUserList: [],
childNode: props.childNodeP
},
{
nodeName: '条件2',
type: 3,
priorityLevel: 2,
conditionList: [],
nodeUserList: [],
childNode: null
}
]
})
}
}
</script>
<style scoped lang="scss">
.add-node-btn-box {
width: 240px;
display: inline-flex;
-ms-flex-negative: 0;
flex-shrink: 0;
-webkit-box-flex: 1;
-ms-flex-positive: 1;
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #cacaca;
}
.add-node-btn {
user-select: none;
width: 240px;
padding: 20px 0 32px;
display: flex;
-webkit-box-pack: center;
justify-content: center;
flex-shrink: 0;
-webkit-box-flex: 1;
flex-grow: 1;
.btn {
outline: none;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
width: 30px;
height: 30px;
background: #3296fa;
border-radius: 50%;
position: relative;
border: none;
line-height: 30px;
-webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
color: #fff;
font-size: 16px;
}
&:hover {
transform: scale(1.3);
box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
}
&:active {
transform: none;
background: #1e83e9;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
}
}
}
.add-node-popover-body {
display: flex;
.add-node-popover-item {
margin-right: 10px;
cursor: pointer;
text-align: center;
flex: 1;
color: #191f25 !important;
.item-wrapper {
user-select: none;
display: inline-block;
width: 80px;
height: 80px;
margin-bottom: 5px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
font-size: 35px;
line-height: 80px;
}
}
&.approver {
.item-wrapper {
color: #ff943e;
}
}
&.notifier {
.item-wrapper {
color: #3296fa;
}
}
&.condition {
.item-wrapper {
color: #15bc83;
}
}
&:hover {
.item-wrapper {
background: #3296fa;
box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
}
.iconfont {
color: #fff;
}
}
&:active {
.item-wrapper {
box-shadow: none;
background: #eaeaea;
}
.iconfont {
color: inherit;
}
}
}
}
</style>
/**
* todo
*/
export const arrToStr = (arr?: [{ name: string }]) => {
if (arr) {
return arr
.map((item) => {
return item.name
})
.toString()
}
}
export const setApproverStr = (nodeConfig: any) => {
if (nodeConfig.settype == 1) {
if (nodeConfig.nodeUserList.length == 1) {
return nodeConfig.nodeUserList[0].name
} else if (nodeConfig.nodeUserList.length > 1) {
if (nodeConfig.examineMode == 1) {
return arrToStr(nodeConfig.nodeUserList)
} else if (nodeConfig.examineMode == 2) {
return nodeConfig.nodeUserList.length + '人会签'
}
}
} else if (nodeConfig.settype == 2) {
const level =
nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
if (nodeConfig.examineMode == 1) {
return level
} else if (nodeConfig.examineMode == 2) {
return level + '会签'
}
} else if (nodeConfig.settype == 4) {
if (nodeConfig.selectRange == 1) {
return '发起人自选'
} else {
if (nodeConfig.nodeUserList.length > 0) {
if (nodeConfig.selectRange == 2) {
return '发起人自选'
} else {
return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
}
} else {
return ''
}
}
} else if (nodeConfig.settype == 5) {
return '发起人自己'
} else if (nodeConfig.settype == 7) {
return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
}
}
export const copyerStr = (nodeConfig: any) => {
if (nodeConfig.nodeUserList.length != 0) {
return arrToStr(nodeConfig.nodeUserList)
} else {
if (nodeConfig.ccSelfSelectFlag == 1) {
return '发起人自选'
}
}
}
export const conditionStr = (nodeConfig, index) => {
const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
if (conditionList.length == 0) {
return index == nodeConfig.conditionNodes.length - 1 &&
nodeConfig.conditionNodes[0].conditionList.length != 0
? '其他条件进入此流程'
: '请设置条件'
} else {
let str = ''
for (let i = 0; i < conditionList.length; i++) {
const {
columnId,
columnType,
showType,
showName,
optType,
zdy1,
opt1,
zdy2,
opt2,
fixedDownBoxValue
} = conditionList[i]
if (columnId == 0) {
if (nodeUserList.length != 0) {
str += '发起人属于:'
str +=
nodeUserList
.map((item) => {
return item.name
})
.join('或') + ' 并且 '
}
}
if (columnType == 'String' && showType == '3') {
if (zdy1) {
str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
}
}
if (columnType == 'Double') {
if (optType != 6 && zdy1) {
const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
str += `${showName} ${optTypeStr} ${zdy1} 并且 `
} else if (optType == 6 && zdy1 && zdy2) {
str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
}
}
}
return str ? str.substring(0, str.length - 4) : '请设置条件'
}
}
export const dealStr = (str: string, obj) => {
const arr = []
const list = str.split(',')
for (const elem in obj) {
list.map((item) => {
if (item == elem) {
arr.push(obj[elem].value)
}
})
}
return arr.join('或')
}
export const removeEle = (arr, elem, key = 'id') => {
let includesIndex
arr.map((item, index) => {
if (item[key] == elem[key]) {
includesIndex = index
}
})
arr.splice(includesIndex, 1)
}
export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
export const placeholderList = ['发起人', '审核人', '抄送人']
export const setTypes = [
{ value: 1, label: '指定成员' },
{ value: 2, label: '主管' },
{ value: 4, label: '发起人自选' },
{ value: 5, label: '发起人自己' },
{ value: 7, label: '连续多级主管' }
]
export const selectModes = [
{ value: 1, label: '选一个人' },
{ value: 2, label: '选多个人' }
]
export const selectRanges = [
{ value: 1, label: '全公司' },
{ value: 2, label: '指定成员' },
{ value: 3, label: '指定角色' }
]
export const optTypes = [
{ value: '1', label: '小于' },
{ value: '2', label: '大于' },
{ value: '3', label: '小于等于' },
{ value: '4', label: '等于' },
{ value: '5', label: '大于等于' },
{ value: '6', label: '介于两个数之间' }
]
<template>
<div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd">
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
<div class="handler-item-text">审批人</div>
</div>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
</div>
<div class="handler-item-text">条件分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-parallel"></span>
</div>
<div class="handler-item-text">并行分支</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
</el-popover>
</div>
</div>
</template>
<script setup lang="ts">
import {
ApproveMethodType,
AssignEmptyHandlerType,
AssignStartUserHandlerType,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
SimpleFlowNode
} from './consts'
import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
})
const popoverShow = ref(false)
const props = defineProps({
childNode: {
type: Object as () => SimpleFlowNode,
default: null
},
showAdd: {
// 是否显示添加节点
type: Boolean,
default: true
}
})
const emits = defineEmits(['update:childNode'])
const addNode = (type: number) => {
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID()
const data: SimpleFlowNode = {
id: id,
name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
showText: '',
type: NodeType.USER_TASK_NODE,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
// 超时处理
rejectHandler: {
type: RejectHandlerType.FINISH_PROCESS
},
timeoutHandler: {
enable: false
},
assignEmptyHandler: {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.COPY_TASK_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
showText: '',
type: NodeType.COPY_TASK_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.CONDITION_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '条件1',
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: 1,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '其它情况进入此流程',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
defaultFlow: true
}
]
}
emits('update:childNode', data)
}
if (type === NodeType.PARALLEL_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '并行分支',
type: NodeType.PARALLEL_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '并行1',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
},
{
id: 'Flow_' + generateUUID(),
name: '并行2',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
}
]
}
emits('update:childNode', data)
}
}
</script>
<style lang="scss" scoped></style>
<template>
<!-- 发起人节点 -->
<StartUserNode
v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
:flow-node="currentNode"
/>
<!-- 审批节点 -->
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 抄送节点 -->
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 并行节点 -->
<ParallelNode
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
v-model:flow-node="currentNode.childNode"
:parent-node="currentNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
<!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
</template>
<script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
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 { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({
name: 'ProcessNodeTree'
})
const props = defineProps({
parentNode: {
type: Object as () => SimpleFlowNode,
default: () => null
},
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = useWatchNode(props)
// 用于删除节点
const handleModelValueUpdate = (updateValue) => {
emits('update:flowNode', updateValue)
}
const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
findNode: SimpleFlowNode,
nodeType: number
) => {
if (!findNode) {
return
}
if (findNode.type === NodeType.START_USER_NODE) {
nodeList.push(findNode)
return
}
if (findNode.type === nodeType) {
nodeList.push(findNode)
}
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
</script>
<style lang="scss" scoped></style>
<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>
<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>
</div>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
import { getForm, FormVO } from '@/api/bpm/form'
import { handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
import { fa } from 'element-plus/es/locale'
defineOptions({
name: 'SimpleProcessDesigner'
})
const router = useRouter() // 路由
const props = defineProps({
modelId: {
type: String,
required: true
}
})
const loading = ref(true)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
provide('formFields', formFields)
provide('formType', formType)
provide('roleList', roleOptions)
provide('postList', postOptions)
provide('userList', userOptions)
provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
if (!props.modelId) {
message.error('缺少模型 modelId 编号')
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('修改失败')
}
}
// 校验节点设置。 暂时以 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) {
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 (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type == NodeType.CONDITION_BRANCH_NODE) {
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
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
// 获取表单字段
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
}
// 获得角色列表
roleOptions.value = await RoleApi.getSimpleRoleList()
// 获得岗位列表
postOptions.value = await PostApi.getSimplePostList()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
// 获得部门列表
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
} else {
// 初始值
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
}
} finally {
loading.value = false
}
})
</script>
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner }
\ No newline at end of file
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="权限" name="user">
<div> 待实现 </div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
defineOptions({
name: 'StartUserNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限配置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.WRITE
)
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'
currentNode.value.name = nodeName.value!
// TODO 暂时写死。后续可以显示谁有权限可以发起
currentNode.value.showText = '已设置'
// 设置表单权限
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
}
// 显示发起人节点配置, 由父组件传过来
const showStartUserNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
// 表单字段权限
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<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.COPY_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div 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" />
</div>
<CopyTaskNodeConfig v-if="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 CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
defineOptions({
name: 'CopyTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="end-node-wrapper">
<div class="end-node-box">
<span class="node-fixed-name" title="结束">结束</span>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'EndEventNode'
})
</script>
<style lang="scss" scoped></style>
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加条件</div>
<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 }">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width 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 class="branch-priority"> 优先级{{ index + 1 }} </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="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="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" />
</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" />
</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 { getDefaultConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ExclusiveNode'
})
const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
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 currentNode = ref<SimpleFlowNode>(props.flowNode)
// const conditionNodes = computed(() => currentNode.value.conditionNodes);
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 || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
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_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加分支</div>
<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">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width 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 class="branch-priority">无优先级</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">
<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="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" />
</div>
</div>
<!-- 递归显示子节点 -->
<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" />
</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 { generateUUID } from '@/utils'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ParallelNode'
})
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 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 || `并行${index + 1}`
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
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: []
}
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 recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span
></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<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" />
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2 } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
defineOptions({
name: 'StartEventNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
}>()
// 监控节点变化
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()
}
</script>
<style lang="scss" scoped></style>
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<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" />
</div>
<div 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" />
</div>
</div>
<UserTaskNodeConfig
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2 } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
defineOptions({
name: 'UserTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>()
// 监控节点变化
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 deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
// 查找可以驳回用户节点
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] // 匹配的节点
) => {
// 从父节点查找
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
}
</script>
<style lang="scss" scoped></style>
import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
if (defaultFlow) {
return '其它情况'
}
return '条件' + (index + 1)
}
export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE
}
if (strTimeUnit === 'H') {
return TimeUnitType.HOUR
}
if (strTimeUnit === 'D') {
return TimeUnitType.DAY
}
return TimeUnitType.HOUR
}
export const getApproveTypeText = (approveType: ApproveType): string => {
let approveTypeText = ''
APPROVE_TYPE.forEach((item) => {
if (item.value === approveType) {
approveTypeText = item.label
return
}
})
return approveTypeText
}
......@@ -11,3 +11,14 @@ export const setupAuth = (app: App<Element>) => {
hasRole(app)
hasPermi(app)
}
/**
* 导出指令:v-mountedFocus
*/
export const setupMountedFocus = (app: App<Element>) => {
app.directive('mountedFocus', {
mounted(el) {
el.focus()
}
})
}
......@@ -28,8 +28,8 @@ import '@/plugins/animate.css'
// 路由
import router, { setupRouter } from '@/router'
// 权限
import { setupAuth } from '@/directives'
// 指令
import { setupAuth, setupMountedFocus } from '@/directives'
import { createApp } from 'vue'
......@@ -58,7 +58,9 @@ const setupAll = async () => {
setupRouter(app)
// directives 指令
setupAuth(app)
setupMountedFocus(app)
await router.isReady()
......
......@@ -292,7 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
},
{
path: 'process-instance/detail',
component: () => import('@/views/bpm/processInstance/detail/index.vue'),
component: () => import('@/views/bpm/processInstance/detail/index_new.vue'),
name: 'BpmProcessInstanceDetail',
meta: {
noCache: true,
......@@ -300,7 +300,14 @@ const remainingRouter: AppRouteRecordRaw[] = [
canTo: true,
title: '流程详情',
activeMenu: '/bpm/task/my'
},
props: route => (
{
id: route.query.id,
taskId: route.query.taskId,
activityId: route.query.activityId
}
)
},
{
path: 'oa/leave/create',
......
import { store } from '../index'
import { store } from '../../index'
import { defineStore } from 'pinia'
export const useWorkFlowStore = defineStore('simpleWorkflow', {
......@@ -6,15 +6,15 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
tableId: '',
isTried: false,
promoterDrawer: false,
flowPermission1: {},
approverDrawer: false,
approverConfig1: {},
copyerDrawer: false,
copyerConfig1: {},
copyerConfig: {},
conditionDrawer: false,
conditionsConfig1: {
conditionNodes: []
}
},
userTaskConfig: {}
}),
actions: {
setTableId(payload) {
......@@ -26,26 +26,26 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
setPromoter(payload) {
this.promoterDrawer = payload
},
setFlowPermission(payload) {
this.flowPermission1 = payload
},
setApprover(payload) {
setApproverDrawer(payload) {
this.approverDrawer = payload
},
setApproverConfig(payload) {
this.approverConfig1 = payload
},
setCopyer(payload) {
setCopyerDrawer(payload) {
this.copyerDrawer = payload
},
setCopyerConfig(payload) {
this.copyerConfig1 = payload
this.copyerConfig = payload
},
setCondition(payload) {
this.conditionDrawer = payload
},
setConditionsConfig(payload) {
this.conditionsConfig1 = payload
},
setUserTaskConfig(payload) {
this.userTaskConfig = payload
}
}
})
......
......@@ -437,3 +437,15 @@ export const ErpBizType = {
SALE_OUT: 21,
SALE_RETURN: 22
}
// ========== BPM 模块 ==========
export const BpmModelType = {
BPMN: 10, // BPMN 设计器
SIMPLE: 20 // 简易设计器
}
export const BpmModelFormType = {
NORMAL: 10, // 流程表单
CUSTOM: 20 // 业务表单
}
......@@ -143,6 +143,7 @@ export enum DICT_TYPE {
INFRA_OPERATE_TYPE = 'infra_operate_type',
// ========== BPM 模块 ==========
BPM_MODEL_TYPE = 'bpm_model_type',
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
......
<template>
<Dialog v-model="dialogVisible" title="导入流程" width="400">
<div>
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl"
:auto-upload="false"
:data="formData"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".bpmn, .xml"
drag
name="bpmnFile"
>
<Icon class="el-icon--upload" icon="ep:upload-filled" />
<div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip" style="color: red">
提示:仅允许导入“bpm”或“xml”格式文件!
</div>
<div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<el-form-item label="流程标识" prop="key">
<el-input
v-model="formData.key"
placeholder="请输入流标标识"
style="width: 250px"
/>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
</el-form>
</div>
</template>
</el-upload>
</div>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { getAccessToken, getTenantId } from '@/utils/auth'
defineOptions({ name: 'ModelImportForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const formData = ref({
key: '',
name: '',
description: ''
})
const formRules = reactive({
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const uploadRef = ref() // 上传 Ref
const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref([]) // 文件列表
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
// 提交请求
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitFormSuccess = async (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
// 提示成功
message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('导入流程失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = () => {
// 重置上传状态和文件
formLoading.value = false
uploadRef.value?.clearFiles()
// 重置表单
formData.value = {
key: '',
name: '',
description: ''
}
formRef.value?.resetFields()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
</script>
......@@ -58,17 +58,17 @@ const initModeler = (item) => {
}
/** 添加/修改模型 */
const save = async (bpmnXml) => {
const save = async (bpmnXml: string) => {
const data = {
...model.value,
bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
} as unknown as ModelApi.ModelVO
// 提交
if (data.id) {
await ModelApi.updateModel(data)
await ModelApi.updateModelBpmn(data)
message.success('修改成功')
} else {
await ModelApi.createModel(data)
await ModelApi.updateModelBpmn(data)
message.success('新增成功')
}
// 跳转回去
......
<template>
<div
class="h-50px position-fixed bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
v-if="runningTask.id"
>
<!-- 【通过】按钮 -->
<el-popover
:visible="passVisible"
placement="top-end"
:width="500"
trigger="click"
v-if="isShowButton(OperationButtonType.APPROVE)"
>
<el-popover :visible="passVisible" placement="top-end" :width="500" trigger="click">
<template #reference>
<el-button plain type="success" @click="openPopover('1')">
<Icon icon="ep:select" />&nbsp; 通过
<Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button>
</template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
......@@ -50,19 +59,28 @@
<el-form-item>
<el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
通过
{{ getButtonDisplayName(OperationButtonType.APPROVE) }}
</el-button>
<el-button @click="passVisible = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<el-popover :visible="rejectVisible" placement="top-end" :width="500" trigger="click">
<!-- 【拒绝】按钮 -->
<el-popover
:visible="rejectVisible"
placement="top-end"
:width="500"
trigger="click"
v-if="isShowButton(OperationButtonType.REJECT)"
>
<template #reference>
<el-button class="mr-20px" plain type="danger" @click="openPopover('2')">
<Icon icon="ep:close" />&nbsp; 拒绝
<Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button>
</template>
<!-- 审批表单 -->
<div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
<el-form
label-position="top"
......@@ -105,21 +123,46 @@
<el-form-item>
<el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
拒绝
{{ getButtonDisplayName(OperationButtonType.REJECT) }}
</el-button>
<el-button @click="rejectVisible = false"> 取消 </el-button>
</el-form-item>
</el-form>
</div>
</el-popover>
<!-- 【抄送】按钮 -->
<div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" />&nbsp;抄送 </div>
<div @click="openTaskUpdateAssigneeForm">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;转交
<!-- 【转交】按钮 -->
<div @click="openTaskUpdateAssigneeForm" v-if="isShowButton(OperationButtonType.TRANSFER)">
<Icon :size="14" icon="fa:share-square-o" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
</div>
<div @click="handleDelegate"> <Icon :size="14" icon="ep:position" />&nbsp;委派 </div>
<div @click="handleSign"> <Icon :size="14" icon="ep:plus" />&nbsp;加签 </div>
<div @click="handleBack"> <Icon :size="14" icon="fa:mail-reply" />&nbsp;退回 </div>
<!-- 【委托】按钮 -->
<div @click="handleDelegate" v-if="isShowButton(OperationButtonType.DELEGATE)">
<Icon :size="14" icon="ep:position" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
</div>
<!-- 【加签】 -->
<div @click="handleSign" v-if="isShowButton(OperationButtonType.ADD_SIGN)">
<Icon :size="14" icon="ep:plus" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
</div>
<!-- TODO @jason:减签 -->
<!-- 【退回】按钮 -->
<div @click="handleBack" v-if="isShowButton(OperationButtonType.RETURN)">
<Icon :size="14" icon="fa:mail-reply" />&nbsp;
{{ getButtonDisplayName(OperationButtonType.RETURN) }}
</div>
<!--TODO @jason:撤回 -->
<!--TODO @jason:再次发起 -->
</div>
<!-- 弹窗:转派审批人 -->
<TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
<!-- 弹窗:回退节点 -->
......@@ -129,7 +172,6 @@
<!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
<TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
</template>
<script lang="ts" setup>
import { setConfAndFields2 } from '@/utils/formCreate'
import { useUserStore } from '@/store/modules/user'
......@@ -140,7 +182,10 @@ import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { isEmpty } from '@/utils/is'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'ProcessInstanceBtnConatiner' })
const userId = useUserStore().getUser.id // 当前登录的编号
......@@ -175,15 +220,17 @@ watch(
deep: true
}
)
// TODO @jaosn:具体的审批任务,要不改成后端返回。让前端弱化下
/**
* 设置 runningTasks 中的任务
*/
const loadRunningTask = (tasks) => {
const loadRunningTask = (tasks: any[]) => {
runningTask.value = {}
auditForm.value = {}
approveForm.value = {}
approveFormFApi.value = {}
tasks.forEach((task) => {
tasks.forEach((task: any) => {
if (!isEmpty(task.children)) {
loadRunningTask(task.children)
}
......@@ -214,7 +261,7 @@ const loadRunningTask = (tasks) => {
}
/** 处理审批通过和不通过的操作 */
const handleAudit = async (pass) => {
const handleAudit = async (pass: any) => {
formLoading.value = true
try {
const auditFormRef = proxy.$refs['formRef']
......@@ -254,6 +301,7 @@ const handleAudit = async (pass) => {
/* 抄送 TODO */
const handleSend = () => {}
// TODO 代码优化:这里 flag 改成 approve: boolean 。因为 flag 目前就只有 1 和 2
const openPopover = (flag) => {
passVisible.value = false
rejectVisible.value = false
......@@ -289,6 +337,24 @@ const getDetail = () => {
emit('success')
}
/** 是否显示按钮 */
const isShowButton = (btnType: OperationButtonType): boolean => {
let isShow = true
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
isShow = runningTask.value.buttonsSetting[btnType].enable
}
return isShow
}
/** 获取按钮的显示名称 */
const getButtonDisplayName = (btnType: OperationButtonType) => {
let displayName = OPERATION_BUTTON_NAME.get(btnType)
if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
displayName = runningTask.value.buttonsSetting[btnType].displayName
}
return displayName
}
defineExpose({ loadRunningTask })
</script>
......@@ -299,10 +365,11 @@ defineExpose({ loadRunningTask })
.btn-container {
> div {
display: flex;
margin: 0 15px;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
color: #6db5ff;
}
......
......@@ -56,29 +56,73 @@
</el-form-item>
</el-form>
<div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
<el-button type="success" @click="handleAudit(item, true)">
<!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
<el-button
type="success"
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
@click="handleAudit(item, true)"
>
<Icon icon="ep:select" />
通过
<!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
{{
item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
}}
</el-button>
<el-button type="danger" @click="handleAudit(item, false)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
type="danger"
@click="handleAudit(item, false)"
>
<Icon icon="ep:close" />
不通过
{{
item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
}}
</el-button>
<el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
type="primary"
@click="openTaskUpdateAssigneeForm(item.id)"
>
<Icon icon="ep:edit" />
转办
{{
item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
}}
</el-button>
<el-button type="primary" @click="handleDelegate(item)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
type="primary"
@click="handleDelegate(item)"
>
<Icon icon="ep:position" />
委派
{{
item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
}}
</el-button>
<el-button type="primary" @click="handleSign(item)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
type="primary"
@click="handleSign(item)"
>
<Icon icon="ep:plus" />
加签
{{
item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
}}
</el-button>
<el-button type="warning" @click="handleBack(item)">
<el-button
v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
type="warning"
@click="handleBack(item)"
>
<Icon icon="ep:back" />
回退
{{
item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
}}
</el-button>
</div>
</el-col>
......@@ -147,6 +191,10 @@ import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { registerComponent } from '@/utils/routerHelper'
import { isEmpty } from '@/utils/is'
import * as UserApi from '@/api/system/user'
import {
OperationButtonType,
OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
defineOptions({ name: 'BpmProcessInstanceDetail' })
......@@ -200,7 +248,11 @@ const handleAudit = async (task, pass) => {
// 1.2 校验表单
const elForm = unref(auditFormRef)
if (!elForm) return
const valid = await elForm.validate()
let valid = await elForm.validate()
if (!valid) return
// 校验申请表单
if (!fApi.value) return
valid = await fApi.value.validate()
if (!valid) return
// 2.1 提交审批
......@@ -216,6 +268,9 @@ const handleAudit = async (task, pass) => {
await formCreateApi.validate()
data.variables = approveForms.value[index].value
}
// 获取表单可编辑字段的值
data.variables = getWritableValueOfForm(task.fieldsPermission)
await TaskApi.approveTask(data)
message.success('审批通过成功')
} else {
......@@ -251,11 +306,11 @@ const handleSign = async (task: any) => {
}
/** 获得详情 */
const getDetail = () => {
// 1. 获得流程实例相关
const getDetail = async () => {
// 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置
await getTaskList()
// 2. 获得流程实例相关
getProcessInstance()
// 2. 获得流程任务列表(审批记录)
getTaskList()
}
/** 加载流程实例 */
......@@ -273,16 +328,29 @@ const getProcessInstance = async () => {
// 设置表单信息
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
if (detailForm.value.rule.length > 0) {
detailForm.value.value = data.formVariables
} else {
setConfAndFields2(
detailForm,
processDefinition.formConf,
processDefinition.formFields,
data.formVariables
)
}
nextTick().then(() => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
// 设置表单权限。后续需要改造成。只处理一个运行中的任务
if (runningTasks.value.length > 0) {
const task = runningTasks.value.at(0)
if (task.fieldsPermission) {
Object.keys(task.fieldsPermission).forEach((item) => {
setFieldPermission(item, task.fieldsPermission[item])
})
}
}
})
} else {
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
......@@ -353,6 +421,7 @@ const loadRunningTask = (tasks) => {
if (!task.assigneeUser || task.assigneeUser.id !== userId) {
return
}
// 2.3 添加到处理任务
runningTasks.value.push({ ...task })
auditForms.value.push({
......@@ -371,6 +440,35 @@ const loadRunningTask = (tasks) => {
})
}
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => {
if (permission === '1') {
fApi.value?.disabled(true, field)
}
if (permission === '2') {
fApi.value?.disabled(false, field)
}
if (permission === '3') {
fApi.value?.hidden(true, field)
}
}
/**
* 获取可以编辑字段的值
*/
const getWritableValueOfForm = (fieldsPermission: Object) => {
const fieldsValue = {}
if (fieldsPermission && fApi.value) {
Object.keys(fieldsPermission).forEach((item) => {
if (fieldsPermission[item] === '2') {
fieldsValue[item] = fApi.value.getValue(item)
}
})
}
return fieldsValue
}
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
......
<template>
<ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
<div class="processInstance-wrap-main">
<el-scrollbar>
<img
class="position-absolute right-20px"
width="150"
:src="auditIcons[processInstance.status]"
alt=""
/>
<div class="text-#878c93">编号:{{ id }}</div>
<div class="text-#878c93 h-15px">编号:{{ id }}</div>
<el-divider class="!my-8px" />
<div class="flex items-center gap-5 mb-10px">
<div class="flex items-center gap-5 mb-10px h-40px">
<div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
</div>
<div class="flex items-center gap-5 mb-10px text-13px">
<div class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600">
<div class="flex items-center gap-5 mb-10px text-13px h-35px">
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
>
<img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
{{ processInstance?.startUser?.nickname }}
</div>
<div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
</div>
<el-tabs>
<el-tabs v-model="activeTab">
<!-- 表单信息 -->
<el-tab-pane label="表单信息">
<el-tab-pane label="审批详情" name="form">
<div class="form-scroll-area">
<el-scrollbar>
<el-row :gutter="10">
<el-col :span="18" class="!flex !flex-col formCol">
<!-- 表单信息 -->
<div v-loading="processInstanceLoading" class="form-box flex flex-col mb-30px flex-1">
<div
v-loading="processInstanceLoading"
class="form-box flex flex-col mb-30px flex-1"
>
<!-- 情况一:流程表单 -->
<el-col
v-if="processInstance?.processDefinition?.formType === 10"
......@@ -46,23 +55,17 @@
<BusinessFormComponent :id="processInstance.businessKey" />
</div>
</div>
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="getDetail"
/>
</el-col>
<el-col :span="6">
<!-- 审批记录时间线 -->
<ProcessInstanceTimeline :process-instance="processInstance" :tasks="tasks" />
<ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
</el-col>
</el-row>
</el-scrollbar>
</div>
</el-tab-pane>
<!-- 流程图 -->
<el-tab-pane label="流程图">
<el-tab-pane label="流程图" name="diagram">
<ProcessInstanceBpmnViewer
:id="`${id}`"
:bpmn-xml="bpmnXml"
......@@ -72,7 +75,7 @@
/>
</el-tab-pane>
<!-- 流转记录 -->
<el-tab-pane label="流转记录">
<el-tab-pane label="流转记录" name="record">
<ProcessInstanceTaskList
:loading="tasksLoad"
:process-instance="processInstance"
......@@ -80,9 +83,24 @@
@refresh="getTaskList"
/>
</el-tab-pane>
<!-- 流转评论 -->
<el-tab-pane label="流转评论"> 流转评论 </el-tab-pane>
<!-- 流转评论 TODO 待开发 -->
<el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
</el-tabs>
<div
class="b-t-solid border-t-1px border-[var(--el-border-color)]"
v-if="activeTab === 'form'"
>
<!-- 操作栏按钮 -->
<ProcessInstanceOperationButton
ref="operationButtonRef"
:processInstance="processInstance"
:userOptions="userOptions"
@success="refresh"
/>
</div>
</el-scrollbar>
</div>
</ContentWrap>
</template>
<script lang="ts" setup>
......@@ -99,18 +117,22 @@ import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import { registerComponent } from '@/utils/routerHelper'
import * as UserApi from '@/api/system/user'
import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import audit1 from '@/assets/svgs/bpm/audit1.svg'
import audit2 from '@/assets/svgs/bpm/audit2.svg'
import audit3 from '@/assets/svgs/bpm/audit3.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const { query } = useRoute() // 查询参数
const props = defineProps<{
id: string // 流程实例的编号
taskId?: string // 任务编号
activityId?: string //流程活动编号,用于抄送查看
}>()
const message = useMessage() // 消息弹窗
const id = query.id as unknown as string // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const operationButtonRef = ref()
const timelineRef = ref()
const bpmnXml = ref('') // BPMN XML
const tasksLoad = ref(true) // 任务的加载中
const tasks = ref<any[]>([]) // 任务列表
......@@ -141,7 +163,7 @@ const BusinessFormComponent = ref<any>(null) // 异步组件
const getProcessInstance = async () => {
try {
processInstanceLoading.value = true
const data = await ProcessInstanceApi.getProcessInstance(id)
const data = await ProcessInstanceApi.getProcessInstance(props.id)
if (!data) {
message.error('查询不到流程信息!')
return
......@@ -151,6 +173,15 @@ const getProcessInstance = async () => {
// 设置表单信息
const processDefinition = data.processDefinition
if (processDefinition.formType === 10) {
// 获取表单字段权限
let fieldsPermission = undefined
if (props.taskId || props.activityId) {
fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
processInstanceId: props.id,
taskId: props.taskId,
activityId: props.activityId
})
}
setConfAndFields2(
detailForm,
processDefinition.formConf,
......@@ -161,6 +192,11 @@ const getProcessInstance = async () => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
if (fieldsPermission) {
Object.keys(fieldsPermission).forEach((item) => {
setFieldPermission(item, fieldsPermission[item])
})
}
})
} else {
// 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
......@@ -174,15 +210,30 @@ const getProcessInstance = async () => {
}
}
/**
* 设置表单权限
*/
const setFieldPermission = (field: string, permission: string) => {
if (permission === FieldPermissionType.READ) {
fApi.value?.disabled(true, field)
}
if (permission === FieldPermissionType.WRITE) {
fApi.value?.disabled(false, field)
}
if (permission === FieldPermissionType.NONE) {
fApi.value?.hidden(true, field)
}
}
/** 加载任务列表 */
const getTaskList = async () => {
try {
// 获得未取消的任务
tasksLoad.value = true
const data = await TaskApi.getTaskListByProcessInstanceId(id)
const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
tasks.value = []
// 1.1 移除已取消的审批
data.forEach((task) => {
data.forEach((task: any) => {
if (task.status !== 4) {
tasks.value.push(task)
}
......@@ -209,6 +260,19 @@ const getTaskList = async () => {
}
}
/**
* 操作成功后刷新
*/
const refresh = () => {
// 重新获取详情
getDetail()
// 刷新审批详情 Timeline
timelineRef.value?.refresh()
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
......@@ -219,6 +283,33 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
$wrap-padding-height: 30px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
.processInstance-wrap-main {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
);
overflow: auto;
.form-scroll-area {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
$process-header-height - 40px
);
max-height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
$process-header-height - 40px
);
overflow: auto;
}
}
.form-box {
:deep(.el-card) {
border: none;
......
<template>
<div>
<section class="dingflow-design">
<div class="box-scale">
<nodeWrap v-model:nodeConfig="nodeConfig" />
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
</section>
</div>
<SimpleProcessDesigner :model-id="modelId" />
</template>
<script lang="ts" setup>
import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
defineOptions({ name: 'SimpleWorkflowDesignEditor' })
let nodeConfig = ref({
nodeName: '发起人',
type: 0,
id: 'root',
formPerms: {},
nodeUserList: [],
childNode: {}
<script setup lang="ts">
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({
name: 'SimpleWorkflowDesignEditor'
})
const { query } = useRoute() // 路由的查询
const modelId = query.modelId as string
</script>
<style>
@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
</style>
\ No newline at end of file
<style lang="scss" scoped></style>
......@@ -111,11 +111,16 @@ const getList = async () => {
/** 处理审批按钮 */
const handleAudit = (row: any) => {
const query = {
id: row.processInstanceId,
activityId: undefined
}
if (row.activityId) {
query.activityId = row.activityId
}
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstanceId
}
query: query
})
}
......
......@@ -158,7 +158,8 @@ const handleAudit = (row: any) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
id: row.processInstance.id,
taskId: row.id
}
})
}
......
......@@ -140,7 +140,8 @@ const handleAudit = (row: any) => {
push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstance.id
id: row.processInstance.id,
taskId: row.id
}
})
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment