create.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <template>
  2. <el-row :gutter="20">
  3. <el-col :span="16">
  4. <ContentWrap title="申请信息">
  5. <el-form
  6. ref="formRef"
  7. v-loading="formLoading"
  8. :model="formData"
  9. :rules="formRules"
  10. label-width="80px"
  11. >
  12. <el-form-item label="请假类型" prop="type">
  13. <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
  14. <el-option
  15. v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
  16. :key="dict.value"
  17. :label="dict.label"
  18. :value="dict.value"
  19. />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item label="开始时间" prop="startTime">
  23. <el-date-picker
  24. v-model="formData.startTime"
  25. clearable
  26. placeholder="请选择开始时间"
  27. type="datetime"
  28. value-format="x"
  29. />
  30. </el-form-item>
  31. <el-form-item label="结束时间" prop="endTime">
  32. <el-date-picker
  33. v-model="formData.endTime"
  34. clearable
  35. placeholder="请选择结束时间"
  36. type="datetime"
  37. value-format="x"
  38. />
  39. </el-form-item>
  40. <el-form-item label="原因" prop="reason">
  41. <el-input v-model="formData.reason" placeholder="请输入请假原因" type="textarea" />
  42. </el-form-item>
  43. <el-form-item>
  44. <el-button :disabled="formLoading" type="primary" @click="submitForm">
  45. 确 定
  46. </el-button>
  47. </el-form-item>
  48. </el-form>
  49. </ContentWrap>
  50. </el-col>
  51. <!-- 审批相关:流程信息 -->
  52. <el-col :span="8">
  53. <ContentWrap title="审批流程" :bodyStyle="{ padding: '0 20px 0' }">
  54. <ProcessInstanceTimeline
  55. ref="timelineRef"
  56. :activity-nodes="activityNodes"
  57. :show-status-icon="false"
  58. @select-user-confirm="selectUserConfirm"
  59. />
  60. </ContentWrap>
  61. </el-col>
  62. </el-row>
  63. </template>
  64. <script lang="ts" setup>
  65. import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
  66. import * as LeaveApi from '@/api/bpm/leave'
  67. import { useTagsViewStore } from '@/store/modules/tagsView'
  68. // 审批相关:import
  69. import * as DefinitionApi from '@/api/bpm/definition'
  70. import ProcessInstanceTimeline from '@/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue'
  71. import * as ProcessInstanceApi from '@/api/bpm/processInstance'
  72. import { CandidateStrategy, NodeId } from '@/components/SimpleProcessDesignerV2/src/consts'
  73. import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
  74. defineOptions({ name: 'BpmOALeaveCreate' })
  75. const message = useMessage() // 消息弹窗
  76. const { delView } = useTagsViewStore() // 视图操作
  77. const { push, currentRoute } = useRouter() // 路由
  78. const { query } = useRoute() // 查询参数
  79. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  80. const formData = ref({
  81. type: undefined,
  82. reason: undefined,
  83. startTime: undefined,
  84. endTime: undefined
  85. })
  86. const formRules = reactive({
  87. type: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }],
  88. reason: [{ required: true, message: '请假原因不能为空', trigger: 'change' }],
  89. startTime: [{ required: true, message: '请假开始时间不能为空', trigger: 'change' }],
  90. endTime: [{ required: true, message: '请假结束时间不能为空', trigger: 'change' }]
  91. })
  92. const formRef = ref() // 表单 Ref
  93. // 审批相关:变量
  94. const processDefineKey = 'oa_leave' // 流程定义 Key
  95. const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
  96. const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
  97. const tempStartUserSelectAssignees = ref({}) // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
  98. const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
  99. const processDefinitionId = ref('')
  100. /** 提交表单 */
  101. const submitForm = async () => {
  102. // 1.1 校验表单
  103. if (!formRef) return
  104. const valid = await formRef.value.validate()
  105. if (!valid) return
  106. // 1.2 审批相关:校验指定审批人
  107. if (startUserSelectTasks.value?.length > 0) {
  108. for (const userTask of startUserSelectTasks.value) {
  109. if (
  110. Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
  111. startUserSelectAssignees.value[userTask.id].length === 0
  112. ) {
  113. return message.warning(`请选择${userTask.name}的审批人`)
  114. }
  115. }
  116. }
  117. // 2. 提交请求
  118. formLoading.value = true
  119. try {
  120. const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
  121. // 审批相关:设置指定审批人
  122. if (startUserSelectTasks.value?.length > 0) {
  123. data.startUserSelectAssignees = startUserSelectAssignees.value
  124. }
  125. await LeaveApi.createLeave(data)
  126. message.success('发起成功')
  127. // 关闭当前 Tab
  128. delView(unref(currentRoute))
  129. await push({ name: 'BpmOALeave' })
  130. } finally {
  131. formLoading.value = false
  132. }
  133. }
  134. /** 审批相关:获取审批详情 */
  135. const getApprovalDetail = async () => {
  136. try {
  137. const data = await ProcessInstanceApi.getApprovalDetail({
  138. processDefinitionId: processDefinitionId.value,
  139. // TODO 小北:可以支持 processDefinitionKey 查询
  140. activityId: NodeId.START_USER_NODE_ID,
  141. processVariablesStr: JSON.stringify({ day: daysDifference() }) // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
  142. })
  143. if (!data) {
  144. message.error('查询不到审批详情信息!')
  145. return
  146. }
  147. // 获取审批节点,显示 Timeline 的数据
  148. activityNodes.value = data.activityNodes
  149. // 获取发起人自选的任务
  150. startUserSelectTasks.value = data.activityNodes?.filter(
  151. (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
  152. )
  153. // 恢复之前的选择审批人
  154. if (startUserSelectTasks.value?.length > 0) {
  155. for (const node of startUserSelectTasks.value) {
  156. if (
  157. tempStartUserSelectAssignees.value[node.id] &&
  158. tempStartUserSelectAssignees.value[node.id].length > 0
  159. ) {
  160. startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
  161. } else {
  162. startUserSelectAssignees.value[node.id] = []
  163. }
  164. }
  165. }
  166. } finally {
  167. }
  168. }
  169. /** 审批相关:选择发起人 */
  170. const selectUserConfirm = (id: string, userList: any[]) => {
  171. startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
  172. }
  173. // 计算天数差
  174. // TODO @小北:可以搞到 formatTime 里面去,然后看看 dayjs 里面有没有现成的方法,或者辅助计算的方法。
  175. const daysDifference = () => {
  176. const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
  177. const diffTime = Math.abs(Number(formData.value.endTime) - Number(formData.value.startTime))
  178. return Math.floor(diffTime / oneDay)
  179. }
  180. /** 获取请假数据,用于重新发起时自动填充 */
  181. const getLeaveData = async (id: number) => {
  182. try {
  183. formLoading.value = true
  184. const data = await LeaveApi.getLeave(id)
  185. if (!data) {
  186. message.error('重新发起请假失败,原因:请假数据不存在')
  187. return
  188. }
  189. formData.value = {
  190. type: data.type,
  191. reason: data.reason,
  192. startTime: data.startTime,
  193. endTime: data.endTime
  194. }
  195. } finally {
  196. formLoading.value = false
  197. }
  198. }
  199. /** 初始化 */
  200. onMounted(async () => {
  201. // TODO @小北:这里可以简化,统一通过 getApprovalDetail 处理么?
  202. const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
  203. undefined,
  204. processDefineKey
  205. )
  206. if (!processDefinitionDetail) {
  207. message.error('OA 请假的流程模型未配置,请检查!')
  208. return
  209. }
  210. processDefinitionId.value = processDefinitionDetail.id
  211. startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
  212. // 如果有业务编号,说明是重新发起,需要加载原有数据
  213. if (query.id) {
  214. await getLeaveData(Number(query.id))
  215. }
  216. // 审批相关:加载最新的审批详情,主要用于节点预测
  217. await getApprovalDetail()
  218. })
  219. /** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
  220. watch(
  221. formData.value,
  222. (newValue, oldValue) => {
  223. if (!oldValue) {
  224. return
  225. }
  226. if (newValue && Object.keys(newValue).length > 0) {
  227. // 记录之前的节点审批人
  228. tempStartUserSelectAssignees.value = startUserSelectAssignees.value
  229. startUserSelectAssignees.value = {}
  230. // 加载最新的审批详情,主要用于节点预测
  231. getApprovalDetail()
  232. }
  233. },
  234. {
  235. immediate: true
  236. }
  237. )
  238. </script>