index.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <template>
  2. <view class="yd-page-container">
  3. <!-- 顶部导航栏 -->
  4. <wd-navbar
  5. title="审批详情"
  6. left-arrow placeholder safe-area-inset-top fixed
  7. @click-left="handleBack"
  8. />
  9. <!-- 区域:流程信息(基本信息) -->
  10. <view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
  11. <view class="p-24rpx">
  12. <!-- 标题和状态 -->
  13. <view class="mb-16rpx flex items-center justify-between">
  14. <text class="text-32rpx text-[#333] font-bold">{{ processInstance.name }}</text>
  15. <wd-tag :type="getStatusType(processInstance.status)">
  16. {{ getStatusText(processInstance.status) }}
  17. </wd-tag>
  18. </view>
  19. <!-- 发起人信息 -->
  20. <view class="flex items-center">
  21. <view class="mr-12rpx h-64rpx w-64rpx flex items-center justify-center rounded-full bg-[#1890ff] text-white">
  22. {{ processInstance.startUser?.nickname?.[0] || '?' }}
  23. </view>
  24. <view>
  25. <text class="text-28rpx text-[#333]">{{ processInstance.startUser?.nickname }}</text>
  26. <text v-if="processInstance.startUser?.deptName" class="ml-8rpx text-24rpx text-[#999]">
  27. {{ processInstance.startUser?.deptName }}
  28. </text>
  29. </view>
  30. </view>
  31. <!-- 提交时间 -->
  32. <view class="mt-16rpx text-24rpx text-[#999]">
  33. 提交于 {{ formatDateTime(processInstance.startTime) }}
  34. </view>
  35. </view>
  36. </view>
  37. <!-- 区域:审批详情(表单) -->
  38. <!-- TODO @jason:看看 idea 告警,怎么优化下 -->
  39. <FormDetail :process-definition="processDefinition" :process-instance="processInstance" />
  40. <!-- 区域:审批记录 TODO @jason:抽成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/detail/modules/task-list.vue -->
  41. <view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
  42. <view class="p-24rpx">
  43. <view class="mb-16rpx flex items-center justify-between">
  44. <text class="text-28rpx text-[#333] font-bold">审批记录</text>
  45. <!-- TODO @AI:去掉 orderAsc,不要 toggleOrder -->
  46. <wd-icon :name="orderAsc ? 'arrow-up' : 'arrow-down'" size="32rpx" @click="toggleOrder" />
  47. </view>
  48. <!-- 任务列表 -->
  49. <view v-for="(task, index) in sortedTasks" :key="task.id || index" class="relative pb-24rpx pl-40rpx">
  50. <!-- 时间线 -->
  51. <view
  52. class="absolute left-12rpx top-8rpx h-16rpx w-16rpx rounded-full"
  53. :class="getTaskDotClass(task)"
  54. />
  55. <view v-if="index < sortedTasks.length - 1" class="absolute bottom-0 left-18rpx top-28rpx w-2rpx bg-[#e5e5e5]" />
  56. <!-- 任务内容 -->
  57. <view>
  58. <text class="text-28rpx text-[#333] font-bold">{{ task.name }}</text>
  59. <view v-if="task.assigneeUser" class="mt-8rpx flex items-center">
  60. <view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
  61. {{ task.assigneeUser?.nickname?.[0] || '?' }}
  62. </view>
  63. <view class="flex-1">
  64. <view class="flex items-center justify-between">
  65. <view class="flex items-center">
  66. <text class="text-26rpx text-[#333]">{{ task.assigneeUser?.nickname }}</text>
  67. <text v-if="task.assigneeUser?.deptName" class="ml-8rpx text-22rpx text-[#999]">
  68. {{ task.assigneeUser?.deptName }}
  69. </text>
  70. </view>
  71. <text v-if="task.endTime" class="text-22rpx text-[#999]">{{ formatPast(task.endTime) }}</text>
  72. </view>
  73. <view class="mt-4rpx flex items-center">
  74. <text :class="getStatusTextClass(task.status)" class="text-24rpx">
  75. {{ getStatusText(task.status) }}
  76. </text>
  77. <text v-if="task.reason" class="ml-8rpx text-24rpx text-[#666]">{{ task.reason }}</text>
  78. </view>
  79. </view>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </view>
  85. <!-- TODO 待开发:区域:流程评论 -->
  86. <!-- 区域:底部操作栏 TODO @jason:抽成类似:/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/detail/modules/operation-button.vue -->
  87. <view v-if="runningTask" class="yd-detail-footer">
  88. <view class="yd-detail-footer-actions">
  89. <wd-button type="error" plain class="flex-1" @click="handleReject">
  90. 拒绝
  91. </wd-button>
  92. <wd-button type="primary" class="flex-1" @click="handleApprove">
  93. 同意
  94. </wd-button>
  95. </view>
  96. <!-- TODO @jason:审批通过、不通过:缺少签名、选择审批人 -->
  97. <!-- TODO @jason:取消流程、重新发起 -->
  98. <!-- TODO @jason:抄送、转派、委派、退回 -->
  99. <!-- TODO @jason:加签、减签 -->
  100. </view>
  101. </view>
  102. </template>
  103. <script lang="ts" setup>
  104. import type { ProcessDefinition, ProcessInstance } from '@/api/bpm/processInstance'
  105. import type { Task } from '@/api/bpm/task'
  106. import { onLoad } from '@dcloudio/uni-app'
  107. import { computed, ref } from 'vue'
  108. import { useToast } from 'wot-design-uni'
  109. import { getApprovalDetail } from '@/api/bpm/processInstance'
  110. import { getTaskListByProcessInstanceId } from '@/api/bpm/task'
  111. import { useUserStore } from '@/store'
  112. import { navigateBackPlus } from '@/utils'
  113. import { formatDateTime, formatPast } from '@/utils/date'
  114. import FormDetail from './components/form-detail.vue'
  115. definePage({
  116. style: {
  117. navigationBarTitleText: '',
  118. navigationStyle: 'custom',
  119. },
  120. })
  121. const userStore = useUserStore()
  122. const toast = useToast()
  123. const processInstanceId = ref('')
  124. const processInstance = ref<Partial<ProcessInstance>>({})
  125. const processDefinition = ref<Partial<ProcessDefinition>>({})
  126. const tasks = ref<Task[]>([])
  127. const orderAsc = ref(true)
  128. /** 当前用户需要处理的任务 */
  129. const runningTask = computed(() => {
  130. return tasks.value.find((task) => {
  131. // 待处理状态
  132. if (task.status !== 1 && task.status !== 6) {
  133. return false
  134. }
  135. // 当前用户是处理人
  136. return task.assigneeUser?.id === userStore.userInfo?.id
  137. })
  138. })
  139. /** 排序后的任务列表 */
  140. const sortedTasks = computed(() => {
  141. const list = [...tasks.value].filter(t => t.status !== 4) // 过滤已取消
  142. list.sort((a, b) => {
  143. if (a.endTime && b.endTime) {
  144. return orderAsc.value ? a.endTime - b.endTime : b.endTime - a.endTime
  145. }
  146. if (a.endTime) {
  147. return orderAsc.value ? -1 : 1
  148. }
  149. if (b.endTime) {
  150. return orderAsc.value ? 1 : -1
  151. }
  152. return orderAsc.value ? a.createTime - b.createTime : b.createTime - a.createTime
  153. })
  154. return list
  155. })
  156. /** 返回上一页 */
  157. function handleBack() {
  158. navigateBackPlus('/pages/bpm/index')
  159. }
  160. /** 切换排序 */
  161. function toggleOrder() {
  162. orderAsc.value = !orderAsc.value
  163. }
  164. /** 获取状态文本 */
  165. // TODO @jason:要有标签,和 vben 一样,盖章
  166. // TODO @jason:通过字典
  167. function getStatusText(status?: number) {
  168. const map: Record<number, string> = {
  169. 0: '待审批',
  170. 1: '审批中',
  171. 2: '审批通过',
  172. 3: '审批不通过',
  173. 4: '已取消',
  174. 5: '已退回',
  175. 6: '委派中',
  176. 7: '审批通过中',
  177. }
  178. return map[status ?? 0] || '待审批'
  179. }
  180. /** 获取状态标签类型 */
  181. function getStatusType(status?: number): 'default' | 'primary' | 'success' | 'warning' | 'danger' {
  182. if ([1, 6, 7].includes(status ?? 0)) {
  183. return 'primary'
  184. }
  185. if (status === 2) {
  186. return 'success'
  187. }
  188. if (status === 3) {
  189. return 'danger'
  190. }
  191. if (status === 4 || status === 5) {
  192. return 'warning'
  193. }
  194. return 'default'
  195. }
  196. /** 获取任务圆点样式 */
  197. // TODO @jason:看看又要对齐 vben
  198. function getTaskDotClass(task: Task) {
  199. if ([1, 6, 7].includes(task.status)) {
  200. return 'bg-[#1890ff]'
  201. }
  202. if (task.status === 2) {
  203. return 'bg-[#52c41a]'
  204. }
  205. if (task.status === 3) {
  206. return 'bg-[#ff4d4f]'
  207. }
  208. if (task.status === 5) {
  209. return 'bg-[#faad14]'
  210. }
  211. return 'bg-[#d9d9d9]'
  212. }
  213. /** 获取状态文本样式 */
  214. // TODO @jason:看看又要对齐 vben
  215. function getStatusTextClass(status: number) {
  216. if ([1, 6, 7].includes(status)) {
  217. return 'text-[#1890ff]'
  218. }
  219. if (status === 2) {
  220. return 'text-[#52c41a]'
  221. }
  222. if (status === 3) {
  223. return 'text-[#ff4d4f]'
  224. }
  225. if (status === 5) {
  226. return 'text-[#faad14]'
  227. }
  228. return 'text-[#999]'
  229. }
  230. /** 同意 */
  231. function handleApprove() {
  232. if (!runningTask.value) {
  233. return
  234. }
  235. uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?id=${runningTask.value.id}&pass=true` })
  236. }
  237. /** 拒绝 */
  238. function handleReject() {
  239. if (!runningTask.value) {
  240. return
  241. }
  242. uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?id=${runningTask.value.id}&pass=false` })
  243. }
  244. /** 加载流程实例 */
  245. async function loadProcessInstance() {
  246. const data = await getApprovalDetail({ processInstanceId: processInstanceId.value })
  247. if (!data || !data.processInstance) {
  248. toast.show('查询不到审批详情信息')
  249. return
  250. }
  251. processInstance.value = data.processInstance
  252. processDefinition.value = data.processDefinition || {}
  253. }
  254. /** 加载任务列表 */
  255. async function loadTasks() {
  256. tasks.value = await getTaskListByProcessInstanceId(processInstanceId.value)
  257. }
  258. /** 初始化 */
  259. onLoad(async (options) => {
  260. // TODO @jason:通过 props id 处理;
  261. if (!options?.id) {
  262. toast.show('参数错误')
  263. return
  264. }
  265. processInstanceId.value = options.id
  266. await Promise.all([loadProcessInstance(), loadTasks()])
  267. })
  268. </script>