index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <template>
  2. <wrapper
  3. v-loading="loading"
  4. class="c-step"
  5. fill
  6. margin
  7. padding
  8. background
  9. >
  10. <div class="l-flex__none l-flex--row c-sibling-item--v u-color--black">
  11. <i
  12. class="l-flex__none c-sibling-item o-icon o-icon--hover el-icon-arrow-left u-pointer u-bold"
  13. @click="onBack"
  14. />
  15. <span
  16. v-if="title"
  17. class="c-sibling-item near u-font-size--md u-bold"
  18. >
  19. {{ title }}
  20. </span>
  21. </div>
  22. <status-wrapper
  23. v-if="loading || error"
  24. :error="error"
  25. @click="getWorkflow"
  26. />
  27. <template v-else>
  28. <template v-if="audit === 0">
  29. <div class="l-flex__none c-sibling-item--v">
  30. <schema-table :schema="schema" />
  31. </div>
  32. <div class="l-flex__fill l-flex c-sibling-item--v c-step">
  33. <div class="l-flex__none l-flex--col c-step__column u-width--md">
  34. <schema-table :schema="deviceSchema" />
  35. </div>
  36. <div class="l-flex__none l-flex--col c-step__column u-width--lg">
  37. <schema-table :schema="timeSchema" />
  38. </div>
  39. <template v-if="hasAssets">
  40. <div class="l-flex__fill l-flex--col c-step__column">
  41. <schema-table :schema="assetSchema" />
  42. </div>
  43. <preview-dialog ref="previewDialog" />
  44. </template>
  45. </div>
  46. </template>
  47. <div
  48. v-if="audit === 1"
  49. class="has-padding"
  50. >
  51. <el-result
  52. icon="success"
  53. title="审核成功"
  54. >
  55. <template slot="extra">
  56. <el-link
  57. type="primary"
  58. @click="onBack"
  59. >
  60. 返回列表
  61. </el-link>
  62. </template>
  63. </el-result>
  64. </div>
  65. <material-dialog ref="materialDialog" />
  66. <confirm-dialog
  67. ref="rejectDialog"
  68. title="驳回"
  69. @confirm="onConfirmReject"
  70. >
  71. <div class="c-grid-form u-align-self--center">
  72. <span class="c-grid-form__label">
  73. 审核意见
  74. </span>
  75. <el-select
  76. v-model="review.type"
  77. placeholder="请选择"
  78. >
  79. <el-option
  80. v-for="option in reviewOptions"
  81. :key="option.label"
  82. :label="option.label"
  83. :value="option.value"
  84. />
  85. </el-select>
  86. <template v-if="review.type === 'reject'">
  87. <span class="c-grid-form__label u-required">
  88. 原因
  89. </span>
  90. <el-input
  91. v-model.trim="review.reason"
  92. type="textarea"
  93. placeholder="请填写驳回原因"
  94. maxlength="50"
  95. :rows="3"
  96. resize="none"
  97. show-word-limit
  98. />
  99. </template>
  100. </div>
  101. </confirm-dialog>
  102. </template>
  103. </wrapper>
  104. </template>
  105. <script>
  106. import { mapGetters } from 'vuex'
  107. import {
  108. State,
  109. Access,
  110. WorkflowState,
  111. WorkflowStateInfo,
  112. PublishTargetType,
  113. EventTarget,
  114. AssetTagInfo,
  115. AssetTypeInfo
  116. } from '@/constant.js'
  117. import { parseDuration } from '@/utils'
  118. import { parseDeploy } from '../utils.js'
  119. import {
  120. getWorkflow,
  121. resolveWorkflow,
  122. rejectWorkflow,
  123. getAssetsByProgramSnap,
  124. getQrCodeStatus,
  125. updateQrCodeStatus
  126. } from '../api.js'
  127. import {
  128. QRCODE_STATUS,
  129. getTenantAttribute
  130. } from '@/views/realm/settings/api.js'
  131. export default {
  132. name: 'WorkflowAudit',
  133. data () {
  134. return {
  135. loading: true,
  136. error: false,
  137. workflow: null,
  138. audit: -1,
  139. deviceSchema: {
  140. nonPagination: true,
  141. props: {
  142. size: 'small'
  143. },
  144. list: this.getDevices,
  145. cols: [
  146. { prop: 'deviceName', label: '上播设备', 'show-overflow-tooltip': false }
  147. ]
  148. },
  149. timeSchema: {
  150. nonPagination: true,
  151. props: {
  152. size: 'small'
  153. },
  154. list: this.getTimes,
  155. cols: [
  156. { prop: 'time', label: '上播时间', 'show-overflow-tooltip': false }
  157. ]
  158. },
  159. assetSchema: {
  160. nonPagination: true,
  161. props: {
  162. size: 'small'
  163. },
  164. list: this.getAssets,
  165. cols: [
  166. { prop: 'tagInfo', label: '分类', width: 72, align: 'center' },
  167. { prop: 'typeInfo', label: '资源', width: 72, align: 'center' },
  168. { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
  169. { prop: 'name', label: '' },
  170. { prop: 'adDuration', label: '上播时长', width: 80, align: 'center' },
  171. { type: 'invoke', render: [
  172. { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
  173. ], width: 64 }
  174. ]
  175. },
  176. reviewOptions: [
  177. { value: 'reject', label: '驳回' },
  178. { value: '图文不符' },
  179. { value: '内容不合规' }
  180. ],
  181. review: {
  182. type: '',
  183. reason: ''
  184. },
  185. qrCodeType: '0',
  186. qrCodeStatus: 1,
  187. qrCodeStatusTimer: -1,
  188. qrCodeLoading: false
  189. }
  190. },
  191. computed: {
  192. ...mapGetters(['access']),
  193. id () {
  194. return this.$route.params.id
  195. },
  196. status () {
  197. const isFinal = this.access.has(Access.REVIEW_RELEASE_FINAL)
  198. if (__JUMP_REVIEW__ && isFinal) {
  199. return [WorkflowState.FIRST_LEVEL, WorkflowState.SECOND_LEVEL, WorkflowState.FINAL_LEVEL]
  200. }
  201. const arr = []
  202. if (isFinal) {
  203. arr.push(WorkflowState.FIRST_LEVEL)
  204. }
  205. if (this.access.has(Access.REVIEW_RELEASE_SECOND)) {
  206. arr.push(WorkflowState.SECOND_LEVEL)
  207. }
  208. if (this.access.has(Access.REVIEW_RELEASE_FIRST)) {
  209. arr.push(WorkflowState.FINAL_LEVEL)
  210. }
  211. return arr
  212. },
  213. title () {
  214. if (__JUMP_REVIEW__ && this.access.has(Access.REVIEW_RELEASE_FINAL)) {
  215. return '审核'
  216. }
  217. if (this.workflow) {
  218. return WorkflowStateInfo[this.workflow.currentSeveralReviewed]
  219. }
  220. return ''
  221. },
  222. isProgram () {
  223. if (this.workflow) {
  224. return this.workflow.options.publish === PublishTargetType.EVENT && this.workflow.options.event === EventTarget.PROGRAM
  225. }
  226. return false
  227. },
  228. isAssets () {
  229. if (this.workflow) {
  230. return this.workflow.options.publish === PublishTargetType.EVENT && this.workflow.options.event === EventTarget.ASSETS
  231. }
  232. return false
  233. },
  234. needView () {
  235. if (this.workflow) {
  236. return this.workflow.options.publish === PublishTargetType.CALENDAR || this.isProgram
  237. }
  238. return false
  239. },
  240. hasAssets () {
  241. return this.isProgram || this.isAssets
  242. },
  243. schema () {
  244. return {
  245. nonPagination: true,
  246. list: this.getList,
  247. cols: [
  248. { prop: 'priority', label: '优先级', width: 100, align: 'center' },
  249. { prop: 'type', label: '上播内容', width: 80, align: 'center' },
  250. { prop: 'name', label: '' },
  251. { prop: 'ratio', label: '适配', width: 120 },
  252. this.needMarketing && { prop: 'qrCodeStatus', label: '营销二维码状态', render: (data, h) => this.qrCodeStatus === 0
  253. ? h('el-tag', {
  254. key: 'success',
  255. staticClass: 'o-tag',
  256. props: {
  257. type: 'success',
  258. size: 'small',
  259. 'disable-transitions': true
  260. }
  261. }, '成功')
  262. : this.qrCodeLoading
  263. ? h('i', {
  264. key: 'loading',
  265. staticClass: 'u-height--sm el-icon-loading u-font-size--md'
  266. })
  267. : h('el-tag', {
  268. key: 'warning',
  269. staticClass: 'o-tag u-pointer',
  270. props: {
  271. type: 'warning',
  272. size: 'small',
  273. 'disable-transitions': true
  274. },
  275. on: {
  276. click: this.onQrCode
  277. }
  278. }, '未获取,点击重试'), width: 160, align: 'center' },
  279. { prop: 'user', label: '申请人', width: 160, align: 'center' },
  280. { type: 'invoke', render: [
  281. this.needView && { label: '查看', on: this.onView },
  282. { label: '通过', allow: () => !this.needMarketing || this.qrCodeStatus === 0, on: this.onResolve },
  283. { label: '驳回', on: this.onReject }
  284. ].filter(Boolean), width: 160 }
  285. ]
  286. }
  287. },
  288. workflowNodeId () {
  289. // 待审核节点
  290. return this.workflow?.nodes.find(({ status }) => status === State.SUBMITTED)?.id
  291. },
  292. needMarketing () {
  293. return this.qrCodeType === '1'
  294. }
  295. },
  296. created () {
  297. this.getWorkflow()
  298. },
  299. beforeDestroy () {
  300. clearTimeout(this.qrCodeStatusTimer)
  301. this.qrCodeStatusTimer = null
  302. },
  303. methods: {
  304. getQrCodeType () {
  305. return getTenantAttribute(QRCODE_STATUS, false).then(({ data }) => {
  306. if (data) {
  307. this.qrCodeType = data.attributeValue
  308. if (this.needMarketing) {
  309. this.qrCodeLoading = true
  310. this.getQrCodeStatus().finally(() => {
  311. this.qrCodeLoading = false
  312. })
  313. }
  314. }
  315. })
  316. },
  317. getQrCodeStatus () {
  318. return getQrCodeStatus(this.id).then(({ data }) => {
  319. this.qrCodeStatus = data
  320. })
  321. },
  322. getCodeStatusDelay (count) {
  323. this.getQrCodeStatus().finally(() => {
  324. if (this.qrCodeStatusTimer === null || count >= 3 || this.qrCodeStatus === 0) {
  325. this.qrCodeLoading = false
  326. return
  327. }
  328. this.qrCodeStatusTimer = setTimeout(() => {
  329. this.getCodeStatusDelay(count + 1)
  330. }, 5000)
  331. })
  332. },
  333. onQrCode () {
  334. this.qrCodeLoading = true
  335. updateQrCodeStatus(this.id).then(() => {
  336. this.getCodeStatusDelay(1)
  337. }, () => {
  338. this.qrCodeLoading = false
  339. })
  340. },
  341. getWorkflow () {
  342. this.loading = true
  343. this.error = false
  344. getWorkflow(this.id).then(({ data }) => {
  345. if (data.status === State.SUBMITTED && this.status.includes(data.currentSeveralReviewed)) {
  346. this.workflow = {
  347. id: data.id,
  348. user: data.createUser,
  349. currentSeveralReviewed: data.currentSeveralReviewed,
  350. nodes: data.workflowNodeDtoList,
  351. ...parseDeploy(data.businessData)
  352. }
  353. this.audit = 0
  354. return this.getQrCodeType()
  355. }
  356. this.$message({
  357. type: 'warning',
  358. message: '流程已结束或无审核权限'
  359. })
  360. this.onBack()
  361. return Promise.resolce()
  362. }).then(() => {
  363. this.loading = false
  364. }, () => {
  365. this.loading = false
  366. this.error = true
  367. })
  368. },
  369. getList () {
  370. return Promise.resolve({ data: [this.workflow] })
  371. },
  372. getDevices () {
  373. return Promise.resolve({ data: this.workflow.deviceList })
  374. },
  375. getTimes () {
  376. return Promise.resolve({ data: this.workflow.targetList })
  377. },
  378. onView ({ detail }) {
  379. this.$refs.materialDialog.showPublishTarget(detail)
  380. },
  381. getAssets () {
  382. if (this.isAssets) {
  383. const assets = this.workflow.detail.detail.target.sources.map(this.transformAsset)
  384. if (assets.length === 1) {
  385. assets[0].adDuration = '独占'
  386. }
  387. return Promise.resolve({ data: Object.freeze(assets) })
  388. }
  389. if (this.isProgram) {
  390. return getAssetsByProgramSnap(this.workflow.detail.detail.target.id).then(({ data }) => {
  391. return {
  392. data: data.map(this.transformAsset)
  393. }
  394. })
  395. }
  396. return Promise.resolve({ data: [] })
  397. },
  398. transformAsset ({ tag, type, keyName, name, originalName, duration }) {
  399. if (!type) {
  400. return {
  401. name: '资源已删除'
  402. }
  403. }
  404. return {
  405. tagInfo: AssetTagInfo[tag],
  406. typeInfo: AssetTypeInfo[type],
  407. file: {
  408. type,
  409. url: keyName
  410. },
  411. name: name || originalName,
  412. adDuration: parseDuration(duration)
  413. }
  414. },
  415. onViewAsset ({ file }) {
  416. this.$refs.previewDialog.show(file)
  417. },
  418. onBack () {
  419. this.$router.replace({ name: 'workflow-list' })
  420. },
  421. resolveWorkflow () {
  422. return resolveWorkflow(this.workflowNodeId)
  423. },
  424. onResolve () {
  425. this.$confirm(
  426. '审核通过?',
  427. '操作确认',
  428. { type: 'warning' }
  429. ).then(() => {
  430. this.resolveWorkflow().then(() => {
  431. this.audit = 1
  432. })
  433. })
  434. },
  435. onReject () {
  436. this.review = {
  437. type: 'reject',
  438. reason: ''
  439. }
  440. this.$refs.rejectDialog.show()
  441. },
  442. onConfirmReject (done) {
  443. const reason = this.review.type === 'reject' ? this.review.reason : this.review.type
  444. if (!reason) {
  445. this.$message({
  446. type: 'warning',
  447. message: '请选择或填写驳回原因'
  448. })
  449. return
  450. }
  451. rejectWorkflow(this.workflowNodeId, reason).then(() => {
  452. done()
  453. this.audit = 1
  454. })
  455. }
  456. }
  457. }
  458. </script>