consentOrRefuseDialog.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <template>
  2. <!-- 同意 拒绝 弹窗 同意/拒绝审批 -->
  3. <el-dialog v-model="operaVisibleDialog" class="le-dialog" :title="currentTip + '审批'" width="700" destroy-on-close :close-on-click-modal="false">
  4. <el-form ref="formRef" v-loading="uploadLoading" label-position="top" element-loading-text="图片上传中..." :model="form" label-width="80px">
  5. <el-form-item
  6. v-if="rejectStrategy === 3 && currentType === 'reject'"
  7. label="回退节点"
  8. prop="nodeKey"
  9. :rules="[{ required: true, message: '请选择回退节点' }]"
  10. >
  11. <el-select v-model="form.nodeKey" placeholder="请选择回退节点">
  12. <el-option v-for="item in rollbackOptions" :key="item.nodeKey" :label="item.nodeName" :value="item.nodeKey" />
  13. </el-select>
  14. </el-form-item>
  15. <el-form-item label="审批意见" prop="content" :rules="[{ required: true, message: '审批意见不能为空' }]">
  16. <el-input v-model="form.content" type="textarea" placeholder="请输入内容" maxlength="64" show-word-limit> </el-input>
  17. </el-form-item>
  18. <el-form-item
  19. v-if="nodeModelsData.length"
  20. label="下一节点审批人"
  21. prop="assigneeMap"
  22. required
  23. :rules="[{ required: true, message: '请选择下一节点审批人' }]"
  24. >
  25. <el-timeline>
  26. <el-timeline-item v-for="(item, index) in nodeModelsData" :key="index" placement="top" :timestamp="item.nodeName">
  27. <div class="flex">
  28. <!-- type: 1[审核人] 2[抄送人] 4[条件路由] 5[子流程] 6[延时处理] 7[触发器] 8[并行路由] 9[包容路由] 10[路由分支]-->
  29. <template v-if="item.type === 1">
  30. <div v-if="[2, 5].indexOf(item.setType) > -1">
  31. <el-tag v-if="item.setType === 5" type="primary">{{ setTypeOptions_config[item.setType] }}</el-tag>
  32. <el-tag v-if="item.setType === 2" type="primary">{{
  33. item.examineLevel === 1 ? '直接主管' : `发起人的第${item.examineLevel}级主管`
  34. }}</el-tag>
  35. </div>
  36. <template v-if="item.setType === 4">
  37. <!-- item.nodeCandidate里面的type属性 type:1|0 角色|用户-->
  38. <div v-if="item.nodeCandidate.type === 1" class="mr-2">
  39. <el-tooltip content="添加角色" placement="bottom">
  40. <el-button style="width: 32px" @click="selectHandler(item, 3)">
  41. <svg-icon style="font-size: 18px" icon-class="flow-group-add" />
  42. </el-button>
  43. </el-tooltip>
  44. </div>
  45. <div v-if="item.nodeCandidate.type === 0" class="mr-2">
  46. <el-tooltip content="添加用户" placement="bottom">
  47. <el-button style="width: 32px" @click="selectHandler(item, 1)">
  48. <svg-icon style="font-size: 18px" icon-class="flow-user-add" />
  49. </el-button>
  50. </el-tooltip>
  51. </div>
  52. </template>
  53. </template>
  54. <template v-else>{{ item.nodeName }}</template>
  55. <div v-if="item.selectMode === 1" class="flex flex-wrap gap-[6px]">
  56. <FlowNodeAvatar v-for="(childItem, childIndex) in item.nodeAssigneeList" :key="childIndex" :name="childItem.name" />
  57. </div>
  58. <div v-if="item.selectMode !== 1" class="flex flex-wrap gap-[6px]">
  59. <FlowNodeAvatar v-for="(childItem, childIndex) in item.nodeAssigneeList" :key="childIndex" :name="childItem.name">
  60. <template #avatar>
  61. <svg-icon icon-class="flow-group" color="#fff" />
  62. </template>
  63. </FlowNodeAvatar>
  64. </div>
  65. </div>
  66. </el-timeline-item>
  67. </el-timeline>
  68. </el-form-item>
  69. <el-form-item v-if="currentType !== 'reject'" label="推荐回复">
  70. <el-tag v-for="(label, index) in buttonLabels" :key="index" type="info" class="ml-2 cursor-pointer" @click="appendToApprovalComments(label)">
  71. {{ label }}
  72. </el-tag>
  73. </el-form-item>
  74. <el-form-item v-if="currentType === 'reject'" prop="termination">
  75. <el-checkbox v-model="form.termination" label="终止流程" />
  76. </el-form-item>
  77. <el-form-item v-if="false" prop="attachment" label="附件" class="example-img-box">
  78. <!--'.docx', '.doc', '.pptx', '.ppt', '.xlsx', '.xls', '.zip', '.csv', '.pdf', '.png', '.jpg' 因前端不支持图片以外格式,所以注释 -->
  79. <FileUpload
  80. v-model="form.attachment"
  81. source="project"
  82. return="array"
  83. :limit="5"
  84. :file-size="10"
  85. :accept="['.png', '.jpg']"
  86. @success="clearValidate"
  87. />
  88. </el-form-item>
  89. </el-form>
  90. <template #footer>
  91. <span class="dialog-footer">
  92. <el-button :loading="btnDisabled" class="dialog-btn" @click="closeDialog">取 消</el-button>
  93. <el-button type="primary" :loading="btnDisabled" class="dialog-btn" @click="submitForm">确 定</el-button>
  94. </span>
  95. </template>
  96. <!-- 选择人员/角色-->
  97. <use-select ref="useSelectRef" v-bind="active_selectOpts" @update:selected="updateActive_assigneeMap"></use-select>
  98. </el-dialog>
  99. </template>
  100. <script setup>
  101. import { computed, reactive, watch, toRefs, nextTick } from 'vue'
  102. import FileUpload from '@/components/FileUpload.vue'
  103. import FlowNodeAvatar from '@/components/Flow/FlowNodeAvatar.vue'
  104. import { nextNodesApi, processConsentTaskApi, processPreviousNodeNameApi, processRejectionTaskApi } from '@/api/flow/processTask'
  105. import { ElMessage } from 'element-plus'
  106. import { debounce } from 'lodash-es'
  107. import UseSelect from '@/components/scWorkflow/select.vue'
  108. import { setTypeOptions_config } from '@/components/scWorkflow/nodes/config'
  109. const props = defineProps({
  110. // 弹窗是否显示
  111. modelValue: {
  112. type: Boolean,
  113. default: false
  114. },
  115. // 审核id
  116. taskId: {
  117. type: String,
  118. default: undefined
  119. },
  120. // 审核类型 同意(agree) or 拒绝(reject)
  121. currentType: {
  122. type: String,
  123. default: 'agree'
  124. },
  125. // 额外 表单数据
  126. formData: {
  127. type: Object,
  128. default: () => ({})
  129. },
  130. // 拒绝策略
  131. rejectStrategy: {
  132. type: Number || undefined,
  133. default: undefined
  134. },
  135. instanceId: {
  136. type: String,
  137. default: ''
  138. }
  139. })
  140. const buttonLabels = ['同意', '已阅', '收到', '已核对', '合格', '情况属实', '确认', '已复核', '知悉', '辛苦了', '已安排']
  141. const form = reactive({
  142. nodeKey: undefined,
  143. content: '',
  144. assigneeMap: null
  145. // attachment: []
  146. })
  147. const datas = reactive({
  148. formRef: null,
  149. uploadLoading: false,
  150. rollbackOptions: [],
  151. btnDisabled: false,
  152. nodeModelsData: [],
  153. active_selectOpts: {},
  154. useSelectRef: null,
  155. active_assigneeKey: null
  156. })
  157. const { formRef, uploadLoading, rollbackOptions, btnDisabled, nodeModelsData, active_selectOpts, useSelectRef, active_assigneeKey } =
  158. toRefs(datas)
  159. const $myEmit = defineEmits(['update:modelValue', 'successCb'])
  160. const closeDialog = () => {
  161. $myEmit('update:modelValue', false)
  162. }
  163. const operaVisibleDialog = computed({
  164. get() {
  165. return props.modelValue
  166. },
  167. set(val) {
  168. $myEmit('update:modelValue', val)
  169. }
  170. })
  171. const currentTip = computed(() => {
  172. return props.currentType === 'agree' ? '同意' : '拒绝'
  173. })
  174. const clearValidate = () => {}
  175. // 回退节点列表
  176. const getProcessPreviousNodeNameApi = async () => {
  177. const res = await processPreviousNodeNameApi(props.taskId)
  178. rollbackOptions.value = res || []
  179. }
  180. const appendToApprovalComments = label => {
  181. form.content += (form.content ? '' : '') + label
  182. }
  183. const getNextNodesEv = async params => {
  184. const { nodeModels } = await nextNodesApi(params)
  185. nodeModelsData.value = nodeModels || []
  186. /**
  187. * "nodeModels": [
  188. {
  189. "nodeName": "CEO审批",
  190. "nodeKey": "flk1720532364452",
  191. "nodeAssigneeList": [
  192. {
  193. "id": "0",
  194. "name": "CEO"
  195. },
  196. {
  197. "id": "1",
  198. "name": "CEO1"
  199. }
  200. ],
  201. "selectMode": 1
  202. },
  203. ],
  204. "nodeType": 1
  205. */
  206. }
  207. const findNodeByKey = nodeKey => {
  208. return nodeModelsData.value.find(node => node.nodeKey === nodeKey)
  209. }
  210. const validateNodeAssigneeLists = () => {
  211. return (nodeModelsData.value || []).every(node => node.nodeAssigneeList && node.nodeAssigneeList.length > 0)
  212. }
  213. const updateActive_assigneeMap = assignees => {
  214. const _cur = findNodeByKey(active_assigneeKey.value)
  215. if (_cur) {
  216. _cur.nodeAssigneeList = assignees
  217. }
  218. }
  219. const selectHandler = (item, type) => {
  220. // nodeModelsData.value数组中查找和nodeKey相匹配的对象值
  221. const foundNode = findNodeByKey(item.nodeKey)
  222. let config = foundNode ? { assignees: foundNode.nodeAssigneeList, type } : { assignees: [], type }
  223. if (item.selectMode === 1) {
  224. config.selectOpts = { maxSelected: 1 }
  225. }
  226. // 插入备选列表 candidateAssignees
  227. config.selectOpts = { ...config.selectOpts, candidateAssignees: foundNode.nodeCandidate?.assignees }
  228. active_assigneeKey.value = item.nodeKey
  229. active_selectOpts.value = config.selectOpts || {}
  230. nextTick(() => {
  231. useSelectRef.value.open(type, config.assignees)
  232. })
  233. }
  234. const submitForm = debounce(() => {
  235. const isAgree = props.currentType === 'agree'
  236. if (isAgree && nodeModelsData.value.length) {
  237. const areAllAssigneeListsValid = validateNodeAssigneeLists()
  238. form.assigneeMap = !areAllAssigneeListsValid ? null : nodeModelsData.value
  239. }
  240. const formData = { taskId: props.taskId, ...props.formData, ...form }
  241. formRef.value
  242. .validate()
  243. .then(async valid => {
  244. if (valid) {
  245. btnDisabled.value = true
  246. let data = null
  247. if (isAgree) {
  248. // 同意
  249. formData.termination = false
  250. const validNodes = nodeModelsData.value.filter(node => node.selectMode)
  251. if (validNodes.length) {
  252. formData.assigneeMap = validNodes.reduce((acc, node) => {
  253. acc[node.nodeKey] = { assigneeList: node.nodeAssigneeList, type: node.nodeCandidate.type }
  254. return acc
  255. }, {}) // 组装成后台需要的格式
  256. } else {
  257. formData.assigneeMap = undefined
  258. }
  259. data = await processConsentTaskApi(formData)
  260. } else {
  261. // 拒绝
  262. data = await processRejectionTaskApi(formData)
  263. }
  264. if (!data) {
  265. ElMessage({
  266. message: '执行失败',
  267. type: 'error'
  268. })
  269. return false
  270. }
  271. $myEmit('successCb')
  272. closeDialog()
  273. btnDisabled.value = false
  274. }
  275. })
  276. .catch(err => {
  277. btnDisabled.value = false
  278. })
  279. })
  280. // 驳回 && rejectStrategy === 3
  281. watch(
  282. () => props.rejectStrategy,
  283. item => {
  284. if (item && Number(item) === 3 && props.currentType !== 'agree') {
  285. getProcessPreviousNodeNameApi()
  286. }
  287. },
  288. {
  289. immediate: true
  290. }
  291. )
  292. watch(
  293. () => operaVisibleDialog.value,
  294. newVal => {
  295. if (newVal && props.currentType === 'agree') {
  296. const t = JSON.parse(props.formData.processForm || '{}')?.formData
  297. const _param = {
  298. instanceId: props.instanceId,
  299. args: t
  300. }
  301. getNextNodesEv(_param)
  302. }
  303. },
  304. { immediate: true }
  305. )
  306. </script>
  307. <style scoped lang="scss">
  308. .example-img-box {
  309. width: 100% !important;
  310. }
  311. .example-img-box .el-form-item__content {
  312. display: block;
  313. }
  314. </style>