Merge pull request #102 from GoldenZqqq/feature/bpm

工作流发起与审核页面具体细节优化
pull/586/MERGE
芋道源码 2024-11-19 12:41:32 +08:00 committed by GitHub
commit 3783582b33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 161 additions and 84 deletions

View File

@ -0,0 +1 @@
<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>

After

Width:  |  Height:  |  Size: 608 B

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -1,25 +1,27 @@
<template> <template>
<Dialog v-model="dialogVisible" title="人员选择" width="900"> <Dialog v-model="dialogVisible" title="人员选择" width="800">
<el-row> <el-row class="gap2" v-loading="formLoading">
<el-col :span="6"> <el-col :span="6">
<el-tree <ContentWrap class="h-1/1">
ref="treeRef" <el-tree
:data="deptList" ref="treeRef"
:expand-on-click-node="false" :data="deptList"
:props="defaultProps" :expand-on-click-node="false"
default-expand-all :props="defaultProps"
highlight-current default-expand-all
node-key="id" highlight-current
@node-click="handleNodeClick" node-key="id"
/> @node-click="handleNodeClick"
/>
</ContentWrap>
</el-col> </el-col>
<el-col :span="17" :offset="1"> <el-col :span="17">
<el-transfer <el-transfer
v-model="selectedUserIdList" v-model="selectedUserIdList"
:titles="['未选', '已选']" :titles="['未选', '已选']"
filterable filterable
filter-placeholder="搜索成员" filter-placeholder="搜索成员"
:data="userList" :data="transferUserList"
:props="{ label: 'nickname', key: 'id' }" :props="{ label: 'nickname', key: 'id' }"
/> />
</el-col> </el-col>
@ -47,62 +49,87 @@ const emit = defineEmits<{
}>() }>()
const { t } = useI18n() // const { t } = useI18n() //
const message = useMessage() // const message = useMessage() //
const deptList = ref<Tree[]>([]) // const deptList = ref<Tree[]>([]) //
const userList: any = ref([]) // const allUserList = ref<UserApi.UserVO[]>([]) //
const filteredUserList = ref<UserApi.UserVO[]>([]) //
const selectedUserIdList: any = ref([]) // const selectedUserIdList: any = ref([]) //
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const formLoading = ref(false) // const formLoading = ref(false) //
const activityId = ref() // TODO @goldenzqqq activityId 使 @submitForm="xxx()" const activityId = ref()
//
const transferUserList = computed(() => {
//
const selectedUsers = allUserList.value.filter((user: any) =>
selectedUserIdList.value.includes(user.id)
)
//
const filteredUnselectedUsers = filteredUserList.value.filter(
(user: any) => !selectedUserIdList.value.includes(user.id)
)
//
return [...selectedUsers, ...filteredUnselectedUsers]
})
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (id: number, selectedList?: any[]) => { const open = async (id: number, selectedList?: any[]) => {
activityId.value = id activityId.value = id
//
resetForm() resetForm()
//
deptList.value = handleTree(await DeptApi.getSimpleDeptList()) deptList.value = handleTree(await DeptApi.getSimpleDeptList())
await getUserList() //
// await getAllUserList()
selectedUserIdList.value = selectedList?.map((item: any) => item.id) //
filteredUserList.value = [...allUserList.value]
// selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
dialogVisible.value = true dialogVisible.value = true
} }
/** 获取所有用户列表 */
/** 获取用户列表 */ const getAllUserList = async () => {
const getUserList = async (deptId?: number) => {
try { try {
// @ts-ignore // @ts-ignore
// TODO @ simple List const data = await UserApi.getSimpleUserList()
const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId }) allUserList.value = data
userList.value = data.list
} finally { } finally {
} }
} }
/** 获取部门过滤后的用户列表 */
const getUserList = async (deptId?: number) => {
formLoading.value = true
try {
// @ts-ignore
// TODO @ simple List deptId
const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
//
filteredUserList.value = data.list
} finally {
formLoading.value = false
}
}
/** 提交选择 */ /** 提交选择 */
const submitForm = async () => { const submitForm = async () => {
//
formLoading.value = true
try { try {
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
dialogVisible.value = false dialogVisible.value = false
const emitUserList = userList.value.filter((user: any) => //
const emitUserList = allUserList.value.filter((user: any) =>
selectedUserIdList.value.includes(user.id) selectedUserIdList.value.includes(user.id)
) )
// //
emit('confirm', activityId.value, emitUserList) emit('confirm', activityId.value, emitUserList)
} finally { } finally {
formLoading.value = false
} }
} }
/** 重置表单 */ /** 重置表单 */
const resetForm = () => { const resetForm = () => {
deptList.value = [] deptList.value = []
userList.value = [] allUserList.value = []
filteredUserList.value = []
selectedUserIdList.value = [] selectedUserIdList.value = []
} }
@ -113,3 +140,20 @@ const handleNodeClick = (row: { [key: string]: any }) => {
defineExpose({ open }) // open defineExpose({ open }) // open
</script> </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

@ -41,7 +41,7 @@
:ref="`category-${categoryCode}`" :ref="`category-${categoryCode}`"
> >
<h3 class="text-18px font-bold mb-10px mt-5px"> <h3 class="text-18px font-bold mb-10px mt-5px">
{{ getCategoryName(categoryCode) }} {{ getCategoryName(categoryCode as any) }}
</h3> </h3>
<div class="grid grid-cols-3 gap3"> <div class="grid grid-cols-3 gap3">
<el-tooltip <el-tooltip
@ -175,7 +175,17 @@ const handleQuery = () => {
/** 流程定义的分组 */ /** 流程定义的分组 */
const processDefinitionGroup: any = computed(() => { const processDefinitionGroup: any = computed(() => {
if (!processDefinitionList.value?.length) return {} if (!processDefinitionList.value?.length) return {}
return groupBy(filteredProcessDefinitionList.value, 'category') const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
const orderedGroup = {}
// categoryList
categoryList.value.forEach((category: any) => {
if (grouped[category.code]) {
orderedGroup[category.code] = grouped[category.code]
}
})
return orderedGroup
}) })
/** 左侧分类切换 */ /** 左侧分类切换 */

View File

@ -1,6 +1,6 @@
<template> <template>
<el-card v-loading="loading" class="box-card"> <el-card v-loading="loading" class="box-card">
<MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="h-700px" /> <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
</el-card> </el-card>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -45,4 +45,9 @@ watch(
width: 100%; width: 100%;
margin-bottom: 20px; margin-bottom: 20px;
} }
:deep(.process-viewer) {
height: 100% !important;
min-height: 500px;
}
</style> </style>

View File

@ -4,6 +4,7 @@
:flow-node="simpleModel" :flow-node="simpleModel"
:tasks="tasks" :tasks="tasks"
:process-instance="processInstance" :process-instance="processInstance"
class="process-viewer"
/> />
</div> </div>
</template> </template>
@ -151,3 +152,10 @@ const setSimpleModelNodeTaskStatus = (
) )
} }
</script> </script>
<style lang="scss" scoped>
:deep(.process-viewer) {
height: 100% !important;
min-height: 500px;
}
</style>

