Merge pull request #99 from GoldenZqqq/feature/bpm

发起流程界面功能点优化与bug修复
pull/582/head
芋道源码 2024-11-07 22:24:16 +08:00 committed by GitHub
commit 32d0093488
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 342 additions and 158 deletions

View File

@ -0,0 +1,100 @@
<template>
<Dialog v-model="dialogVisible" title="人员选择" width="900">
<el-row>
<el-col :span="6">
<el-tree
ref="treeRef"
:data="deptList"
:expand-on-click-node="false"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</el-col>
<el-col :span="17" :offset="1">
<el-transfer
v-model="selectedUserIdList"
filterable
filter-placeholder="搜索成员"
:data="userList"
: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, 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 deptList = ref<Tree[]>([]) //
const userList: any = ref([]) //
const message = useMessage() //
const selectedUserIdList: any = ref([]) //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const activityId = ref() // id
/** 打开弹窗 */
const open = async (id, selectedList?) => {
activityId.value = id
resetForm()
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
await getUserList()
selectedUserIdList.value = selectedList?.map((item) => item.id)
//
dialogVisible.value = true
}
/* 获取用户列表 */
const getUserList = async (deptId?) => {
try {
// @ts-ignore
const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
userList.value = data.list
} finally {
}
}
const submitForm = async () => {
//
formLoading.value = true
try {
message.success(t('common.updateSuccess'))
dialogVisible.value = false
const emitUserList = userList.value.filter((user) => selectedUserIdList.value.includes(user.id))
//
emit('confirm', activityId.value, emitUserList)
} finally {
formLoading.value = false
}
}
const resetForm = () => {
deptList.value = []
userList.value = []
selectedUserIdList.value = []
}
/** 处理部门被点击 */
const handleNodeClick = async (row: { [key: string]: any }) => {
getUserList(row.id)
}
defineExpose({ open }) // open
</script>

View File

