useFlow.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /**
  2. * useFlow - 业务侧流程集成 Composable
  3. *
  4. * 设计目标:业务组件只需关心业务逻辑,无需关心流程引擎细节
  5. *
  6. * 使用示例:
  7. * const { startFlow, flowStatus, approvalHistory, isRunning, canWithdraw } = useFlow('leave_apply', businessKey)
  8. * await startFlow({ title: '张三请假申请', variables: { days: 3 } })
  9. */
  10. import { computed, readonly, ref, watch } from 'vue'
  11. import flowApi from '@/api/flow'
  12. import { useUserStore } from '@/store'
  13. /**
  14. * 业务流程集成
  15. * @param {string} processKey - 流程模型 Key(对应 FlowModel.modelKey)
  16. * @param {import('vue').Ref<string>|string} businessKeyRef - 业务唯一标识(可传 ref 或字符串)
  17. */
  18. export function useFlow(processKey, businessKeyRef) {
  19. const userStore = useUserStore()
  20. // ======= 状态 =======
  21. const statusData = ref(null) // FlowBusiness 业务状态数据
  22. const loading = ref(false)
  23. const submitting = ref(false)
  24. // ======= 计算属性 =======
  25. const flowStatus = computed(() => statusData.value?.status || 'none')
  26. /** 流程是否在运行中 */
  27. const isRunning = computed(() => flowStatus.value === 'running')
  28. /** 流程是否已结束(通过/驳回/取消) */
  29. const isFinished = computed(() =>
  30. ['approved', 'rejected', 'canceled'].includes(flowStatus.value),
  31. )
  32. /** 是否可以发起(未发起或已结束才能重新发起) */
  33. const canStart = computed(() =>
  34. flowStatus.value === 'none' || flowStatus.value === 'canceled',
  35. )
  36. /** 发起人是否可以撤回(运行中才能撤回) */
  37. const canWithdraw = computed(() =>
  38. isRunning.value && statusData.value?.applyUserId === userStore.userId,
  39. )
  40. /** 状态文本 */
  41. const statusText = computed(() => {
  42. const map = {
  43. none: '未发起',
  44. draft: '草稿',
  45. running: '审批中',
  46. approved: '已通过',
  47. rejected: '已驳回',
  48. canceled: '已取消',
  49. }
  50. return map[flowStatus.value] || '未知'
  51. })
  52. /** 状态徽标类型(对应 Naive UI NTag type) */
  53. const statusTagType = computed(() => {
  54. const map = {
  55. none: 'default',
  56. draft: 'default',
  57. running: 'warning',
  58. approved: 'success',
  59. rejected: 'error',
  60. canceled: 'default',
  61. }
  62. return map[flowStatus.value] || 'default'
  63. })
  64. // ======= 获取业务 Key =======
  65. function getBusinessKey() {
  66. if (typeof businessKeyRef === 'string')
  67. return businessKeyRef
  68. if (businessKeyRef?.value)
  69. return businessKeyRef.value
  70. return null
  71. }
  72. // ======= 核心方法 =======
  73. /**
  74. * 刷新流程状态
  75. */
  76. async function refreshStatus() {
  77. const businessKey = getBusinessKey()
  78. if (!businessKey)
  79. return
  80. loading.value = true
  81. try {
  82. const res = await flowApi.getProcessStatus(businessKey)
  83. if (res.code === 200 && res.data) {
  84. statusData.value = res.data
  85. }
  86. else {
  87. statusData.value = null
  88. }
  89. }
  90. catch (e) {
  91. statusData.value = null
  92. }
  93. finally {
  94. loading.value = false
  95. }
  96. }
  97. /**
  98. * 发起流程
  99. * @param {object} options
  100. * @param {string} options.title 流程标题(必填)
  101. * @param {string} [options.businessType] 业务类型(可选,用于分类过滤)
  102. * @param {object} [options.variables] 流程变量(可选)
  103. * @returns {Promise<{success: boolean, processInstanceId: string}>}
  104. */
  105. async function startFlow({ title, businessType, variables = {} } = {}) {
  106. const businessKey = getBusinessKey()
  107. if (!businessKey)
  108. throw new Error('businessKey 不能为空')
  109. if (!title)
  110. throw new Error('流程标题 title 不能为空')
  111. submitting.value = true
  112. try {
  113. const res = await flowApi.startProcess(processKey, {
  114. businessKey,
  115. businessType: businessType || processKey,
  116. title,
  117. variables,
  118. userId: userStore.userId,
  119. userName: userStore.userInfo?.nickName || userStore.userInfo?.userName,
  120. deptId: userStore.userInfo?.deptId,
  121. deptName: userStore.userInfo?.deptName,
  122. })
  123. if (res.code === 200) {
  124. await refreshStatus()
  125. return { success: true, processInstanceId: res.data }
  126. }
  127. return { success: false, message: res.message }
  128. }
  129. finally {
  130. submitting.value = false
  131. }
  132. }
  133. /**
  134. * 撤回流程(发起人在运行中撤回)
  135. * @param {string} [reason] 撤回原因
  136. */
  137. async function withdrawFlow(reason = '') {
  138. const businessKey = getBusinessKey()
  139. if (!businessKey)
  140. return
  141. submitting.value = true
  142. try {
  143. const res = await flowApi.withdrawProcess({
  144. processInstanceId: statusData.value?.processInstanceId,
  145. userId: userStore.userId,
  146. reason,
  147. })
  148. if (res.code === 200) {
  149. await refreshStatus()
  150. return { success: true }
  151. }
  152. return { success: false, message: res.message }
  153. }
  154. finally {
  155. submitting.value = false
  156. }
  157. }
  158. /**
  159. * 终止流程(管理员强制终止)
  160. * @param {string} reason 终止原因
  161. */
  162. async function terminateFlow(reason = '') {
  163. const businessKey = getBusinessKey()
  164. if (!businessKey)
  165. return
  166. submitting.value = true
  167. try {
  168. const res = await flowApi.terminateProcess(businessKey, {
  169. userId: userStore.userId,
  170. reason,
  171. })
  172. if (res.code === 200) {
  173. await refreshStatus()
  174. return { success: true }
  175. }
  176. return { success: false, message: res.message }
  177. }
  178. finally {
  179. submitting.value = false
  180. }
  181. }
  182. /**
  183. * 获取流程审批历史
  184. * @returns {Promise<Array>}
  185. */
  186. async function getApprovalHistory() {
  187. if (!statusData.value?.processInstanceId)
  188. return []
  189. try {
  190. const res = await flowApi.getProcessComments(statusData.value.processInstanceId)
  191. if (res.code === 200)
  192. return res.data || []
  193. }
  194. catch (e) {
  195. console.error('获取审批历史失败:', e)
  196. }
  197. return []
  198. }
  199. /**
  200. * 获取流程图信息(节点高亮)
  201. * @returns {Promise<object|null>}
  202. */
  203. async function getDiagramInfo() {
  204. if (!statusData.value?.processInstanceId)
  205. return null
  206. try {
  207. const res = await flowApi.getProcessDiagramInfo(statusData.value.processInstanceId)
  208. if (res.code === 200)
  209. return res.data
  210. }
  211. catch (e) {
  212. console.error('获取流程图失败:', e)
  213. }
  214. return null
  215. }
  216. // businessKey 变化时自动刷新状态
  217. if (businessKeyRef && typeof businessKeyRef !== 'string') {
  218. watch(businessKeyRef, (newKey) => {
  219. if (newKey)
  220. refreshStatus()
  221. }, { immediate: true })
  222. }
  223. else {
  224. // 字符串形式,立即初始化
  225. const bk = getBusinessKey()
  226. if (bk)
  227. refreshStatus()
  228. }
  229. return {
  230. // 状态
  231. statusData: readonly(statusData),
  232. flowStatus,
  233. statusText,
  234. statusTagType,
  235. loading: readonly(loading),
  236. submitting: readonly(submitting),
  237. // 计算属性
  238. isRunning,
  239. isFinished,
  240. canStart,
  241. canWithdraw,
  242. // 方法
  243. startFlow,
  244. withdrawFlow,
  245. terminateFlow,
  246. refreshStatus,
  247. getApprovalHistory,
  248. getDiagramInfo,
  249. }
  250. }
  251. /**
  252. * 用于审批页面(任务处理人视角)的 composable
  253. * 封装通过/驳回/转办等操作
  254. */
  255. export function useFlowTask() {
  256. const userStore = useUserStore()
  257. const processing = ref(false)
  258. /**
  259. * 审批通过
  260. */
  261. async function approve(taskId, comment, variables = {}) {
  262. processing.value = true
  263. try {
  264. const res = await flowApi.approveTask({
  265. taskId,
  266. userId: userStore.userId,
  267. comment,
  268. variables,
  269. })
  270. return { success: res.code === 200, message: res.message }
  271. }
  272. catch (e) {
  273. return { success: false, message: e.message }
  274. }
  275. finally {
  276. processing.value = false
  277. }
  278. }
  279. /**
  280. * 审批驳回
  281. */
  282. async function reject(taskId, comment) {
  283. processing.value = true
  284. try {
  285. const res = await flowApi.rejectTask({
  286. taskId,
  287. userId: userStore.userId,
  288. comment,
  289. })
  290. return { success: res.code === 200, message: res.message }
  291. }
  292. catch (e) {
  293. return { success: false, message: e.message }
  294. }
  295. finally {
  296. processing.value = false
  297. }
  298. }
  299. /**
  300. * 转办
  301. */
  302. async function delegate(taskId, targetUserId, comment = '') {
  303. processing.value = true
  304. try {
  305. const res = await flowApi.delegateTask({
  306. taskId,
  307. userId: userStore.userId,
  308. targetUserId,
  309. comment,
  310. })
  311. return { success: res.code === 200, message: res.message }
  312. }
  313. catch (e) {
  314. return { success: false, message: e.message }
  315. }
  316. finally {
  317. processing.value = false
  318. }
  319. }
  320. /**
  321. * 签收候选任务
  322. */
  323. async function claim(taskId) {
  324. processing.value = true
  325. try {
  326. const res = await flowApi.claimTask(taskId, userStore.userId)
  327. return { success: res.code === 200, message: res.message }
  328. }
  329. catch (e) {
  330. return { success: false, message: e.message }
  331. }
  332. finally {
  333. processing.value = false
  334. }
  335. }
  336. return {
  337. processing: readonly(processing),
  338. approve,
  339. reject,
  340. delegate,
  341. claim,
  342. }
  343. }