View File

@ -16,9 +16,10 @@
<img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" /> <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
<div <div
v-if="showStatusIcon" v-if="showStatusIcon"
class="position-absolute top-17px left-17px bg-#fff rounded-full flex items-center p-2px" class="position-absolute top-17px left-17px rounded-full flex items-center p-2px"
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
> >
<el-icon :size="12" :color="getApprovalNodeColor(activity.status)"> <el-icon :size="12" color="#fff">
<component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" /> <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
</el-icon> </el-icon>
</div> </div>
@ -46,19 +47,22 @@
" "
> >
<!-- && activity.nodeType === NodeType.USER_TASK_NODE --> <!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
<el-button
class="!px-8px" <el-tooltip content="添加用户" placement="left">
@click="handleSelectUser(activity.id, customApproveUsers[activity.id])" <el-button
> class="!px-6px"
<Icon icon="fa:user-plus" /> @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
</el-button> >
<img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
</el-button>
</el-tooltip>
<div <div
v-for="(user, idx1) in customApproveUsers[activity.id]" v-for="(user, idx1) in customApproveUsers[activity.id]"
:key="idx1" :key="idx1"
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative" class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
> >
<el-avatar :size="28" v-if="user.avatar" :src="user.avatar" /> <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar :size="28" v-else> <el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }} {{ user.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
{{ user.nickname }} {{ user.nickname }}
@ -73,40 +77,39 @@
> >
<!-- 信息头像昵称 --> <!-- 信息头像昵称 -->
<div <div
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative" class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
> >
<template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname"> <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
<el-avatar <el-avatar
class="!m-5px"
:size="28" :size="28"
v-if="task.assigneeUser?.avatar" v-if="task.assigneeUser?.avatar"
:src="task.assigneeUser?.avatar" :src="task.assigneeUser?.avatar"
/> />
<el-avatar :size="28" v-else> <el-avatar class="!m-5px" :size="28" v-else>
{{ task.assigneeUser?.nickname.substring(0, 1) }} {{ task.assigneeUser?.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
{{ task.assigneeUser?.nickname }} {{ task.assigneeUser?.nickname }}
</template> </template>
<template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname"> <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
<el-avatar <el-avatar
class="!m-5px"
:size="28" :size="28"
v-if="task.ownerUser?.avatar" v-if="task.ownerUser?.avatar"
:src="task.ownerUser?.avatar" :src="task.ownerUser?.avatar"
/> />
<el-avatar :size="28" v-else> <el-avatar class="!m-5px" :size="28" v-else>
{{ task.ownerUser?.nickname.substring(0, 1) }} {{ task.ownerUser?.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
{{ task.ownerUser?.nickname }} {{ task.ownerUser?.nickname }}
</template> </template>
<!-- 信息任务 ICON --> <!-- 信息任务 ICON -->
<div <div
v-if="onlyStatusIconShow.includes(task.status)" v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px" class="position-absolute top-19px left-23px rounded-full flex items-center p-2px"
:style="{ backgroundColor: statusIconMap2[task.status]?.color }"
> >
<Icon <Icon :size="12" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
:size="12"
:icon="statusIconMap2[task.status]?.icon"
:color="statusIconMap2[task.status]?.color"
/>
</div> </div>
</div> </div>
</div> </div>
@ -126,23 +129,21 @@
<div <div
v-for="(user, idx1) in activity.candidateUsers" v-for="(user, idx1) in activity.candidateUsers"
:key="idx1" :key="idx1"
class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative" class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
> >
<el-avatar :size="28" v-if="user.avatar" :src="user.avatar" /> <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar :size="28" v-else> <el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }} {{ user.nickname.substring(0, 1) }}
</el-avatar> </el-avatar>
{{ user.nickname }} {{ user.nickname }}
<!-- 信息任务 ICON --> <!-- 信息任务 ICON -->
<div <div
class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px" v-if="showStatusIcon"
class="position-absolute top-19px left-23px rounded-full flex items-center p-2px"
:style="{ backgroundColor: statusIconMap2['-1']?.color }"
> >
<Icon <Icon :size="12" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
:size="12"
:icon="statusIconMap2['-1']?.icon"
:color="statusIconMap2['-1']?.color"
/>
</div> </div>
</div> </div>
</div> </div>
@ -184,7 +185,7 @@ const statusIconMap2 = {
// //
'-1': { color: '#909398', icon: 'ep-clock' }, '-1': { color: '#909398', icon: 'ep-clock' },
// //
'0': { color: '#e5e7ec', icon: 'ep:loading' }, '0': { color: '#00b32a', icon: 'ep:loading' },
// //
'1': { color: '#448ef7', icon: 'ep:loading' }, '1': { color: '#448ef7', icon: 'ep:loading' },
// //
@ -204,7 +205,7 @@ const statusIconMap2 = {
const statusIconMap = { const statusIconMap = {
// //
'-1': { color: '#909398', icon: Clock }, '-1': { color: '#909398', icon: Clock },
'0': { color: '#e5e7ec', icon: Clock }, '0': { color: '#00b32a', icon: Clock },
// //
'1': { color: '#448ef7', icon: Loading }, '1': { color: '#448ef7', icon: Loading },
// //
@ -223,9 +224,9 @@ const statusIconMap = {
const nodeTypeSvgMap = { const nodeTypeSvgMap = {
// //
[NodeType.END_EVENT_NODE]: { color: '#ffffff', svg: finishSvg }, [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
// //
[NodeType.START_USER_NODE]: { color: '#ffffff', svg: starterSvg }, [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
// //
[NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg }, [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
// //

View File

@ -5,7 +5,7 @@
<img <img
class="position-absolute right-20px" class="position-absolute right-20px"
width="150" width="150"
:src="auditIcons[processInstance.status]" :src="auditIconsMap[processInstance.status]"
alt="" alt=""
/> />
<div class="text-#878c93 h-15px">编号{{ id }}</div> <div class="text-#878c93 h-15px">编号{{ id }}</div>
@ -137,11 +137,11 @@ import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue' import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue' import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts' import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
// TODO icon import { TaskStatusEnum } from '@/api/bpm/task'
import audit1 from '@/assets/svgs/bpm/audit1.svg' import runningSvg from '@/assets/svgs/bpm/running.svg'
import audit2 from '@/assets/svgs/bpm/audit2.svg' import approveSvg from '@/assets/svgs/bpm/approve.svg'
import audit3 from '@/assets/svgs/bpm/audit3.svg' import rejectSvg from '@/assets/svgs/bpm/reject.svg'
import audit4 from '@/assets/svgs/bpm/audit4.svg' import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' }) defineOptions({ name: 'BpmProcessInstanceDetail' })
const props = defineProps<{ const props = defineProps<{
@ -155,11 +155,11 @@ const processInstance = ref<any>({}) // 流程实例
const processDefinition = ref<any>({}) // const processDefinition = ref<any>({}) //
const processModelView = ref<any>({}) // const processModelView = ref<any>({}) //
const operationButtonRef = ref() // ref const operationButtonRef = ref() // ref
const auditIcons = { const auditIconsMap = {
1: audit1, [TaskStatusEnum.RUNNING]: runningSvg,
2: audit2, [TaskStatusEnum.APPROVE]: approveSvg,
3: audit3, [TaskStatusEnum.REJECT]: rejectSvg,
4: audit4 [TaskStatusEnum.CANCEL]: cancelSvg
} }
// ========== ========== // ========== ==========
@ -242,7 +242,6 @@ const getApprovalDetail = async () => {
/** 获取流程模型视图*/ /** 获取流程模型视图*/
const getProcessModelView = async () => { const getProcessModelView = async () => {
if (BpmModelType.BPMN === processDefinition.value?.modelType) { if (BpmModelType.BPMN === processDefinition.value?.modelType) {
// BPMN // BPMN
processModelView.value = { processModelView.value = {
@ -320,6 +319,15 @@ $process-header-height: 194px;
$process-header-height - 40px $process-header-height - 40px
); );
overflow: auto; overflow: auto;
:deep(.box-card) {
height: 100%;
.el-card__body {
height: 100%;
padding: 0;
}
}
} }
} }