@ -19,40 +19,7 @@
v-model="detailForm.value"
:option="detailForm.option"
@submit="submitForm"
>
<template #type-startUserSelect>
<el-col :span="24">
<el-card class="mb-10px">
<template #header>指定审批人</template>
<el-form
:model="startUserSelectAssignees"
:rules="startUserSelectAssigneesFormRules"
ref="startUserSelectAssigneesFormRef"
>
<el-form-item
v-for="userTask in startUserSelectTasks"
:key="userTask.id"
:label="`任务【${userTask.name}】`"
:prop="userTask.id"
>
<el-select
v-model="startUserSelectAssignees[userTask.id]"
multiple
placeholder="请选择审批人"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.nickname"
:value="user.id"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
</el-col>
</template>
</form-create>
/>
</el-col>
<el-col :span="6" :offset="1">
@ -61,7 +28,9 @@
ref="timelineRef"
:activity-nodes="activityNodes"
:show-status-icon="false"
candidateField="candidateUserList"
:startUserSelectTasks="startUserSelectTasks"
:startUserSelectAssignees="startUserSelectAssignees"
@select-user-confirm="selectUserConfirm"
/>
</el-col>
</el-row>
@ -104,7 +73,6 @@ import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'ProcessDefinitionDetail' })
const props = defineProps<{
@ -121,11 +89,8 @@ const detailForm: any = ref({
}) //
const fApi = ref<ApiAttrs>()
//
const startUserSelectAssigneesFormRef = ref() // Ref
const startUserSelectTasks: any = ref([]) //
const startUserSelectAssignees = ref({}) //
const startUserSelectAssigneesFormRules = ref({}) // Rules
const userList = ref<any[]>([]) //
const bpmnXML: any = ref(null) // BPMN
/** 当前的Tab */
const activeTab = ref('form')
@ -138,7 +103,6 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
//
startUserSelectTasks.value = []
startUserSelectAssignees.value = {}
startUserSelectAssigneesFormRules.value = {}
//
if (row.formType == 10) {
@ -163,24 +127,12 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
if (processDefinitionDetail) {
bpmnXML.value = processDefinitionDetail.bpmnXml
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
//
if (startUserSelectTasks.value?.length > 0) {
detailForm.value.rule.push({
type: 'startUserSelect',
props: {
title: '指定审批人'
}
})
//
for (const userTask of startUserSelectTasks.value) {
//
startUserSelectAssignees.value[userTask.id] = []
startUserSelectAssigneesFormRules.value[userTask.id] = [
{ required: true, message: '请选择审批人', trigger: 'blur' }
]
}
//
userList.value = await UserApi.getSimpleUserList()
}
}
//
@ -209,14 +161,19 @@ const getApprovalDetail = async (row: any) => {
}
}
/** 提交按钮 */
const submitForm = async (formData: any) => {
debugger
const submitForm = async () => {
if (!fApi.value || !props.selectProcessDefinition) {
return
}
//
if (startUserSelectTasks.value?.length > 0) {
await startUserSelectAssigneesFormRef.value.validate()
for (const userTask of startUserSelectTasks.value) {
if (
Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
startUserSelectAssignees.value[userTask.id].length === 0
)
return message.warning(`请选择${userTask.name}的审批人`)
}
}
//
@ -224,7 +181,7 @@ const submitForm = async (formData: any) => {
try {
await ProcessInstanceApi.createProcessInstance({
processDefinitionId: props.selectProcessDefinition.id,
variables: formData || detailForm.value.value,
variables: detailForm.value.value,
startUserSelectAssignees: startUserSelectAssignees.value
})
//
@ -243,6 +200,10 @@ const handleCancel = () => {
emit('cancel')
}
const selectUserConfirm = (id, userList) => {
startUserSelectAssignees.value[id] = userList?.map((item) => item.id)
}
defineExpose({ initProcessInfo })
</script>
@ -250,7 +211,7 @@ defineExpose({ initProcessInfo })
$wrap-padding-height: 20px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 194px;
$process-header-height: 105px;
.processInstance-wrap-main {
height: calc(

View File

@ -1,46 +1,74 @@
<template>
<!-- 第一步通过流程定义的列表选择对应的流程 -->
<ContentWrap
class="process-definition-container position-relative pb-20px"
v-if="!selectProcessDefinition"
v-loading="loading"
>
<el-row :gutter="20" class="!flex-nowrap">
<el-col :span="5">
<div class="flex flex-col">
<div
v-for="category in categoryList"
:key="category.code"
class="flex items-center p-10px cursor-pointer text-14px rounded-md"
:class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
@click="handleCategoryClick(category)"
>
{{ category.name }}
<template v-if="!selectProcessDefinition">
<el-input
v-model="currentSearchKey"
class="!w-50% mb-15px"
placeholder="请输入流程名称"
clearable
@keyup.enter="handleQuery"
@clear="handleClear"
>
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<ContentWrap
:class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
class="position-relative pb-20px h-700px"
v-loading="loading"
>
<el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
<el-col :span="5">
<div class="flex flex-col">
<div
v-for="category in categoryList"
:key="category.code"
class="flex items-center p-10px cursor-pointer text-14px rounded-md"
:class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
@click="handleCategoryClick(category)"
>
{{ category.name }}
</div>
</div>
</div>
</el-col>
<el-col :span="19">
<h3 class="text-16px font-bold mb-10px mt-5px">{{ categoryActive.name }}</h3>
<div class="grid grid-cols-3 gap3" v-if="categoryProcessDefinitionList.length">
<el-card
v-for="definition in categoryProcessDefinitionList"
:key="definition.id"
shadow="hover"
class="cursor-pointer definition-item-card"
@click="handleSelect(definition)"
>
<template #default>
<div class="flex">
<el-image :src="definition.icon" class="w-32px h-32px" />
<el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
</el-col>
<el-col :span="19">
<el-scrollbar ref="scrollWrapper" height="700">
<div
class="mb-20px pl-10px"
v-for="(definitions, title) in processDefinitionGroup"
:key="title"
:ref="`category-${title}`"
>
<h3 class="text-18px font-bold mb-10px mt-5px">{{ title }}</h3>
<div class="grid grid-cols-3 gap3">
<el-tooltip
v-for="definition in definitions"
:key="definition.id"
:content="definition.description"
placement="top"
>
<el-card
shadow="hover"
class="cursor-pointer definition-item-card"
@click="handleSelect(definition)"
>
<template #default>
<div class="flex">
<el-image :src="definition.icon" class="w-32px h-32px" />
<el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
</div>
</template>
</el-card>
</el-tooltip>
</div>
</template>
</el-card>
</div>
<el-empty v-else />
</el-col>
</el-row>
</ContentWrap>
</div>
</el-scrollbar>
</el-col>
</el-row>
<el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
</ContentWrap>
</template>
<!-- 第二步填写表单进行流程的提交 -->
<ProcessDefinitionDetail
@ -56,12 +84,14 @@ import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { CategoryApi } from '@/api/bpm/category'
import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
import { groupBy } from 'lodash-es'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const { proxy } = getCurrentInstance() as any
const route = useRoute() //
const message = useMessage() //
const currentSearchKey = ref('') //
const processInstanceId: any = route.query.processInstanceId
const loading = ref(true) //
const categoryList: any = ref([]) //
@ -71,15 +101,10 @@ const processDefinitionList = ref([]) // 流程定义的列表
const getList = async () => {
loading.value = true
try {
//
categoryList.value = await CategoryApi.getCategorySimpleList()
if (categoryList.value.length > 0) {
categoryActive.value = categoryList.value[0]
}
//
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
//
await getCategoryList()
//
await getDefinitionList()
// processInstanceId
if (processInstanceId?.length > 0) {
@ -102,11 +127,55 @@ const getList = async () => {
}
}
/** 选中分类对应的流程定义列表 */
const categoryProcessDefinitionList: any = computed(() => {
return processDefinitionList.value.filter(
(item: any) => item.category == categoryActive.value.code
)
//
const getCategoryList = async () => {
try {
//
categoryList.value = await CategoryApi.getCategorySimpleList()
if (categoryList.value.length > 0) {
categoryActive.value = categoryList.value[0]
}
} finally {
}
}
//
const getDefinitionList = async () => {
try {
//
processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
suspensionState: 1
})
//
filteredProcessDefinitionList.value = processDefinitionList.value
} finally {
}
}
const filteredProcessDefinitionList = ref([]) //
//
const handleQuery = () => {
if (currentSearchKey.value.trim()) {
//
filteredProcessDefinitionList.value = processDefinitionList.value.filter(
(definition: any) =>
definition.name.toLowerCase().includes(currentSearchKey.value.toLowerCase()) //
)
} else {
//
filteredProcessDefinitionList.value = processDefinitionList.value
}
}
// input `clearable`
const handleClear = () => {
filteredProcessDefinitionList.value = processDefinitionList.value
}
//
const processDefinitionGroup: any = computed(() => {
if (!processDefinitionList.value?.length) return {}
return groupBy(filteredProcessDefinitionList.value, 'categoryName')
})
// ========== ==========
@ -122,8 +191,16 @@ const handleSelect = async (row, formVariables?) => {
processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
}
//
const handleCategoryClick = (val: number) => {
categoryActive.value = val
const handleCategoryClick = (category) => {
categoryActive.value = category
const categoryRef = proxy.$refs[`category-${category.name}`] // DOM
if (categoryRef?.length) {
const scrollWrapper = proxy.$refs.scrollWrapper //
const categoryOffsetTop = categoryRef[0].offsetTop
//
scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
}
}
/** 初始化 */

View File

@ -24,7 +24,7 @@
</div>
</div>
</template>
<div class="flex flex-col items-start">
<div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
<!-- 第一行节点名称时间 -->
<div class="flex w-full">
<div class="font-bold"> {{ activity.name }}</div>
@ -36,53 +36,79 @@
{{ getApprovalNodeTime(activity) }}
</div>
</div>
<div class="flex items-center flex-wrap mt-1">
<!-- 需要自定义选择审批人 -->
<div
class="flex flex-wrap gap2 items-center"
v-if="
startUserSelectTasks?.length > 0 && Array.isArray(startUserSelectAssignees[activity.id])
"
>
<!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
<el-button
class="!px-8px"
@click="handleSelectUser(activity.id, customApprover[activity.id])"
>
<Icon icon="fa:user-plus" />
</el-button>
<div
v-for="(user, idx1) in customApprover[activity.id]"
:key="idx1"
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
>
<el-avatar :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar :size="28" v-else>
{{ user.nickname.substring(0, 1) }}
</el-avatar>
{{ user.nickname }}
</div>
</div>
<div v-else class="flex items-center flex-wrap mt-1 gap2">
<!-- 情况一遍历每个审批节点下的进行中task 任务 -->
<div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
<div class="flex flex-col pr-2 gap2">
<div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
<div
class="position-relative flex flex-wrap gap2"
v-if="task.assigneeUser || task.ownerUser"
>
<!-- 信息头像昵称 -->
<div
class="position-relative flex flex-wrap gap2"
v-if="task.assigneeUser || task.ownerUser"
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
>
<!-- 信息头像昵称 -->
<template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
<el-avatar
:size="28"
v-if="task.assigneeUser?.avatar"
:src="task.assigneeUser?.avatar"
/>
<el-avatar :size="28" v-else>
{{ task.assigneeUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ task.assigneeUser?.nickname }}
</template>
<template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
<el-avatar
:size="28"
v-if="task.ownerUser?.avatar"
:src="task.ownerUser?.avatar"
/>
<el-avatar :size="28" v-else>
{{ task.ownerUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ task.ownerUser?.nickname }}
</template>
<!-- 信息任务 ICON -->
<div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
v-if="onlyStatusIconShow.includes(task.status)"
class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
<el-avatar
:size="28"
v-if="task.assigneeUser?.avatar"
:src="task.assigneeUser?.avatar"
/>
<el-avatar :size="28" v-else>
{{ task.assigneeUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ task.assigneeUser?.nickname }}
</template>
<template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
<el-avatar
:size="28"
v-if="task.ownerUser?.avatar"
:src="task.ownerUser?.avatar"
/>
<el-avatar :size="28" v-else>
{{ task.ownerUser?.nickname.substring(0, 1) }}
</el-avatar>
{{ task.ownerUser?.nickname }}
</template>
<!-- 信息任务 ICON -->
<div
v-if="onlyStatusIconShow.includes(task.status)"
class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px"
>
<Icon
:size="12"
:icon="statusIconMap2[task.status]?.icon"
:color="statusIconMap2[task.status]?.color"
/>
</div>
<Icon
:size="12"
:icon="statusIconMap2[task.status]?.icon"
:color="statusIconMap2[task.status]?.color"
/>
</div>
</div>
</div>
<teleport defer :to="`#activity-task-${activity.id}`">
<div
v-if="
task.reason &&
@ -92,7 +118,7 @@
>
审批意见{{ task.reason }}
</div>
</div>
</teleport>
</div>
<!-- 情况二遍历每个审批节点下的候选的task 任务例如说1依次审批2未来的审批任务等 -->
<div
@ -121,6 +147,8 @@
</div>
</el-timeline-item>
</el-timeline>
<!-- 用户选择弹窗 -->
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template>
<script lang="ts" setup>
@ -141,9 +169,13 @@ withDefaults(
defineProps<{
activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] //
showStatusIcon?: boolean //
startUserSelectTasks?: any[] //
startUserSelectAssignees?: any //
}>(),
{
showStatusIcon: true // true
showStatusIcon: true, // true
startUserSelectTasks: () => [], //
startUserSelectAssignees: () => {}
}
)
@ -241,4 +273,19 @@ const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
return `${formatDate(node.startTime)}`
}
}
//
const userSelectFormRef = ref()
const handleSelectUser = (activityId, selectedList) => {
userSelectFormRef.value.open(activityId, selectedList)
}
const emit = defineEmits<{
selectUserConfirm: [id: any, userList: any[]]
}>()
const customApprover: any = ref({})
//
const handleUserSelectConfirm = (activityId, userList) => {
customApprover.value[activityId] = userList || []
emit('selectUserConfirm', activityId, userList)
}
</script>

View File

@ -222,7 +222,6 @@ const handleCreate = async (row?: ProcessInstanceVO) => {
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
row.processDefinitionId
)
debugger
if (processDefinitionDetail.formType === 20) {
message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
return