index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. <template>
  2. <wrapper
  3. v-loading="!dataMap.length"
  4. fill
  5. margin
  6. padding
  7. background
  8. >
  9. <status-wrapper
  10. slot="empty"
  11. :error="error"
  12. @click="getPublishWorkflowDetail"
  13. />
  14. <div v-if="dataMap.length">
  15. <div class="l-flex--row o-title has-bottom-padding u-color--black u-bold">{{ title }}</div>
  16. <div class="l-flex--row has-padding">
  17. <el-steps
  18. :active="active"
  19. finish-status="success"
  20. class="l-flex__fill"
  21. align-center
  22. >
  23. <el-step
  24. v-if="dataMap.includes('minios')"
  25. title="媒资审核"
  26. />
  27. <el-step
  28. v-if="dataMap.includes('items')"
  29. title="节目审核"
  30. />
  31. <el-step
  32. v-if="dataMap.includes('carousels')"
  33. title="轮播审核"
  34. />
  35. <el-step
  36. v-if="dataMap.includes('programCalendar')"
  37. title="排期审核"
  38. />
  39. <el-step
  40. v-if="dataMap.includes('calendarReleaseScheduling')"
  41. title="发布审核"
  42. />
  43. <el-step title="完成" />
  44. </el-steps>
  45. </div>
  46. <div class="l-flex--row fl-end has-padding">
  47. <!-- <el-button
  48. v-if="active === totalStep - 2"
  49. class="o-button"
  50. @click="onPublish"
  51. >确认发布</el-button
  52. > -->
  53. <el-button
  54. v-if="showTempReject && tempRejectInfo.length"
  55. class="o-button"
  56. @click="reject()"
  57. >{{ tempRejectMap[backDataType]["text"] }}</el-button>
  58. </div>
  59. <schema-table
  60. v-if="active < totalStep"
  61. ref="table"
  62. :key="tableKey"
  63. :schema="schema"
  64. @row-click="onToggle"
  65. >
  66. <preview-dialog ref="previewDialog" />
  67. <review-dialog
  68. v-if="dataMap[active] === 'minios'"
  69. ref="reviewDialog"
  70. :list="tableData"
  71. @resolve="onResolve"
  72. @reject="onConfirmReject"
  73. />
  74. <schedule-dialog ref="scheduleDialog" />
  75. </schema-table>
  76. <div
  77. v-if="active === totalStep"
  78. class="o-card has-padding"
  79. >
  80. <i class="el-icon-success o-card__icon" />
  81. <div class="o-card__title">发布成功</div>
  82. <div class="o-card__desc">排期发布成功!</div>
  83. </div>
  84. <confirm-dialog
  85. ref="rejectDialog"
  86. title="驳回"
  87. @confirm="onConfirmReject"
  88. >
  89. <div class="c-grid-form u-align-self--center">
  90. <span class="c-grid-form__label">审核意见:</span>
  91. <el-select
  92. v-model="review.type"
  93. placeholder="请选择"
  94. >
  95. <el-option
  96. v-for="option in reviewOptions"
  97. :key="option.label"
  98. :label="option.label"
  99. :value="option.value"
  100. />
  101. </el-select>
  102. <template v-if="review.type === 'reject'">
  103. <span class="c-grid-form__label required">原因:</span>
  104. <el-input
  105. v-model.trim="review.reason"
  106. type="textarea"
  107. placeholder="请填写驳回原因"
  108. maxlength="50"
  109. :rows="3"
  110. resize="none"
  111. show-word-limit
  112. />
  113. </template>
  114. </div>
  115. </confirm-dialog>
  116. </div>
  117. </wrapper>
  118. </template>
  119. <script>
  120. import ReviewDialog from './components/ReviewDialog'
  121. import {
  122. getPublishWorkflowDetail,
  123. calendarPublishReject
  124. } from '@/api/workflow'
  125. import { resolveAsset } from '@/api/asset'
  126. import { resolveProgram } from '@/api/program'
  127. import { resolveSchedule } from '@/api/calendar'
  128. import { resolvePublish } from '@/api/platform'
  129. import {
  130. parseByte, parseDuration
  131. } from '@/utils'
  132. import mediaMixin from '@/views/platform/media/mixin.js'
  133. import {
  134. State,
  135. ScheduleType,
  136. EventPriority,
  137. EventFreq,
  138. EventTarget,
  139. PublishType
  140. } from '@/constant'
  141. // 前端命名 和后端数据命名
  142. const front2back = {
  143. assets: 'minios',
  144. program: 'items',
  145. programRecur: 'carousels',
  146. schedule: 'programCalendar',
  147. publish: 'calendarReleaseScheduling'
  148. }
  149. // 数据 是否单个还是list
  150. const singleMap = {
  151. minios: false,
  152. items: false,
  153. carousels: false,
  154. programCalendar: true,
  155. calendarReleaseScheduling: true
  156. }
  157. // 数据 转 驳回type
  158. const data2type = {
  159. minios: 'minio',
  160. items: 'item',
  161. carousels: 'carousel',
  162. programCalendar: 'program',
  163. calendarReleaseScheduling: 'calendar'
  164. }
  165. // label
  166. const data2label = {
  167. minios: '媒资',
  168. items: '节目',
  169. carousels: '轮播',
  170. programCalendar: '排期',
  171. calendarReleaseScheduling: '发布'
  172. }
  173. function showOpt (status) {
  174. return ![State.RESOLVED, State.REJECTED].includes(status)
  175. }
  176. const allDataMap = [
  177. 'minios',
  178. 'items',
  179. 'carousels',
  180. 'programCalendar',
  181. 'calendarReleaseScheduling'
  182. ]
  183. export default {
  184. name: 'ReviewDetail',
  185. components: {
  186. ReviewDialog
  187. },
  188. data () {
  189. return {
  190. error: false,
  191. dataMap: [],
  192. active: 0,
  193. tableKey: 1,
  194. sourceMap: {}, // 总数据
  195. loaded: false,
  196. reviewOptions: [
  197. { value: 'reject', label: '驳回' },
  198. { value: '图文不符' },
  199. { value: '内容不合规' }
  200. ],
  201. review: {
  202. type: '',
  203. reason: ''
  204. },
  205. tempRejectInfo: [],
  206. tempRejectMap: {}
  207. }
  208. },
  209. computed: {
  210. backDataType () {
  211. return this.dataMap[this.active]
  212. },
  213. tableData () {
  214. return this.sourceMap[this.backDataType] || []
  215. },
  216. id () {
  217. return this.$route.params.id
  218. },
  219. title () {
  220. return this.$route.params.name
  221. },
  222. totalStep () {
  223. return this.dataMap.length + 1
  224. },
  225. showTempReject () {
  226. if (
  227. this.tempRejectMap[this.backDataType]
  228. && this.tempRejectMap[this.backDataType].length > 1
  229. ) {
  230. return true
  231. }
  232. return false
  233. },
  234. schema () {
  235. switch (this.backDataType) {
  236. case front2back['assets']:
  237. return {
  238. singlePage: true,
  239. condition: { status: State.REVIEW },
  240. list: this.getList('assets'),
  241. // transform: this.transform,
  242. cols: [
  243. { prop: 'typeName', label: '类型', align: 'center', width: 80 },
  244. { prop: 'file', type: 'asset', on: this.onViewAsset },
  245. { prop: 'originalName', label: '' },
  246. { prop: 'duration', label: '时长' },
  247. { prop: 'size', label: '文件大小' },
  248. { prop: 'createBy', label: '申请人' },
  249. { prop: 'ai', label: 'AI审核', type: 'tag', width: 100 },
  250. {
  251. label: '审核状态',
  252. type: 'tag',
  253. render ({ status }) {
  254. return {
  255. type: ['warning', 'warning', 'success', 'danger'][status],
  256. label: ['未审核', '未审核', '通过', '驳回'][status]
  257. }
  258. }
  259. },
  260. {
  261. type: 'invoke',
  262. width: 80,
  263. render: [{ label: '审核', on: this.onView }]
  264. }
  265. ]
  266. }
  267. case front2back['program']:
  268. return {
  269. singlePage: true,
  270. condition: { status: State.REVIEW },
  271. list: this.getList('program'),
  272. // transform: this.transform,
  273. cols: [
  274. { prop: 'name', label: '节目名称', 'min-width': 100 },
  275. { prop: 'resolutionRatio', label: '分辨率' },
  276. { prop: 'createBy', label: '申请人' },
  277. {
  278. label: '审核状态',
  279. type: 'tag',
  280. render ({ status }) {
  281. return {
  282. type: ['warning', 'warning', 'success', 'danger'][status],
  283. label: ['未审核', '未审核', '通过', '驳回'][status]
  284. }
  285. }
  286. },
  287. {
  288. type: 'invoke',
  289. width: 160,
  290. render: [
  291. { label: '查看', on: this.onView },
  292. {
  293. label: '通过',
  294. render ({ status }) {
  295. return showOpt(status)
  296. },
  297. on: this.onResolve
  298. },
  299. {
  300. label: '驳回',
  301. render ({ status }) {
  302. return showOpt(status)
  303. },
  304. on: this.onReject
  305. }
  306. ]
  307. }
  308. ]
  309. }
  310. case front2back['programRecur']:
  311. case front2back['schedule']:
  312. return {
  313. singlePage: true,
  314. condition: {
  315. type: ScheduleType.COMPLEX,
  316. status: State.REVIEW
  317. },
  318. list: this.getList('schedule'),
  319. cols: [
  320. { prop: 'name', label: '排期名称', 'min-width': 100 },
  321. { prop: 'resolutionRatio', label: '分辨率' },
  322. { prop: 'createBy', label: '申请人' },
  323. {
  324. label: '审核状态',
  325. type: 'tag',
  326. render ({ status }) {
  327. return {
  328. type: ['warning', 'warning', 'success', 'danger'][status],
  329. label: ['未审核', '未审核', '通过', '驳回'][status]
  330. }
  331. }
  332. },
  333. {
  334. type: 'invoke',
  335. width: 160,
  336. render: [
  337. { label: '查看', on: this.onView },
  338. {
  339. label: '通过',
  340. render ({ status }) {
  341. return showOpt(status)
  342. },
  343. on: this.onResolve
  344. },
  345. {
  346. label: '驳回',
  347. render ({ status }) {
  348. return showOpt(status)
  349. },
  350. on: this.onReject
  351. }
  352. ]
  353. }
  354. ]
  355. }
  356. case front2back['publish']:
  357. return {
  358. singlePage: true,
  359. condition: { status: State.REVIEW },
  360. list: this.getList('publish'),
  361. // transform: this.transform,
  362. cols: [
  363. {
  364. prop: 'expand',
  365. type: 'expand',
  366. render (data, h) {
  367. return h(
  368. 'div',
  369. {
  370. staticClass: 'o-info'
  371. },
  372. [
  373. h('div', null, data.desc),
  374. h('div', null, `设备:${data.device}`)
  375. ]
  376. )
  377. }
  378. },
  379. { prop: 'type', label: '类型', width: 100 },
  380. { prop: 'name', label: '名称', 'min-width': 100 },
  381. { prop: 'resolutionRatio', label: '分辨率' },
  382. { prop: 'createBy', label: '申请人' },
  383. {
  384. type: 'invoke',
  385. width: 160,
  386. render: [
  387. { label: '查看', on: this.onView },
  388. {
  389. label: '通过',
  390. render ({ status }) {
  391. return showOpt(status)
  392. },
  393. on: this.onPublish
  394. },
  395. {
  396. label: '驳回',
  397. render ({ status }) {
  398. return showOpt(status)
  399. },
  400. on: this.onReject
  401. }
  402. ]
  403. }
  404. ]
  405. }
  406. default:
  407. return {}
  408. }
  409. }
  410. },
  411. mounted () {
  412. this.getPublishWorkflowDetail()
  413. },
  414. methods: {
  415. // 判断是否需要缓存驳回
  416. calTempReject (item) {
  417. if (!singleMap[item]) {
  418. this.tempRejectMap[item] = {
  419. text: `${data2label[item]}驳回`,
  420. show:
  421. !this.tempRejectMap.length
  422. && this.sourceMap[item].filter(i => ![State.RESOLVED, State.REJECTED].includes(i.status))
  423. .length
  424. }
  425. }
  426. },
  427. async getPublishWorkflowDetail () {
  428. let res = await getPublishWorkflowDetail(this.id).catch(err => {
  429. console.log(err)
  430. })
  431. if (!res || !res.success) {
  432. this.error = true
  433. return
  434. }
  435. this.error = false
  436. res = res.data
  437. for (const key in res) {
  438. if (Object.prototype.hasOwnProperty.call(res, key)) {
  439. const element = res[key]
  440. if (!Array.isArray(element)) {
  441. res[key] = [element]
  442. }
  443. }
  444. }
  445. this.sourceMap = res
  446. // init
  447. // 判断步骤 filter datamap 并transform row
  448. const temp = []
  449. for (const item of allDataMap) {
  450. if (this.sourceMap[item][0]) {
  451. this.sourceMap[item] = this.sourceMap[item].map(row => this.transform(row, item))
  452. temp.push(item)
  453. // 处理缓存驳回初始状态
  454. if (!singleMap[item]) {
  455. this.tempRejectMap[item] = {
  456. text: `${data2label[item]}驳回`,
  457. length: this.sourceMap[item].filter(
  458. i => ![State.RESOLVED, State.REJECTED].includes(i.status)
  459. ).length
  460. }
  461. }
  462. }
  463. }
  464. this.dataMap = temp
  465. // 确定节点
  466. let active = 0
  467. for (const item of this.dataMap) {
  468. if (this.sourceMap[item].every(i => i.status === State.RESOLVED)) {
  469. active++
  470. continue
  471. }
  472. break
  473. }
  474. this.active = active
  475. if (this.active === this.totalStep - 1) {
  476. this.active = this.totalStep
  477. }
  478. // this.$refs.table.onPagination();
  479. },
  480. refresh () {
  481. this.getPublishWorkflowDetail()
  482. },
  483. refreshStatus (item, status, review) {
  484. const list = this.sourceMap[this.backDataType]
  485. const index = list.findIndex(i => i.id === item.id)
  486. list[index].status = status
  487. this.$refs.table.onPagination()
  488. if (status === State.RESOLVED && this.tableData.every(i => i.status === State.RESOLVED)) {
  489. this.closePreviewDialog()
  490. this.nextStep()
  491. }
  492. if (status === State.REJECTED) {
  493. list[index].review = review
  494. }
  495. },
  496. getList () {
  497. return () => Promise.resolve({ data: this.tableData })
  498. },
  499. // 全部审批通过
  500. nextStep () {
  501. this.active += 1
  502. if (this.active === this.totalStep - 1) {
  503. this.active = this.totalStep
  504. }
  505. this.tempRejectInfo = []
  506. this.tableKey++
  507. },
  508. next () {
  509. if (this.active++ > 5) {
  510. this.active = 0
  511. }
  512. this.tableKey++
  513. },
  514. transform (row, kind) {
  515. const { type } = row
  516. switch (kind) {
  517. case front2back['assets']:
  518. row.typeName = [null, '图片', '视频', '音频'][type]
  519. row.file = {
  520. type: row.type,
  521. url: row.keyName,
  522. thumbnail: row.thumbnail
  523. }
  524. row.duration = parseDuration(row.duration)
  525. row.size = parseByte(row.size)
  526. row.createBy = row.userName || row.createBy
  527. row.ai = mediaMixin.methods.getAIState(row)
  528. row.id = row.keyName
  529. return row
  530. case front2back['program']:
  531. row.createBy = row.userName || row.createBy
  532. return row
  533. case front2back['publish']:
  534. return { ...this.getSame(row), ...this.getDiff(row) }
  535. default:
  536. return row
  537. }
  538. },
  539. // publish 排期辅助函数
  540. getSame ({
  541. id,
  542. programCalendarName,
  543. resolutionRatio,
  544. createBy,
  545. createByUsername,
  546. createTime,
  547. calendarReleaseDeviceList
  548. }) {
  549. return {
  550. id,
  551. name: programCalendarName,
  552. resolutionRatio,
  553. createBy: createByUsername || createBy,
  554. createTime,
  555. device: calendarReleaseDeviceList
  556. ?.map(item => item.deviceName)
  557. .join(',')
  558. }
  559. },
  560. getDiff (item) {
  561. const target = JSON.parse(item.target)
  562. let type = ''
  563. switch (target.type) {
  564. case PublishType.CALENDAR:
  565. type = '排期'
  566. break
  567. case PublishType.EVENT:
  568. type = ['', '默认播放', '单播', '插播'][target.detail.priority]
  569. break
  570. default:
  571. break
  572. }
  573. return {
  574. type,
  575. target,
  576. desc: this.getDesc(target)
  577. }
  578. },
  579. getDesc (target) {
  580. if (
  581. target.type === PublishType.EVENT
  582. && target.detail.priority === EventPriority.INSERTED
  583. ) {
  584. const { freq, start, until, byDay, startTime, endTime } = target.detail
  585. switch (freq) {
  586. case EventFreq.WEEKLY:
  587. return `自${start.split(' ')[0]}开始${
  588. until ? `${until.split(' ')[0]}前` : ''
  589. } 每周${byDay
  590. .split(',')
  591. .map(val => ['日', '一', '二', '三', '四', '五', '六'][val])
  592. .join('、')} ${startTime} - ${endTime}`
  593. default:
  594. return until ? `${start} - ${until}` : `自${start}开始`
  595. }
  596. }
  597. return ''
  598. },
  599. onToggle (row) {
  600. if (this.backDataType !== 'publish') {
  601. return
  602. }
  603. this.$refs.table.getInst().toggleRowExpansion(row)
  604. },
  605. onPublishView ({ target: { type, detail } }) {
  606. switch (type) {
  607. case PublishType.CALENDAR:
  608. this.viewSchedule(detail)
  609. break
  610. case PublishType.EVENT:
  611. if (detail.target.type === EventTarget.RECUR) {
  612. this.viewSchedule(detail.target.id)
  613. } else {
  614. this.viewProgram(detail.target.id)
  615. }
  616. break
  617. default:
  618. break
  619. }
  620. },
  621. viewSchedule (id) {
  622. this.$refs.scheduleDialog.show(id)
  623. },
  624. viewProgram (id) {
  625. this.$viewProgram(id)
  626. },
  627. // 查看
  628. onView (row) {
  629. const { id } = row
  630. switch (this.backDataType) {
  631. case front2back['assets']:
  632. this.$refs.reviewDialog.show(
  633. this.tableData.findIndex(i => i.id === id)
  634. )
  635. break
  636. case front2back['program']:
  637. this.$viewProgram(id)
  638. break
  639. case front2back['schedule']:
  640. case front2back['programRecur']:
  641. this.$refs.scheduleDialog.show(id)
  642. break
  643. case front2back['publish']:
  644. this.onPublishView(row)
  645. break
  646. default:
  647. break
  648. }
  649. },
  650. onViewAsset (asset) {
  651. this.$refs.previewDialog.show(asset)
  652. },
  653. closePreviewDialog () {
  654. if (this.backDataType === front2back['assets']) {
  655. this.$refs.reviewDialog.close()
  656. }
  657. },
  658. // 通过
  659. onResolve (item) {
  660. this.resolve(item).then(() => {
  661. this.refreshStatus(item, State.RESOLVED)
  662. })
  663. },
  664. resolve (item) {
  665. switch (this.backDataType) {
  666. case front2back['assets']:
  667. return resolveAsset(item)
  668. case front2back['program']:
  669. return resolveProgram(item)
  670. case front2back['schedule']:
  671. case front2back['programRecur']:
  672. return resolveSchedule(item)
  673. case front2back['publish']:
  674. return resolvePublish(item)
  675. default:
  676. return new Promise().reject()
  677. }
  678. },
  679. // 行内
  680. // 驳回 列表按钮触发
  681. onReject (item) {
  682. this.$item = item
  683. this.review = {
  684. type: 'reject',
  685. reason: ''
  686. }
  687. this.$refs.rejectDialog.show()
  688. },
  689. // dialog 直接触发
  690. onConfirmReject (done, props) {
  691. if (props) {
  692. const { item, review } = props
  693. this.$item = item
  694. this.review = review
  695. }
  696. const reason =
  697. this.review.type === 'reject' ? this.review.reason : this.review.type
  698. if (!reason) {
  699. this.$message({
  700. type: 'warning',
  701. message: '请选择或填写驳回原因'
  702. })
  703. return
  704. }
  705. const rejectInfo = {
  706. id: this.$item.id,
  707. name: this.$item.name || this.$item.originalName,
  708. remark: reason
  709. }
  710. if (this.showTempReject) {
  711. this.$confirm(`驳回 ${rejectInfo.name} ?`, {
  712. type: 'warning'
  713. }).then(() => {
  714. // 缓存驳回数据
  715. done()
  716. this.tempRejectInfo.push(rejectInfo)
  717. this.refreshStatus(this.$item, State.REJECTED, {
  718. type: this.review.type,
  719. reason: this.review.reason
  720. })
  721. })
  722. } else {
  723. // 取最新
  724. done()
  725. this.reject([rejectInfo], rejectInfo.name)
  726. }
  727. },
  728. // 流程驳回
  729. reject (
  730. rejectInfo = this.tempRejectInfo,
  731. name = data2label[this.backDataType]
  732. ) {
  733. return calendarPublishReject(
  734. this.id,
  735. {
  736. type: data2type[this.backDataType],
  737. rejectInfo
  738. },
  739. name
  740. )
  741. .then(() => {
  742. this.$router.push({ name: 'workflow-list' })
  743. })
  744. .catch(err => {
  745. console.log(err)
  746. })
  747. },
  748. // 确认发布
  749. onPublish () {
  750. resolvePublish({
  751. name: this.tableData[0].programCalendarName,
  752. id: this.tableData[0].id
  753. }).then(() => {
  754. this.nextStep()
  755. })
  756. }
  757. }
  758. }
  759. </script>
  760. <style lang="scss" scoped>
  761. .o-card {
  762. display: flex;
  763. align-items: center;
  764. flex-direction: column;
  765. width: 100%;
  766. padding-top: 100px;
  767. &__icon {
  768. color: #04a681;
  769. font-size: 48px;
  770. }
  771. &__title {
  772. font-size: 16px;
  773. font-weight: 500;
  774. color: #333333;
  775. line-height: 36px;
  776. }
  777. &__desc {
  778. font-weight: 400;
  779. color: #d5d9e4;
  780. line-height: 36px;
  781. font-size: 14px;
  782. padding-left: 15px;
  783. }
  784. }
  785. .o-title {
  786. font-size: 18px;
  787. }
  788. .fl-end {
  789. justify-content: flex-end;
  790. }
  791. </style>