更多ruoyi-nbcio功能请看演示系统
gitee源代码地址
前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio
演示地址:RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/
更多nbcio-boot功能请看演示系统
gitee源代码地址
后端代码: https://gitee.com/nbacheng/nbcio-boot
前端代码:https://gitee.com/nbacheng/nbcio-vue.git
在线演示(包括H5) : http://122.227.135.243:9888
1、因为修改vue3后也要支持自定义表单额显示处理,所以修改如下:
<template>
<div class="app-container">
<el-tabs tab-position="top" :model-value="processed === true ? 'approval' : 'form'">
<el-tab-pane label="任务办理" name="approval" v-if="processed === true">
<el-card class="box-card" shadow="hover" v-if="taskFormOpen">
<template #header>
<span>填写表单</span>
</template>
<div class="cu-content">
<v-form-render :form-json="{}" :form-data="{}" ref="vfRenderRef" />
</div>
</el-card>
<el-card class="box-card" shadow="hover">
<template #header>
<span>审批流程</span>
</template>
<el-row>
<el-col :span="20" :offset="2">
<el-form ref="taskFormRef" :model="taskForm" :rules="rules" label-width="120px">
<el-form-item label="审批意见" prop="comment">
<el-input type="textarea" :rows="5" v-model="taskForm.comment" placeholder="请输入 审批意见" />
</el-form-item>
<el-form-item label="抄送人" prop="copyUserIds">
<el-tag :key="index" v-for="(item, index) in copyUser" closable :disable-transitions="false" @close="handleClose('copy', item)">
{{ item.nickName }}
</el-tag>
<el-button class="button-new-tag" type="primary" icon="el-icon-plus" circle @click="onSelectCopyUsers" />
</el-form-item>
<el-form-item label="指定审批人" prop="copyUserIds">
<el-tag :key="index" v-for="(item, index) in nextUser" closable :disable-transitions="false" @close="handleClose('next', item)">
{{ item.nickName }}
</el-tag>
<el-button class="button-new-tag" type="primary" icon="el-icon-plus" circle @click="onSelectNextUsers" />
</el-form-item>
</el-form>
</el-col>
</el-row>
<el-row :gutter="10" type="flex" justify="center">
<el-col :span="1.5">
<el-button icon="CircleCheck" type="success" @click="handleComplete">通过</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="ChatLineSquare" type="primary" @click="handleDelegate">委派</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Switch" type="success" @click="handleTransfer">转办</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="RefreshLeft" type="warning" @click="handleReturn">退回</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="CircleClose" type="danger" @click="handleReject">拒绝</el-button>
</el-col>
</el-row>
</el-card>
</el-tab-pane>
<el-tab-pane label="表单信息" name="form">
<div v-if="customForm.visible"> <!-- 自定义表单 -->
<component ref="refCustomForm" :disabled="customForm.disabled" :is="customForm.formComponent" :model="customForm.model"
:customFormData="customForm.customFormData" :isNew = "customForm.isNew"></component>
</div>
<div v-if="formVisible">
<el-card class="box-card" shadow="never" v-for="(item, index) in processFormList" :key="index">
<template #header>
<span>{{ item.title }}</span>
</template>
<!--流程处理表单模块-->
<div class="cu-content">
<v-form-render :form-json="item.formModel" :form-data="item.formData" ref="vFormRenderRef" />
</div>
</el-card>
</div>
<div style="margin-left:10%;margin-bottom: 30px">
<!--对上传文件进行显示处理,临时方案 add by nbacheng 2022-07-27 -->
<!-- <el-upload action="#" :on-preview="handleFilePreview" :file-list="fileList" v-if="fileDisplay" /> -->
</div>
</el-tab-pane >
<el-tab-pane label="流转记录" name="record">
<el-card class="box-card" shadow="never">
<el-col :span="20" :offset="2">
<div class="block">
<el-timeline>
<el-timeline-item v-for="(item, index) in historyProcNodeList" :key="index" :type="tagType(item.endTime)">
<p style="font-weight: 700">{{ item.activityName }}</p>
<el-card v-if="item.activityType === 'startEvent'" class="box-card" shadow="hover">
{{ item.assigneeName }} 在 {{ item.createTime }} 发起流程
</el-card>
<el-card v-if="item.activityType === 'userTask'" class="box-card" shadow="hover">
<el-descriptions :column="5" :labelStyle="{'font-weight': 'bold'}">
<el-descriptions-item label="实际办理">{{ item.assigneeName || '-'}}</el-descriptions-item>
<el-descriptions-item label="候选办理">{{ item.candidate || '-'}}</el-descriptions-item>
<el-descriptions-item label="接收时间">{{ item.createTime || '-'}}</el-descriptions-item>
<el-descriptions-item label="办结时间">{{ item.endTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ item.duration || '-'}}</el-descriptions-item>
</el-descriptions>
<div v-if="item.commentList && item.commentList.length > 0">
<div v-for="(comment, index) in item.commentList" :key="index">
<el-divider content-position="left">
<el-tag :type="approveTypeTag(comment.type)">{{ commentType(comment.type) }}</el-tag>
<el-tag type="info" effect="plain">{{ comment.time }}</el-tag>
</el-divider>
<span>{{ comment.fullMessage }}</span>
</div>
</div>
</el-card>
<el-card v-if="item.activityType === 'endEvent'" class="box-card" shadow="hover">
{{ item.createTime }} 结束流程
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-col>
</el-card>
</el-tab-pane>
<el-tab-pane label="流程跟踪" name="track">
<el-card class="box-card" shadow="never">
<process-viewer
:key="`designer-${loadIndex}`"
:style="'height:' + height"
:xml="processXml"
:finishedInfo="finishedInfo"
:allCommentList="historyProcNodeList"
/>
</el-card>
</el-tab-pane>
</el-tabs>
<!--退回流程-->
<el-dialog :title="returnDialog.title" v-model="returnDialog.visible" width="40%" append-to-body>
<el-radio-group v-model="returnTaskKey">
<el-radio-button v-for="item in returnTaskList" :key="item.id" :label="item.id">
{{ item.name }}
</el-radio-button>
</el-radio-group>
<template #footer>
<el-button @click="returnDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitReturn">确 定</el-button>
</template>
</el-dialog>
<el-dialog :title="userSelectDialog.title" v-model="userSelectDialog.visible" width="60%" append-to-body>
<el-row type="flex" :gutter="20">
<!--部门数据-->
<el-col :span="5">
<el-card shadow="never" style="height: 100%">
<template #header>
<span>部门列表</span>
</template>
<div class="head-container">
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
<el-tree
:data="deptOptions"
:props="{ label: 'label', children: 'children' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="deptTreeRef"
default-expand-all
@node-click="handleNodeClick"
/>
</div>
</el-card>
</el-col>
<el-col :span="18">
<el-table
ref="userTable"
:key="userSelectType"
height="500"
v-loading="userLoading"
:data="userList"
highlight-current-row
@current-change="changeCurrentUser"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="userSelectType === 'copy' || userSelectType === 'next'" width="55" type="selection" />
<el-table-column v-else width="30">
<template #default="scope">
<el-radio :label="scope.row.userId" v-model="currentUserId">{{''}}</el-radio>
</template>
</el-table-column>
<el-table-column label="用户名称" align="center" prop="userName" />
<el-table-column label="用户昵称" align="center" prop="nickName" />
<el-table-column label="手机" align="center" prop="phonenumber" />
</el-table>
<pagination :total="userTotal" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-col>
</el-row>
<template #footer>
<el-button @click="userSelectDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitUserData">确 定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="Detail" lang="ts">
import { detailProcess, processIscompleted } from "@/api/workflow/process";
import { getProcessVariables } from '@/api/workflow/task';
import { complete, delegate, transfer, rejectTask, returnList, returnTask } from "@/api/workflow/work/task";
import { deptTreeSelect, selectUser } from "@/api/workflow/identity";
import { TaskForm } from "@/api/workflow/work/types";
import { UserVO, DeptVO } from "@/api/workflow/identity/types";
import { ComponentInternalInstance } from "vue";
import {useRoute} from "vue-router";
import {
useFlowable
} from '@/views/workflow/hooks/useFlowable'
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { getFormComponent } = useFlowable()
const userList = ref<UserVO[]>([]);
const processed = ref(false);
const taskFormOpen = ref(false)
const userMultipleSelection = ref([]);
const userSelectType = ref();
const currentUserId = ref();
const userLoading = ref(false);
const userTotal = ref(0);
const loadIndex = ref('');
const height = ref(document.documentElement.clientHeight - 205 + 'px;');
const processXml = ref('');
const taskFormVisible = ref(false);
const processFormList = ref([]);
const taskFormData = ref([]);
const historyProcNodeList = ref<any>();
const formVisible = ref(false);
const finishedInfo = ref({});
const customForm = ref({ //自定义业务表单
formId: '',
title: '',
disabled: false,
visible: false,
formComponent: null,
model: {},
/*流程数据*/
customFormData: {},
isNew: false,
disableSubmit: true
})
const deptName = ref('');
const deptOptions = ref<DeptVO[]>([]);
const returnTaskList = ref();
const returnTaskKey = ref();
const copyUser = ref([]);
const nextUser = ref([]);
const taskFormRef = ref(ElForm);
const vFormRenderRef = ref(null);
const deptTreeRef = ref(null);
const refCustomForm = ref(null);
const activeName = ref(''); //获取当然tabname
const returnDialog = reactive<DialogOption>({
visible: false,
title: '退回流程'
});
const userSelectDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const taskForm = reactive<TaskForm>({
comment: '',
procInsId: '',
taskId: '',
userId: '',
copyUserIds: '',
nextUserIds: '',
vars: '',
targetKey: ''
});
const rules = ref({
comment: [{ required: true, message: '请输入审批意见', trigger: 'blur' }]
});
const queryParams = ref({
pageNum: 1,
pageSize: 10
});
const tagType = (val: any) => {
if (val) {
return "success";
} else {
return "info";
}
}
const commentType = (val: string) => {
switch (val) {
case '1': return '通过'
case '2': return '退回'
case '3': return '驳回'
case '4': return '委派'
case '5': return '转办'
case '6': return '终止'
case '7': return '撤回'
case '8': return '拒绝'
case '9': return '跳过'
case '10': return '前加签'
case '11': return '后加签'
case '12': return '多实例加签'
case '13': return '跳转'
case '14': return '收回'
}
}
const approveTypeTag = (val: string) => {
switch (val) {
case '1': return 'success'
case '2': return 'warning'
case '3': return 'danger'
case '4': return 'primary'
case '5': return 'success'
case '6': return 'danger'
case '7': return 'info'
}
}
const initData = () => {
taskForm.procInsId = route.params && route.params.procInsId as string;
taskForm.taskId = route.query && route.query.taskId as string;
processed.value = route.query && (route.query.processed || false) === "true";
// 流程任务重获取变量表单
//getProcessDetails(taskForm.procInsId, taskForm.taskId);
//判断流程是否结束
processIscompleted({procInsId: taskForm.procInsId}).then(res => {
console.log("processIscompleted res=",res);
if(res.data) {
processed.value = false;
}
// 获取流程变量
processVariables(taskForm.taskId);
});
};
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.indexOf(value) !== -1
}
// /** 根据名称筛选部门树 */
// watchEffect(
// () => {deptTreeRef.value.filter(deptName.value);},
// {
// flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
// }
// );
// 节点单击事件
const handleNodeClick = (data: any) => {
getList(data.id);
}
/** 查询部门下拉树结构 */
const getTreeSelect = async () => {
const res = await deptTreeSelect();
deptOptions.value = res.data;
};
/** 查询用户列表 */
const getList = async (deptId?: number) => {
userLoading.value = true;
const res = await selectUser({deptId: deptId});
userLoading.value = false;
userList.value = res.rows;
userTotal.value = res.total;
}
/** 获取流程变量内容 */
const processVariables = (taskId: string) => {
console.log("processVariables taskId",taskId);
if (taskId) {
getProcessVariables(taskId).then(res => {
console.log("getProcessVariables res=",res);
if(res.code == 200) {
if(res.data.hasOwnProperty('dataId') && res.data.dataId) {
customForm.value.formId = res.data.dataId;
// 流程任务重获取变量表单
getProcessDetails(taskForm.procInsId, taskForm.taskId, res.data.dataId);
loadIndex.value = taskForm.procInsId;
if(processed.value) {
activeName.value = "approval";
}
else {
activeName.value = "form";
}
}
else {
// 流程任务重获取变量表单
getProcessDetails(taskForm.procInsId, taskForm.taskId, "");
loadIndex.value = taskForm.procInsId;
if(processed.value) {
activeName.value = "approval";
}
else {
activeName.value = "form";
// 回填数据,这里主要是处理文件列表显示,临时解决,以后应该在formdesigner里完成
/*this.processFormList.forEach((item, i) => {
if (item?.hasOwnProperty('list')) {
fillFormData(item.list, item)
// 更新表单
this.key = +new Date().getTime()
}
});*/
}
}
}
});
}
}
const fillFormData = (list, formConf) => { // for formdesigner
console.log("fillFormData list=",list);
console.log("fillFormData formConf=",formConf);
list.forEach((item, i) => {
// 特殊处理el-upload,包括 回显图片
if(formConf.formValues[item.id] != '') {
const val = formConf.formValues[item.id];
if (item.ele === 'el-upload') {
console.log('fillFormData val=',val)
if(item['list-type'] != 'text') {//图片
this.fileList = [] //隐藏加的el-upload文件列表
//item['file-list'] = JSON.parse(val)
if(val != '') {
item['file-list'] = JSON.parse(val)
}
}
else { //列表
console.log("列表fillFormData val",val)
this.fileList = JSON.parse(val)
item['file-list'] = [] //隐藏加的表单设计器的文件列表
}
// 回显图片
this.fileDisplay = true
}
}
if (Array.isArray(item.columns)) {
this.fillFormData(item.columns, formConf)
}
})
}
const getProcessDetails = async (procInsId: string, taskId: string, dataId: string) => {
const params = {procInsId: procInsId, taskId: taskId, dataId: dataId}
const res = await detailProcess(params);
const data = res.data;
console.log("getProcessDetails data",data);
processXml.value = data.bpmnXml;
processFormList.value = data.processFormList;
console.log("processFormList",processFormList);
if(processFormList.value.length == 1 &&processFormList.value[0].formValues.hasOwnProperty('routeName')) {
customForm.value.disabled = true;
customForm.value.visible = true;
customForm.value.formComponent = getFormComponent(processFormList.value[0].formValues.routeName).component;
customForm.value.model = processFormList.value[0].formValues.formData;
customForm.value.customFormData = processFormList.value[0].formValues.formData;
if(data.startUserNode) {
customForm.value.isNew = true;
customForm.value.disabled = false;
}
console.log("detailProcess customForm",customForm.value);
}
else {
taskFormVisible.value = data.existTaskForm;
if (taskFormVisible.value) {
taskFormData.value = data.taskFormData;
}
formVisible.value = true;
nextTick(() => {
processFormList.value.forEach((item: any, index: any) => {
if (item.disabled) {
vFormRenderRef.value[index].disableForm();
}
})
})
}
historyProcNodeList.value = data.historyProcNodeList;
finishedInfo.value = data.flowViewer;
}
const onSelectCopyUsers = () => {
userMultipleSelection.value = copyUser;
onSelectUsers('添加抄送人', 'copy')
}
const onSelectNextUsers = () => {
userMultipleSelection.value = nextUser;
onSelectUsers('指定审批人', 'next')
}
const onSelectUsers = (title: string, type: string) => {
userSelectType.value = type;
userSelectDialog.title = title;
userSelectDialog.visible = true;
getTreeSelect();
getList()
}
/** 通过任务 */
const handleComplete = () => {
// 校验表单
taskFormRef.value.validate(async (valid: boolean) => {
if (valid) {
const res = await complete(taskForm)
proxy?.$modal.msgSuccess(res.msg);
goBack();
}
});
}
/** 委派任务 */
const handleDelegate = () => {
userSelectType.value = 'delegate';
userSelectDialog.title = '委派任务'
userSelectDialog.visible = true;
getTreeSelect();
}
/** 转办任务 */
const handleTransfer = () => {
userSelectType.value = 'transfer';
userSelectDialog.title = '转办任务';
userSelectDialog.visible = true;
getTreeSelect();
}
/** 退回任务 */
const handleReturn = async () => {
// 校验表单
taskFormRef.value.validate(async (valid: boolean) => {
if (valid) {
const res = await returnList(taskForm);
returnTaskList.value = res.data;
returnDialog.visible = true;
}
});
}
/** 拒绝任务 */
const handleReject = async () => {
await proxy?.$modal.confirm('拒绝审批单流程会终止,是否继续?');
await rejectTask(taskForm);
proxy?.$modal.msgSuccess("操作成功");
goBack();
}
/** 返回页面 */
const goBack = () => {
// 关闭当前标签页并返回上个页面
proxy?.$tab.closePage(route);
router.back()
}
// 关闭标签
const handleClose = (type: any, tag: any) => {
let userObj = userMultipleSelection.value.find(item => item.userId === tag.id);
userMultipleSelection.value.splice(userMultipleSelection.value.indexOf(userObj), 1);
if (type === 'copy') {
copyUser.value = userMultipleSelection.value;
// 设置抄送人ID
if (copyUser.value && copyUser.value.length > 0) {
const val = copyUser.value.map(item => item.id);
taskForm.copyUserIds = val instanceof Array ? val.join(',') : val;
} else {
taskForm.copyUserIds = '';
}
} else if (type === 'next') {
nextUser.value = userMultipleSelection.value;
// 设置抄送人ID
if (nextUser.value && nextUser.value.length > 0) {
const val = nextUser.value.map(item => item.id);
taskForm.nextUserIds = val instanceof Array ? val.join(',') : val;
} else {
taskForm.nextUserIds = '';
}
}
}
const changeCurrentUser = (val: any) => {
// currentUserId = val.userId
}
const handleSelectionChange = () => {
}
const submitReturn = () => {
// 校验表单
taskFormRef.value.validate(async (valid: boolean) => {
if (valid) {
if (!returnTaskKey) {
proxy?.$modal.msgError("请选择退回节点!");
}
taskForm.targetKey = returnTaskKey.value;
const res = await returnTask(taskForm);
proxy?.$modal.msgSuccess(res.msg);
goBack()
}
});
console.log("taskForm => ", taskForm.targetKey);
}
const submitUserData = () => {
let type = userSelectType.value;
if (type === 'copy' || type === 'next') {
if (!userMultipleSelection || userMultipleSelection.value.length <= 0) {
proxy?.$modal.msgError("请选择用户");
return false;
}
let userIds = userMultipleSelection.value.map(k => k.userId);
if (type === 'copy') {
// 设置抄送人ID信息
copyUser.value = userMultipleSelection.value;
taskForm.copyUserIds = userIds instanceof Array ? userIds.join(',') : userIds;
} else if (type === 'next') {
// 设置下一级审批人ID信息
nextUser.value = userMultipleSelection.value;
taskForm.nextUserIds = userIds instanceof Array ? userIds.join(',') : userIds;
}
userSelectDialog.visible = false;
} else {
if (!taskForm.comment) {
proxy?.$modal.msgError("请输入审批意见");
return false;
}
if (!currentUserId.value) {
proxy?.$modal.msgError("请选择用户");
return false;
}
taskForm.userId = currentUserId.value;
if (type === 'delegate') {
delegate(taskForm).then(res => {
proxy?.$modal.msgSuccess(res.msg);
goBack();
});
}
if (type === 'transfer') {
transfer(taskForm).then(res => {
proxy?.$modal.msgSuccess(res.msg);
goBack();
});
}
}
}
onMounted(() => {
initData();
});
</script>
<style lang="scss" scoped>
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.box-card {
width: 100%;
margin-bottom: 20px;
}
.el-tag + .el-tag {
margin-left: 10px;
}
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.el-col {
border-radius: 4px;
}
.button-new-tag {
margin-left: 10px;
}
</style>
2、其中hooks的useFlowable修改如下(原先minixs)
export const useFlowable = () => {
const allFormComponent = computed(() => {
return [
{
text:'单表示例',
routeName: '@/views/workflow/demo/wf',
component: defineAsyncComponent( () => import('@/views/workflow/demo/wf')),
businessTable:'wf_demo'
},
/*{
text:'主子表示例',
routeName:'@/views/workflow/demo/modules/CesOrderMainForm',
component:() => defineAsyncComponent(import(`@/views/workflow/demo/modules/CesOrderMainForm`)),
businessTable:'ces_order_main'
}*/
]
})
const getFormComponent = (routeName) => {
return allFormComponent.value.find((item) => item.routeName === routeName) || {}
}
return {
allFormComponent,
getFormComponent
}
}
3、效果图如下: