【功能完善】仿钉钉流程模型浏览,增加弹窗显示用户任务信息

pull/582/head
jason 2024-10-29 23:54:51 +08:00
parent 7044bcee60
commit 8465f8fada
6 changed files with 287 additions and 606 deletions

View File

@ -1,5 +1,5 @@
<template>
<SimpleProcessModel :flow-node="simpleModel" :readonly="true"/>
<SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
</template>
<script setup lang="ts">
@ -16,10 +16,33 @@ const props = defineProps({
required: true
},
//
tasks : {
type : Array,
tasks: {
type: Array,
default: () => [] as any[]
},
//
processInstance: {
type: Object,
default: () => undefined
}
})
const approveTasks = ref<any[]>(props.tasks)
const currentProcessInstance = ref(props.processInstance)
const simpleModel = useWatchNode(props)
watch(
() => props.tasks,
(newValue) => {
approveTasks.value = newValue
}
)
watch(
() => props.processInstance,
(newValue) => {
currentProcessInstance.value = newValue
}
)
provide('tasks', approveTasks)
provide('processInstance', currentProcessInstance)
</script>
p

View File

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

View File

@ -22,7 +22,7 @@
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
@ -37,12 +37,78 @@
</div>
</div>
<StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
<!-- 审批记录 -->
<el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px" append-to-body>
<el-row>
<el-table
:data="selectTasks"
size="small"
border
header-cell-class-name="table-header-gray"
>
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column
label="审批人"
min-width="100"
align="center"
>
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
align="center"
label="审批建议"
prop="reason"
min-width="120"
/>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
name: 'StartEventNode'
})
@ -53,6 +119,7 @@ const props = defineProps({
}
})
const readonly = inject<Boolean>('readonly') //
const tasks = inject<Ref<any[]>>('tasks')
//
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
@ -63,15 +130,27 @@ const currentNode = useWatchNode(props)
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
//
const nodeClick = () => {
if (readonly) {
return
//
if(tasks && tasks.value){
dialogTitle.value = currentNode.value.name
selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === currentNode.value.id)
dialogVisible.value = true
}
} else {
//
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
const dialogVisible = ref(false) //
const dialogTitle = ref<string | undefined>(undefined) //
const selectTasks = ref<any[]|undefined>([]) //
</script>
<style lang="scss" scoped></style>

View File

@ -20,7 +20,7 @@
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
@ -45,12 +45,78 @@
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
<!-- 审批记录 -->
<el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px" append-to-body>
<el-row>
<el-table
:data="selectTasks"
size="small"
border
header-cell-class-name="table-header-gray"
>
<el-table-column
label="序号"
header-align="center"
align="center"
type="index"
width="50"
/>
<el-table-column
label="审批人"
min-width="100"
align="center"
>
<template #default="scope">
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
</template>
</el-table-column>
<el-table-column label="部门" min-width="100" align="center">
<template #default="scope">
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="开始时间"
prop="createTime"
min-width="140"
/>
<el-table-column
:formatter="dateFormatter"
align="center"
label="结束时间"
prop="endTime"
min-width="140"
/>
<el-table-column align="center" label="审批状态" prop="status" min-width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
align="center"
label="审批建议"
prop="reason"
min-width="120"
/>
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
<template #default="scope">
{{ formatPast2(scope.row.durationInMillis) }}
</template>
</el-table-column>
</el-table>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
name: 'UserTaskNode'
})
@ -67,26 +133,32 @@ const emits = defineEmits<{
//
const readonly = inject<Boolean>('readonly')
const tasks = inject<Ref<any[]>>('tasks')
//
const currentNode = useWatchNode(props)
//
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
//
const openNodeConfig = () => {
if (readonly) {
return
const nodeClick = () => {
if (readonly) {
if(tasks && tasks.value){
dialogTitle.value = currentNode.value.name
//
selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === currentNode.value.id)
dialogVisible.value = true
}
} else {
//
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
//
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
//
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] //
@ -94,5 +166,11 @@ const findReturnTaskNodes = (
//
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
}
//
const dialogVisible = ref(false) //
const dialogTitle = ref<string | undefined>(undefined) //
const selectTasks = ref<any[]|undefined>([]) //
</script>
<style lang="scss" scoped></style>

View File

@ -1,573 +1,3 @@
// .simple-flow-canvas {
// z-index: 1;
// overflow: auto;
// background-color: #fafafa;
// // user-select: none;
// .simple-flow-container {
// position: relative;
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// .top-area-container {
// position: sticky;
// inset: 0;
// display: flex;
// width: 100%;
// height: 42px;
// z-index: 1;
// // padding: 4px 0;
// background-color: #fff;
// justify-content: flex-end;
// align-items: center;
// .top-actions {
// display: flex;
// margin: 4px;
// margin-right: 8px;
// align-items: center;
// .canvas-control {
// font-size: 16px;
// .control-scale-group {
// display: inline-flex;
// align-items: center;
// margin-right: 8px;
// .control-scale-button {
// display: inline-flex;
// width: 28px;
// height: 28px;
// padding: 2px;
// text-align: center;
// cursor: pointer;
// justify-content: center;
// align-items: center;
// }
// .control-scale-label {
// margin: 0 4px;
// font-size: 14px;
// }
// }
// }
// }
// }
// .scale-container {
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// margin-top: 16px;
// background-color: #fafafa;
// transform-origin: 50% 0 0;
// transform: scale(1);
// transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
// //
// .node-container {
// width: 200px;
// }
// //
// .node-box {
// position: relative;
// display: flex;
// min-height: 70px;
// padding: 5px 10px 8px;
// cursor: pointer;
// background-color: #fff;
// flex-direction: column;
// border: 2px solid transparent;
// border-radius: 8px;
// box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
// transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
// &.status-pass {
// border-color: #67c23a;
// background-color: #a9da90;
// }
// &.status-pass:hover {
// border-color: #67c23a;
// }
// &.status-running {
// border-color: #5a9cf8;
// background-color: #e7f0fe;
// }
// &.status-running:hover {
// border-color: #5a9cf8;
// }
// &.status-reject {
// border-color: #e47470;
// background-color: #f6e5e5;
// }
// &.status-reject:hover {
// border-color: #e47470;
// }
// &:hover {
// border-color: #0089ff;
// .node-toolbar {
// opacity: 1;
// }
// .branch-node-move {
// display: flex;
// }
// }
// //
// .node-title-container {
// display: flex;
// padding: 4px;
// cursor: pointer;
// border-radius: 4px 4px 0 0;
// align-items: center;
// .node-title-icon {
// display: flex;
// align-items: center;
// &.user-task {
// color: #ff943e;
// }
// &.copy-task {
// color: #3296fa;
// }
// &.start-user {
// color: #676565;
// }
// }
// .node-title {
// margin-left: 4px;
// font-size: 14px;
// font-weight: 600;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// color: #1f1f1f;
// line-height: 18px;
// &:hover {
// border-bottom: 1px dashed #f60;
// }
// }
// }
// //
// .branch-node-title-container {
// display: flex;
// padding: 4px 0;
// cursor: pointer;
// border-radius: 4px 4px 0 0;
// align-items: center;
// justify-content: space-between;
// .input-max-width {
// max-width: 115px !important;
// }
// .branch-title {
// font-size: 13px;
// font-weight: 600;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// color: #f60;
// &:hover {
// border-bottom: 1px dashed #000;
// }
// }
// .branch-priority {
// min-width: 50px;
// font-size: 12px;
// }
// }
// .node-content {
// display: flex;
// min-height: 32px;
// padding: 4px 8px;
// margin-top: 4px;
// line-height: 32px;
// justify-content: space-between;
// align-items: center;
// color: #111f2c;
// background: rgba(0, 0, 0, 0.03);
// border-radius: 4px;
// .node-text {
// display: -webkit-box;
// overflow: hidden;
// font-size: 14px;
// line-height: 24px;
// text-overflow: ellipsis;
// word-break: break-all;
// -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
// -webkit-box-orient: vertical;
// }
// }
// //
// .branch-node-content {
// display: flex;
// min-height: 32px;
// padding: 4px 0;
// margin-top: 4px;
// line-height: 32px;
// align-items: center;
// color: #111f2c;
// border-radius: 4px;
// .branch-node-text {
// overflow: hidden;
// font-size: 12px;
// line-height: 24px;
// text-overflow: ellipsis;
// word-break: break-all;
// -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
// -webkit-box-orient: vertical;
// }
// }
// //
// .node-toolbar {
// opacity: 0;
// position: absolute;
// top: -20px;
// right: 0px;
// display: flex;
// .toolbar-icon {
// text-align: center;
// vertical-align: middle;
// }
// }
// //
// .branch-node-move {
// position: absolute;
// width: 10px;
// cursor: pointer;
// display: none;
// align-items: center;
// height: 100%;
// justify-content: center;
// }
// .move-node-left {
// left: -2px;
// top: 0px;
// background: rgba(126, 134, 142, 0.08);
// border-top-left-radius: 8px;
// border-bottom-left-radius: 8px;
// }
// .move-node-right {
// right: -2px;
// top: 0px;
// background: rgba(126, 134, 142, 0.08);
// border-top-right-radius: 6px;
// border-bottom-right-radius: 6px;
// }
// }
// .node-config-error {
// border-color: #ff5219 !important;
// }
// //
// .node-wrapper {
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// }
// // 线
// .node-handler-wrapper {
// position: relative;
// display: flex;
// height: 70px;
// align-items: center;
// user-select: none;
// justify-content: center;
// flex-direction: column;
// &::before {
// position: absolute;
// top: 0;
// z-index: 0;
// width: 2px;
// height: 100%;
// margin: auto;
// background-color: #dedede;
// content: '';
// }
// .node-handler {
// .add-icon {
// position: relative;
// top: -5px;
// display: flex;
// align-items: center;
// justify-content: center;
// cursor: pointer;
// width: 25px;
// height: 25px;
// color: #fff;
// background-color: #0089ff;
// border-radius: 50%;
// &:hover {
// transform: scale(1.1);
// }
// }
// }
// .node-handler-arrow {
// position: absolute;
// bottom: 0;
// left: 50%;
// display: flex;
// transform: translateX(-50%);
// }
// }
// //
// .branch-node-wrapper {
// position: relative;
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// margin-top: 16px;
// .branch-node-container {
// position: relative;
// display: flex;
// &::before {
// position: absolute;
// height: 100%;
// width: 4px;
// background-color: #fafafa;
// content: '';
// left: 50%;
// transform: translate(-50%);
// }
// .branch-node-add {
// position: absolute;
// top: -18px;
// left: 50%;
// z-index: 1;
// height: 36px;
// padding: 0 10px;
// font-size: 12px;
// line-height: 36px;
// border: 2px solid #dedede;
// border-radius: 18px;
// transform: translateX(-50%);
// transform-origin: center center;
// }
// .branch-node-readonly {
// position: absolute;
// top: -18px;
// left: 50%;
// z-index: 1;
// width: 36px;
// height: 36px;
// display: flex;
// align-items: center;
// justify-content: center;
// border: 2px solid #dedede;
// background-color: #fff;
// border-radius: 50%;
// transform: translateX(-50%);
// transform-origin: center center;
// &.status-pass {
// border-color: #6bb63c;
// background-color: #e9f4e2;
// }
// &.status-pass:hover {
// border-color: #6bb63c;
// }
// .icon-size {
// font-size: 22px;
// color: #67c23a;
// }
// }
// .branch-node-item {
// position: relative;
// display: flex;
// flex-direction: column;
// align-items: center;
// min-width: 280px;
// padding: 40px 40px 0;
// background: transparent;
// border-top: 2px solid #dedede;
// border-bottom: 2px solid #dedede;
// &::before {
// position: absolute;
// width: 2px;
// height: 100%;
// margin: auto;
// inset: 0;
// background-color: #dedede;
// content: '';
// }
// }
// // 线
// .branch-line-first-top {
// position: absolute;
// top: -5px;
// left: -1px;
// width: 50%;
// height: 7px;
// background-color: #fafafa;
// content: '';
// }
// // 线
// .branch-line-first-bottom {
// position: absolute;
// bottom: -5px;
// left: -1px;
// width: 50%;
// height: 7px;
// background-color: #fafafa;
// content: '';
// }
// // 线
// .branch-line-last-top {
// position: absolute;
// top: -5px;
// right: -1px;
// width: 50%;
// height: 7px;
// background-color: #fafafa;
// content: '';
// }
// // 线
// .branch-line-last-bottom {
// position: absolute;
// right: -1px;
// bottom: -5px;
// width: 50%;
// height: 7px;
// background-color: #fafafa;
// content: '';
// }
// }
// }
// .node-fixed-name {
// display: inline-block;
// width: auto;
// padding: 0 4px;
// overflow: hidden;
// text-align: center;
// text-overflow: ellipsis;
// white-space: nowrap;
// }
// //
// .start-node-wrapper {
// position: relative;
// margin-top: 16px;
// .start-node-container {
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// .start-node-box {
// display: flex;
// justify-content: center;
// align-items: center;
// width: 90px;
// height: 36px;
// padding: 3px 4px;
// color: #212121;
// cursor: pointer;
// background: #fafafa;
// border-radius: 30px;
// box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
// box-sizing: border-box;
// }
// }
// }
// //
// .end-node-wrapper {
// margin-bottom: 16px;
// .end-node-box {
// display: flex;
// justify-content: center;
// align-items: center;
// width: 80px;
// height: 36px;
// border: 2px solid #fafafa;
// color: #212121;
// border-radius: 30px;
// box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
// box-sizing: border-box;
// &.status-pass {
// border-color: #6bb63c;
// background-color: #a9da90;
// }
// &.status-pass:hover {
// border-color: #6bb63c;
// }
// &.status-reject {
// border-color: #e47470;
// background-color: #f6e5e5;
// }
// &.status-reject:hover {
// border-color: #e47470;
// }
// &.status-cancel {
// border-color: #919398;
// background-color: #eaeaeb;
// }
// &.status-cancel:hover {
// border-color: #919398;
// }
// }
// }
// // title
// .editable-title-input {
// height: 20px;
// max-width: 145px;
// line-height: 20px;
// font-size: 12px;
// margin-left: 4px;
// border: 1px solid #d9d9d9;
// border-radius: 4px;
// transition: all 0.3s;
// &:focus {
// border-color: #40a9ff;
// outline: 0;
// box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
// }
// }
// }
// }
// }
//
.config-header {
@ -739,7 +169,7 @@
font-size: 13px;
}
}
// Simple
.simple-process-model-container {
height: 100%;
overflow: auto;

View File

@ -1,6 +1,6 @@
<template>
<div v-loading="loading" class="mb-20px">
<SimpleProcessViewer :flow-node="simpleModel" :tasks="tasks"/>
<SimpleProcessViewer :flow-node="simpleModel" :tasks="tasks" :process-instance="processInstance"/>
</div>
</template>
<script lang="ts" setup>
@ -16,10 +16,11 @@ const props = defineProps({
loading: propTypes.bool.def(false), //
id: propTypes.string //
})
const tasks = ref([])
const simpleModel = ref()
//
const tasks = ref([])
//
const processInstance = ref()
/** 只有 loading 完成时,才去加载流程列表 */
watch(
() => props.loading,
@ -27,7 +28,8 @@ watch(
if (value && props.id) {
const modelView = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
if (modelView) {
tasks.value = modelView.tasks;
tasks.value = modelView.tasks
processInstance.value = modelView.processInstance
// UserTask
const rejectedTaskActivityIds: string[] = modelView.rejectedTaskActivityIds
// UserTask
@ -44,7 +46,6 @@ watch(
finishedActivityIds,
finishedSequenceFlowActivityIds
)
console.log('modelView.simpleModel==>', modelView.simpleModel)
simpleModel.value = modelView.simpleModel
}
}
@ -140,11 +141,4 @@ const setSimpleModelNodeTaskStatus = (
finishedSequenceFlowActivityIds
)
}
/** 监听 bpmnXml */
// watch(
// () => props.bpmnXml,
// (value) => {
// view.value.bpmnXml = value
// }
// )
</script>