ProcessDefinitionDetail.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <template>
  2. <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
  3. <div class="processInstance-wrap-main">
  4. <el-scrollbar>
  5. <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
  6. <el-divider class="!my-8px" />
  7. <!-- 中间主要内容 tab 栏 -->
  8. <el-tabs v-model="activeTab">
  9. <!-- 表单信息 -->
  10. <el-tab-pane label="表单填写" name="form">
  11. <div class="form-scroll-area" v-loading="processInstanceStartLoading">
  12. <el-scrollbar>
  13. <el-row>
  14. <el-col :span="17">
  15. <form-create
  16. :rule="detailForm.rule"
  17. v-model:api="fApi"
  18. v-model="detailForm.value"
  19. :option="detailForm.option"
  20. @submit="submitForm"
  21. />
  22. </el-col>
  23. <el-col :span="6" :offset="1">
  24. <!-- 流程时间线 -->
  25. <ProcessInstanceTimeline
  26. ref="timelineRef"
  27. :activity-nodes="activityNodes"
  28. :show-status-icon="false"
  29. @select-user-confirm="selectUserConfirm"
  30. />
  31. </el-col>
  32. </el-row>
  33. </el-scrollbar>
  34. </div>
  35. </el-tab-pane>
  36. <!-- 流程图 -->
  37. <el-tab-pane label="流程图" name="diagram">
  38. <div class="form-scroll-area">
  39. <!-- BPMN 流程图预览 -->
  40. <ProcessInstanceBpmnViewer
  41. :bpmn-xml="bpmnXML"
  42. v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
  43. />
  44. <!-- Simple 流程图预览 -->
  45. <ProcessInstanceSimpleViewer
  46. :simple-json="simpleJson"
  47. v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
  48. />
  49. </div>
  50. </el-tab-pane>
  51. </el-tabs>
  52. <!-- 底部操作栏 -->
  53. <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
  54. <!-- 操作栏按钮 -->
  55. <div
  56. v-if="activeTab === 'form'"
  57. class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
  58. >
  59. <el-button plain type="success" @click="submitForm">
  60. <Icon icon="ep:select" />&nbsp; 发起
  61. </el-button>
  62. <el-button plain type="danger" @click="handleCancel">
  63. <Icon icon="ep:close" />&nbsp; 取消
  64. </el-button>
  65. </div>
  66. </div>
  67. </el-scrollbar>
  68. </div>
  69. </ContentWrap>
  70. </template>
  71. <script lang="ts" setup>
  72. import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
  73. import { BpmModelType } from '@/utils/constants'
  74. import {
  75. CandidateStrategy,
  76. NodeId,
  77. FieldPermissionType
  78. } from '@/components/SimpleProcessDesignerV2/src/consts'
  79. import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
  80. import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
  81. import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
  82. import type { ApiAttrs } from '@form-create/element-ui/types/config'
  83. import { useTagsViewStore } from '@/store/modules/tagsView'
  84. import * as ProcessInstanceApi from '@/api/bpm/processInstance'
  85. import * as DefinitionApi from '@/api/bpm/definition'
  86. import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
  87. defineOptions({ name: 'ProcessDefinitionDetail' })
  88. const props = defineProps<{
  89. selectProcessDefinition: any
  90. }>()
  91. const emit = defineEmits(['cancel'])
  92. const processInstanceStartLoading = ref(false) // 流程实例发起中
  93. const { push, currentRoute } = useRouter() // 路由
  94. const message = useMessage() // 消息弹窗
  95. const { delView } = useTagsViewStore() // 视图操作
  96. const detailForm: any = ref({
  97. rule: [],
  98. option: {},
  99. value: {}
  100. }) // 流程表单详情
  101. const fApi = ref<ApiAttrs>()
  102. // 指定审批人
  103. const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表
  104. const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
  105. const bpmnXML: any = ref(null) // BPMN 数据
  106. const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式
  107. const activeTab = ref('form') // 当前的 Tab
  108. const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
  109. /** 设置表单信息、获取流程图数据 **/
  110. const initProcessInfo = async (row: any, formVariables?: any) => {
  111. // 重置指定审批人
  112. startUserSelectTasks.value = []
  113. startUserSelectAssignees.value = {}
  114. // 情况一:流程表单
  115. if (row.formType == 10) {
  116. // 设置表单
  117. // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
  118. // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
  119. // 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
  120. const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
  121. for (const key in formVariables) {
  122. if (!allowedFields.includes(key)) {
  123. delete formVariables[key]
  124. }
  125. }
  126. setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
  127. await nextTick()
  128. fApi.value?.btn.show(false) // 隐藏提交按钮
  129. // 获取流程审批信息
  130. await getApprovalDetail(row)
  131. // 加载流程图
  132. const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
  133. if (processDefinitionDetail) {
  134. bpmnXML.value = processDefinitionDetail.bpmnXml
  135. simpleJson.value = processDefinitionDetail.simpleModel
  136. }
  137. // 情况二:业务表单
  138. } else if (row.formCustomCreatePath) {
  139. await push({
  140. path: row.formCustomCreatePath
  141. })
  142. // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
  143. }
  144. }
  145. /** 获取审批详情 */
  146. const getApprovalDetail = async (row: any) => {
  147. try {
  148. // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效)
  149. const data = await ProcessInstanceApi.getApprovalDetail({
  150. processDefinitionId: row.id,
  151. activityId: NodeId.START_USER_NODE_ID
  152. })
  153. if (!data) {
  154. message.error('查询不到审批详情信息!')
  155. return
  156. }
  157. // 获取发起人自选的任务
  158. startUserSelectTasks.value = data.activityNodes?.filter(
  159. (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
  160. )
  161. if (startUserSelectTasks.value?.length > 0) {
  162. for (const node of startUserSelectTasks.value) {
  163. startUserSelectAssignees.value[node.id] = []
  164. }
  165. }
  166. // 获取审批节点,显示 Timeline 的数据
  167. activityNodes.value = data.activityNodes
  168. // 获取表单字段权限
  169. const formFieldsPermission = data.formFieldsPermission
  170. // 设置表单字段权限
  171. if (formFieldsPermission) {
  172. Object.keys(formFieldsPermission).forEach((item) => {
  173. setFieldPermission(item, formFieldsPermission[item])
  174. })
  175. }
  176. } finally {
  177. }
  178. }
  179. /**
  180. * 设置表单权限
  181. */
  182. const setFieldPermission = (field: string, permission: string) => {
  183. if (permission === FieldPermissionType.READ) {
  184. //@ts-ignore
  185. fApi.value?.disabled(true, field)
  186. }
  187. if (permission === FieldPermissionType.WRITE) {
  188. //@ts-ignore
  189. fApi.value?.disabled(false, field)
  190. }
  191. if (permission === FieldPermissionType.NONE) {
  192. //@ts-ignore
  193. fApi.value?.hidden(true, field)
  194. }
  195. }
  196. /** 提交按钮 */
  197. const submitForm = async () => {
  198. if (!fApi.value || !props.selectProcessDefinition) {
  199. return
  200. }
  201. // 流程表单校验
  202. await fApi.value.validate()
  203. // 如果有指定审批人,需要校验
  204. if (startUserSelectTasks.value?.length > 0) {
  205. for (const userTask of startUserSelectTasks.value) {
  206. if (
  207. Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
  208. startUserSelectAssignees.value[userTask.id].length === 0
  209. )
  210. return message.warning(`请选择${userTask.name}的候选人`)
  211. }
  212. }
  213. // 提交请求
  214. processInstanceStartLoading.value = true
  215. try {
  216. await ProcessInstanceApi.createProcessInstance({
  217. processDefinitionId: props.selectProcessDefinition.id,
  218. variables: detailForm.value.value,
  219. startUserSelectAssignees: startUserSelectAssignees.value
  220. })
  221. // 提示
  222. message.success('发起流程成功')
  223. // 跳转回去
  224. delView(unref(currentRoute))
  225. await push({
  226. name: 'BpmProcessInstanceMy'
  227. })
  228. } finally {
  229. processInstanceStartLoading.value = false
  230. }
  231. }
  232. /** 取消发起审批 */
  233. const handleCancel = () => {
  234. emit('cancel')
  235. }
  236. /** 选择发起人 */
  237. const selectUserConfirm = (id: string, userList: any[]) => {
  238. startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
  239. }
  240. defineExpose({ initProcessInfo })
  241. </script>
  242. <style lang="scss" scoped>
  243. $wrap-padding-height: 20px;
  244. $wrap-margin-height: 15px;
  245. $button-height: 51px;
  246. $process-header-height: 105px;
  247. .processInstance-wrap-main {
  248. height: calc(
  249. 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
  250. );
  251. max-height: calc(
  252. 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
  253. );
  254. overflow: auto;
  255. .form-scroll-area {
  256. height: calc(
  257. 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
  258. $process-header-height - 40px
  259. );
  260. max-height: calc(
  261. 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
  262. $process-header-height - 40px
  263. );
  264. overflow: auto;
  265. }
  266. }
  267. .form-box {
  268. :deep(.el-card) {
  269. border: none;
  270. }
  271. }
  272. </style>