approvedContent.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <template>
  2. <div class="flow-detail-content">
  3. <div class="flow-detail-container">
  4. <!-- 值为空 -->
  5. <div v-if="!currentTaskRow.instanceId" class="flow-empty-detail-box">
  6. <el-empty description="暂无数据" />
  7. </div>
  8. <!-- 值不为空 -->
  9. <template v-if="currentTaskRow.instanceId">
  10. <!-- 1、头部信息 -->
  11. <div class="flow-status-stamp">
  12. <div class="flow-stamp-container">
  13. <FlowStatusStamp :status="currentTaskRow.instanceState" />
  14. </div>
  15. </div>
  16. <div class="flow-header-box">
  17. <div class="flow-no">编号:{{ currentTaskRow.instanceId }}</div>
  18. <div class="action-area">
  19. <div class="action-item"></div>
  20. </div>
  21. </div>
  22. <!-- 2、内容体 -->
  23. <div class="flow-detail-box">
  24. <!--头部-->
  25. <div class="header-box">
  26. <div class="summary-info">
  27. <div class="title">{{ currentTaskRow.processName }}</div>
  28. <FlowStatusTag :status="currentTaskRow.instanceState" />
  29. </div>
  30. <div class="initiator-info">
  31. <FlowNodeAvatar :name="currentTaskRow.createBy" />
  32. <div class="begin-time">{{ currentTaskRow.createTime }} 提交</div>
  33. </div>
  34. </div>
  35. <div class="area-divider"></div>
  36. <div class="scroll-wrap">
  37. <!-- 表单 -->
  38. <div v-loading="validateForm.loading" class="form-wrap">
  39. <!--
  40. <FormCreate v-show="validateForm.rule.length" v-model:api="validateForm.api" :option="validateForm.option" :rule="validateForm.rule" />
  41. -->
  42. <er-form-preview ref="EReditorRef" :is-show-complete-button="false" />
  43. <LeNoData v-if="!validateForm.rule.length" message="表单无数据" />
  44. </div>
  45. <div class="area-divider"></div>
  46. <!--审批流-->
  47. <el-timeline style="margin-left: 50px">
  48. <el-timeline-item v-for="active in activeData" :key="active.id" hollow :timestamp="active.local_timestamp">
  49. <template #dot>
  50. <FlowTypeDot :status="active.id ? 0 : 1" :type="active.taskType" :name="active.createBy" />
  51. </template>
  52. <div v-show="active.type === 0" class="timeline-box flex-1">
  53. <span style="color: #86909c; display: block; margin-bottom: 3px; padding-left: 4px">评论</span>
  54. <div class="flex flex-align-center">
  55. <div class="timeline-box-user flex-1">
  56. <span style="padding-left: 4px">{{ active.createBy }}</span>
  57. <div v-if="active.local_content" class="comment">
  58. <div class="comment-content">{{ active.local_content }}</div>
  59. </div>
  60. </div>
  61. <span class="timeline-box-date">{{ formatTimestamp(active.finishTime) }}</span>
  62. </div>
  63. </div>
  64. <div v-show="active.type !== 0" class="timeline-box flex-1">
  65. <span style="color: #86909c; display: block; margin-bottom: 3px; padding-left: 4px">{{ active.taskName }}</span>
  66. <div class="flex flex-align-center">
  67. <div class="timeline-box-user flex-1">
  68. <span v-if="active.id" style="padding-left: 4px">{{ active.createBy }}</span>
  69. <div style="display: flex; gap: 6px; margin-top: 3px">
  70. <FlowNodeAvatar v-for="nodeUser in active.local_nodeUserList" :key="nodeUser.id" :name="nodeUser.name" />
  71. <FlowNodeAvatar v-for="nodeRole in active.local_nodeRoleList" :key="nodeRole.id" :name="nodeRole.name" />
  72. </div>
  73. <div v-if="active.local_content" class="comment">
  74. <div class="comment-content">{{ active.local_content }}</div>
  75. </div>
  76. </div>
  77. <span class="timeline-box-date">{{ formatTimestamp(active.finishTime) }}</span>
  78. </div>
  79. </div>
  80. </el-timeline-item>
  81. </el-timeline>
  82. </div>
  83. </div>
  84. <!-- 3、底部操作按钮 审批拒绝 强制终止不展示操作按钮-->
  85. <!--
  86. 1、已审批的任务不显示操作按钮
  87. 2、我的申请显示撤回按钮
  88. 3、认领任务显示认领按钮
  89. 4、我收到的任务显示评论
  90. -->
  91. <div v-if="currentTaskRow.instanceState === 0" class="flow-actions">
  92. <el-button :icon="ChatLineSquare" @click="openComment('reviewVisible', 'review')">评论</el-button>
  93. <template v-if="currentTaskType !== 'myReceived' || currentTaskType !== 'approved'">
  94. <el-button
  95. v-if="currentTaskType === 'pendingApproval'"
  96. :icon="Check"
  97. type="primary"
  98. @click="openComment('consentOrRefuseVisible', 'agree')"
  99. >同意</el-button
  100. >
  101. <el-button
  102. v-if="currentTaskType === 'pendingApproval'"
  103. :icon="Close"
  104. type="danger"
  105. @click="openComment('consentOrRefuseVisible', 'reject')"
  106. >拒绝</el-button
  107. >
  108. <el-button v-if="currentTaskType === 'myApplication'" :icon="Close" @click="openComment('reviewVisible', 'revoke')">撤回</el-button>
  109. <el-button v-if="currentTaskType === 'pendingClaim'" :icon="Close" @click="claimTaskEv">认领</el-button>
  110. <el-dropdown v-if="currentTaskType === 'pendingApproval'" style="margin-left: 12px">
  111. <el-button :icon="More">更多</el-button>
  112. <template #dropdown>
  113. <el-dropdown-menu>
  114. <el-dropdown-item @click.native="openComment('deliverToReviewVisible')">
  115. <el-icon><DArrowLeft /></el-icon>
  116. 转交
  117. </el-dropdown-item>
  118. <el-dropdown-item @click.native="openComment('rollbackVisible')">
  119. <el-icon><Switch /></el-icon>
  120. 回退
  121. </el-dropdown-item>
  122. <el-dropdown-item @click.native="openComment('addSignVisible')">
  123. <el-icon><Plus /></el-icon>
  124. 加签
  125. </el-dropdown-item>
  126. <el-dropdown-item v-if="false" @click.native="openComment('loseSignVisible')">
  127. <el-icon><Minus /></el-icon>
  128. 减签
  129. </el-dropdown-item>
  130. </el-dropdown-menu>
  131. </template>
  132. </el-dropdown>
  133. </template>
  134. </div>
  135. </template>
  136. </div>
  137. <!-- 评论弹窗-->
  138. <review-dialog
  139. v-if="reviewVisible"
  140. v-model="reviewVisible"
  141. :instance-id="currentTaskRow.instanceId"
  142. :task-id="taskId"
  143. :current-type="currentType"
  144. @success-cb="closeDetailEv"
  145. ></review-dialog>
  146. <!-- 加签弹窗 -->
  147. <add-sign-dialog v-if="addSignVisible" v-model="addSignVisible" :task-id="taskId" @success-cb="closeDetailEv"></add-sign-dialog>
  148. <!-- 同意或拒绝弹窗 -->
  149. <consent-or-refuse-dialog
  150. v-if="consentOrRefuseVisible"
  151. v-model="consentOrRefuseVisible"
  152. :task-id="taskId"
  153. :current-type="currentType"
  154. :form-data="currentFormData"
  155. @success-cb="closeDetailEv"
  156. ></consent-or-refuse-dialog>
  157. <!-- 转交审批弹窗 -->
  158. <deliver-to-review-dialog
  159. v-if="deliverToReviewVisible"
  160. v-model="deliverToReviewVisible"
  161. :task-id="taskId"
  162. @success-cb="closeDetailEv"
  163. ></deliver-to-review-dialog>
  164. <!-- 减签弹窗 -->
  165. <lose-sign-dialog v-if="loseSignVisible" v-model="loseSignVisible" :task-id="taskId" @success-cb="closeDetailEv"></lose-sign-dialog>
  166. <!-- 回退弹窗 -->
  167. <rollback-dialog v-if="rollbackVisible" v-model="rollbackVisible" :task-id="taskId" @success-cb="closeDetailEv"></rollback-dialog>
  168. </div>
  169. </template>
  170. <script setup>
  171. import useTaskProcessStore from '@/store/modules/taskProcess'
  172. import { computed, ref, onMounted, nextTick, watch } from 'vue'
  173. import FlowStatusStamp from '@/components/Flow/FlowStatusStamp.vue'
  174. import FlowStatusTag from '@/components/Flow/FlowStatusTag.vue'
  175. import FlowNodeAvatar from '@/components/Flow/FlowNodeAvatar.vue'
  176. import FlowTypeDot from '@/components/Flow/FlowTypeDot.vue'
  177. import { ChatLineSquare, Check, Close, Switch, DArrowLeft, Plus, Minus, More } from '@element-plus/icons-vue'
  178. import { processTaskApprovalInfo, processClaimTaskApi } from '@/api/flow/processTask'
  179. import { formatTimestamp } from '@/utils/datetime'
  180. import ReviewDialog from './reviewDialog'
  181. import AddSignDialog from './addSignDialog'
  182. import ConsentOrRefuseDialog from './consentOrRefuseDialog'
  183. import DeliverToReviewDialog from './deliverToReviewDialog'
  184. import LoseSignDialog from './loseSignDialog'
  185. import RollbackDialog from './rollbackDialog'
  186. import viewForm from '@/utils/form'
  187. import { storeToRefs } from 'pinia'
  188. import { ElMessage, ElMessageBox } from 'element-plus'
  189. import { erFormPreview } from 'everright-formeditor'
  190. defineProps({
  191. /**
  192. * pendingApproval 待审批
  193. * myApplication 我的申请
  194. * myReceived 我收到的
  195. * pendingClaim 认领任务
  196. * approved 已审批
  197. */
  198. currentTaskType: {
  199. type: String,
  200. default: ''
  201. }
  202. })
  203. // store值
  204. const taskProcessInfo = useTaskProcessStore()
  205. const { currentTaskRow } = storeToRefs(taskProcessInfo)
  206. // 各种操作弹窗显示隐藏 start
  207. const reviewVisible = ref(false)
  208. const addSignVisible = ref(false)
  209. const consentOrRefuseVisible = ref(false)
  210. const deliverToReviewVisible = ref(false)
  211. const loseSignVisible = ref(false)
  212. const rollbackVisible = ref(false)
  213. // 各种操作弹窗显示隐藏 end
  214. const activeData = ref([])
  215. const currentType = ref(null)
  216. const currentFormData = ref({})
  217. // 当前form 表单数据字符串
  218. let cur_processForm_str =
  219. '{"formStructure":{"list":[{"type":"inline","columns":["G87FL_bjPlVjZjSd2P3SE"],"style":{},"id":"XqyjpOONTVRdA9XeA667G","key":"inline_XqyjpOONTVRdA9XeA667G"},{"type":"inline","columns":["fg_137feCy7pJSJs9sOaU"],"style":{},"id":"RsbiEBhHked3LYw_GDwYt","key":"inline_RsbiEBhHked3LYw_GDwYt"}],"config":{"isSync":true,"pc":{"size":"default","labelPosition":"left","completeButton":{"text":"提交","color":"","backgroundColor":""}},"mobile":{"labelPosition":"left","completeButton":{"text":"提交","color":"","backgroundColor":""}}},"fields":[{"type":"input","label":"姓名","icon":"input","key":"input_name","id":"G87FL_bjPlVjZjSd2P3SE","options":{"clearable":true,"isShowWordLimit":false,"renderType":1,"disabled":false,"showPassword":false,"defaultValue":"","placeholder":"请输入","labelWidth":100,"isShowLabel":true,"required":false,"min":null,"max":null},"style":{"width":{"pc":"100%","mobile":"100%"}}},{"type":"input","label":"别名","icon":"input","key":"input_alias","id":"fg_137feCy7pJSJs9sOaU","options":{"clearable":true,"isShowWordLimit":false,"renderType":1,"disabled":false,"showPassword":false,"defaultValue":"","placeholder":"请输入","labelWidth":100,"isShowLabel":true,"required":false,"min":null,"max":null},"style":{"width":{"pc":"100%","mobile":"100%"}}}],"data":{},"logic":{}},"formData":{"input_name":"罗小胖","input_alias":"luoxiaopang"}}'
  220. const EReditorRef = ref()
  221. const taskId = computed(() => {
  222. return currentTaskRow.value.taskId || ''
  223. })
  224. const FormCreate = viewForm.$form()
  225. const validateForm = ref({
  226. api: {},
  227. option: {
  228. submitBtn: false
  229. },
  230. rule: [],
  231. loading: false
  232. })
  233. /**
  234. * 详情按钮各个操作弹窗
  235. * @param visibleType 评论 拒绝 同意等
  236. */
  237. const openComment = async (visibleType, item) => {
  238. switch (visibleType) {
  239. case 'reviewVisible':
  240. currentType.value = item
  241. reviewVisible.value = !reviewVisible.value
  242. break
  243. case 'addSignVisible':
  244. addSignVisible.value = !addSignVisible.value
  245. break
  246. case 'consentOrRefuseVisible':
  247. currentFormData.value = {}
  248. // 点击同意
  249. let bool = true
  250. if (item === 'agree') {
  251. const api = validateForm.value.api
  252. bool = await api.validate((valid, fail) => {
  253. if (valid) {
  254. // 表单验证通过
  255. const values = api.formData()
  256. const processForm = JSON.parse(cur_processForm_str)
  257. processForm.forEach(v => {
  258. // 填写的数据存储(local_: 本地数据处理标识)
  259. v.local_value = values[v.field]
  260. })
  261. console.warn(processForm, 'processForm')
  262. // 流程表单JSON内容 & local_value 保存
  263. currentFormData.value = { processForm: JSON.stringify(processForm) }
  264. }
  265. })
  266. }
  267. if (!bool) return
  268. currentType.value = item
  269. consentOrRefuseVisible.value = !consentOrRefuseVisible.value
  270. break
  271. case 'deliverToReviewVisible':
  272. deliverToReviewVisible.value = !deliverToReviewVisible.value
  273. break
  274. case 'loseSignVisible':
  275. loseSignVisible.value = !loseSignVisible.value
  276. break
  277. case 'rollbackVisible':
  278. rollbackVisible.value = !rollbackVisible.value
  279. break
  280. }
  281. }
  282. /**
  283. * 获取taskId对应的详情
  284. */
  285. const getTaskDetail = () => {
  286. const cur = currentTaskRow.value || {}
  287. // 提交的表单 数据展示
  288. validateForm.value.loading = true
  289. processTaskApprovalInfo({
  290. taskId: cur.taskId,
  291. instanceId: cur.instanceId,
  292. instanceState: cur.instanceState
  293. })
  294. .then(data => {
  295. const activeList = data.processApprovals
  296. activeList.forEach(v => {
  297. v.local_timestamp = v.id && formatTimestamp(v.createTime)
  298. const _content = v.content // JSON.parse(v.content || '{}')
  299. v.local_nodeUserList = _content.nodeUserList || []
  300. v.local_nodeRoleList = _content.nodeRoleList || []
  301. v.local_content = _content?.opinion
  302. })
  303. activeData.value = activeList
  304. console.log(data, 'data.......')
  305. // validateForm.value.origin = data
  306. try {
  307. /*descItemsData.value.list = JSON.parse(data.formContent).map(item => {
  308. const showLabel = item.title
  309. let showValue = item.local_value
  310. const options = item.options
  311. if (Array.isArray(options) && showValue !== undefined) {
  312. if (Array.isArray(showValue)) {
  313. showValue = showValue.reduce(val => {
  314. const cur = options.find(option => option.value === val)
  315. return cur?.label || val
  316. }, [])
  317. } else {
  318. const cur = options.find(option => option.value === showValue)
  319. showValue = cur?.label || showValue
  320. }
  321. }
  322. return {
  323. showLabel,
  324. showValue
  325. }
  326. })*/
  327. const { formStructure, formData } = JSON.parse(cur_processForm_str)
  328. EReditorRef.value.setData(formStructure, formData)
  329. return
  330. const forms = JSON.parse(data.formContent)
  331. cur_processForm_str = data.formContent
  332. if (Array.isArray(forms)) {
  333. validateForm.value.rule = forms
  334. const api = validateForm.value.api
  335. api.setValue(
  336. forms.reduce((obj, item) => {
  337. obj[item.field] = item.local_value
  338. return obj
  339. }, {})
  340. )
  341. api.nextTick(() => {
  342. // 是否有编辑权限 操作
  343. api.disabled(false /*true*/)
  344. })
  345. }
  346. } catch (e) {
  347. console.error('解析 descItems 数据出现问题', e)
  348. // descItemsData.value.list = []
  349. validateForm.value.rule = []
  350. }
  351. })
  352. .finally(() => {
  353. validateForm.value.loading = false
  354. })
  355. }
  356. /**
  357. * 详情页面操作按钮回调
  358. */
  359. const closeDetailEv = () => {
  360. ElMessage({
  361. message: '操作成功',
  362. type: 'success'
  363. })
  364. setTimeout(() => {
  365. // 如果这里有表单,是否要把所有表单的内容进行存储,存储完毕后,才能关闭这个详情,刷新左侧的列表 todo
  366. if (currentType.value === 'review') {
  367. // 评论按钮,不要刷新左侧的列表,只更新右侧的详情即可
  368. getTaskDetail()
  369. return
  370. }
  371. taskProcessInfo.refresh = true
  372. taskProcessInfo.setCurrentTaskRow({})
  373. }, 1000)
  374. }
  375. /** 认领任务 **/
  376. const claimTaskEv = async () => {
  377. ElMessageBox.confirm('确定认领当前审批流程?', '提示', {
  378. confirmButtonText: '确认',
  379. cancelButtonText: '取消',
  380. type: 'warning',
  381. buttonSize: 'default'
  382. })
  383. .then(async () => {
  384. const data = await processClaimTaskApi(currentTaskRow.value.taskId)
  385. if (!data) return
  386. closeDetailEv()
  387. })
  388. .catch(() => {
  389. console.log('取消')
  390. })
  391. }
  392. /**
  393. * 监听同级子组件的instanceId的值变化 这里可能也有实例Id
  394. * 1、监听instanceId的值变化,如果值有变化,则重新获取审批详情
  395. * 2、如果instanceId没有值,则不请求接口,暂时暂无数据img
  396. */
  397. watch(
  398. () => currentTaskRow.value.instanceId,
  399. (nValue, oValue) => {
  400. if (!nValue) return
  401. nextTick(() => {
  402. getTaskDetail()
  403. })
  404. }
  405. )
  406. </script>
  407. <style scoped lang="scss">
  408. .flow-detail-content {
  409. height: 100%;
  410. overflow: hidden;
  411. background: var(--el-bg-color);
  412. border-radius: 6px;
  413. flex: auto;
  414. .flow-detail-container {
  415. display: flex;
  416. flex-direction: column;
  417. position: relative;
  418. height: 100%;
  419. overflow: hidden;
  420. }
  421. // 通过、不通过样式
  422. .flow-status-stamp {
  423. position: absolute;
  424. right: 10px;
  425. top: 10px;
  426. z-index: 99;
  427. }
  428. // 头部
  429. .flow-header-box {
  430. font-weight: 400;
  431. font-size: 13px;
  432. border-bottom: 1px solid var(--el-border-color);
  433. padding: 0 20px;
  434. height: 39px;
  435. display: flex;
  436. align-items: center;
  437. justify-content: space-between;
  438. color: var(--el-color-info);
  439. .action-area {
  440. display: flex;
  441. gap: 4px;
  442. .action-item {
  443. cursor: pointer;
  444. padding: 4px;
  445. border-radius: 6px;
  446. width: fit-content;
  447. height: fit-content;
  448. }
  449. }
  450. }
  451. // 内容体
  452. .flow-detail-box {
  453. //height: calc(100% - 92px);
  454. //overflow: hidden;
  455. //overflow-y: auto;
  456. flex: 1;
  457. min-height: 0;
  458. padding: 0 20px;
  459. display: flex;
  460. flex-direction: column;
  461. height: 100%;
  462. .header-box {
  463. display: flex;
  464. flex-direction: column;
  465. justify-content: center;
  466. // padding-top: 20px;
  467. .summary-info {
  468. display: flex;
  469. align-items: center;
  470. padding-top: 10px;
  471. font-size: 24px;
  472. .title {
  473. font-family: PingFangSC-Semibold, PingFang SC;
  474. color: var(--el-text-color-primary);
  475. }
  476. }
  477. .initiator-info {
  478. display: flex;
  479. align-items: center;
  480. margin-top: 16px;
  481. .begin-time {
  482. margin-left: 16px;
  483. font-weight: 350;
  484. color: var(--el-text-color-placeholder);
  485. font-size: 13px;
  486. -webkit-user-select: none;
  487. user-select: none;
  488. }
  489. }
  490. }
  491. .area-divider {
  492. border-bottom: 1px solid var(--el-border-color);
  493. margin: 20px 0;
  494. //position: relative;
  495. }
  496. .scroll-wrap {
  497. overflow: hidden;
  498. overflow-y: auto;
  499. flex: 1;
  500. }
  501. }
  502. // 底部
  503. .flow-actions {
  504. display: flex;
  505. align-items: center;
  506. justify-content: flex-end;
  507. height: 52px;
  508. //border-top: 1px solid var(--color-neutral-3);
  509. border-top: 1px solid var(--el-border-color-light);
  510. background: var(--el-bg-color);
  511. padding: 0 20px;
  512. }
  513. }
  514. .comment-content {
  515. user-select: none;
  516. margin-top: 4px;
  517. padding: 8px;
  518. border-radius: 4px;
  519. background-color: var(--el-bg-color-page);
  520. }
  521. .timeline-box {
  522. margin-left: 5px;
  523. }
  524. :deep(.el-timeline-item__timestamp) {
  525. margin-left: 6px;
  526. }
  527. </style>