Browse Source

Merge branch 'staging' of http://pms.inspur.com/web/msr into staging

Casper Dai 3 years ago
parent
commit
5f70dc2508

+ 58 - 0
src/api/workflow.js

@@ -0,0 +1,58 @@
+// 流程审批
+import request, { tenantRequest } from '@/utils/request'
+import {
+  addScope, addUser, submit, reject
+} from './base'
+//  审核管理分页列表
+export function getPublishWorkflows (query) {
+  const { pageNum: pageIndex, pageSize, self, ...params } = query
+  return tenantRequest({
+    url: '/workflow/calendarRelease/page',
+    method: 'GET',
+    params: self
+      ? addUser({
+        pageIndex,
+        pageSize,
+        ...params
+      })
+      : addScope({
+        pageIndex,
+        pageSize,
+        ...params
+      })
+  })
+}
+// 审核详情资源
+export function getPublishWorkflowDetail (workflowId) {
+  return request({
+    url: `/workflow/${workflowId}/list`,
+    method: 'GET'
+  })
+}
+// 审批通过
+
+// 审核驳回
+export function calendarPublishReject (workflowId, data, name) {
+  return reject(
+    {
+      url: `/workflow/calendarRelease/${workflowId}/reject`,
+      method: 'POST',
+      data
+    },
+    name
+  )
+}
+// 流程重提交
+export function calendarPublishRestart (workflowId, name) {
+  return submit({
+    url: `/workflow/${workflowId}/restart `,
+    method: 'POST'
+  }, name)
+}
+// 流程终止
+export function calendarPublishStop (workflowId) {
+  return request({
+    url: `/workflow/${workflowId}/stop`,
+    method: 'POST'
+  })
+}

+ 17 - 3
src/router/index.js

@@ -144,11 +144,25 @@ export const asyncRoutes = [
         meta: { title: '排期发布' }
       },
       {
-        name: 'review',
         path: 'review',
-        component: () => import('@/views/review/index'),
+        component: Solo,
         access: Access.MANAGE_GROUP,
-        meta: { title: '审核管理' }
+        meta: { title: '审核管理' },
+        children: [
+          {
+            name: 'review-list',
+            path: '',
+            component: () => import('@/views/review/index'),
+            meta: { cache: 'Review' }
+          },
+          {
+            hidden: true,
+            name: 'review-detail',
+            path: ':id',
+            component: () => import('@/views/review/detail/index'),
+            meta: { title: '审核', cache: 'Review' }
+          }
+        ]
       },
       {
         name: 'schedule-deploy-history',

+ 206 - 0
src/views/review/components/MyWorkflow.vue

@@ -0,0 +1,206 @@
+<template>
+  <div>
+    <el-tabs
+      :value="active"
+      class="c-tabs has-bottom-padding"
+      @tab-click="onTabClick"
+    >
+
+      <el-tab-pane
+        label="待审核"
+        name="1"
+      />
+      <el-tab-pane
+        label="已审核"
+        name="2"
+      />
+      <el-tab-pane
+        label="被驳回"
+        name="3"
+      />
+    </el-tabs>
+    <schema-table
+      ref="table"
+      :schema="schema"
+      @row-click="onToggle"
+    />
+    <schedule-dialog ref="scheduleDialog" />
+  </div>
+
+</template>
+
+<script>
+import {
+  getPublishWorkflows, calendarPublishRestart
+} from '@/api/workflow'
+import {
+  PublishType, EventPriority
+} from '@/constant'
+import { getEventDescription } from '@/utils/event'
+export default {
+  name: 'MyWorkflow',
+  data () {
+    return {
+      active: '1',
+      schema: {
+        condition: { self: true, status: 1, name: '' },
+        // filters: [
+        //   { key: 'name', type: 'search', placeholder: '名称' }
+        // ],
+        list: getPublishWorkflows,
+        transform: this.transform,
+        cols: [
+          {
+            prop: 'expand',
+            type: 'expand',
+            render (data, h) {
+              return h(
+                'div',
+                {
+                  staticClass: 'o-info'
+                },
+                [
+                  h('div', null, data.desc),
+                  h('div', null, `设备:${data.device}`)
+                ]
+              )
+            }
+          },
+          { prop: 'type', label: '类型', width: 100 },
+          { prop: 'name', label: '名称', 'min-width': 100 },
+          { prop: 'resolutionRatio', label: '分辨率' },
+          { prop: 'createBy', label: '申请人' },
+          { prop: 'createTime', label: '提交时间' },
+          {
+            label: '审核状态',
+            type: 'tag',
+            render ({ status }) {
+              return {
+                type: ['', 'warning', 'success', 'danger'][status],
+                label: ['草稿', '待审核', '通过', '驳回'][status]
+              }
+            }
+          },
+          {
+            type: 'invoke',
+            width: 160,
+            render: [
+              { label: '查看', on: this.onView },
+              { label: '提交', render ({ status }) {
+                return status === 3
+              }, on: this.restart }
+            ]
+          }
+        ]
+      }
+    }
+  },
+  methods: {
+    onTabClick ({ name: active }) {
+      if (this.active !== active) {
+        this.active = active
+        this.$refs.table.mergeCondition({ status: Number(active) })
+      }
+    },
+    transform (item) {
+      const same = this.getSame(item.calendarRelease)
+      const diff = this.getDiff(item.calendarRelease)
+      return { ...same, ...diff, workflowId: item.id, status: item.status }
+    },
+    getSame ({
+      id,
+      programCalendarName,
+      resolutionRatio,
+      createBy,
+      createByUsername,
+      createTime,
+      calendarReleaseDeviceList
+    }) {
+      return {
+        id,
+        name: programCalendarName,
+        resolutionRatio,
+        createBy: createByUsername || createBy,
+        createTime,
+        device: calendarReleaseDeviceList
+          ?.map(item => item.deviceName)
+          .join(',')
+      }
+    },
+    getDiff (item) {
+      const target = JSON.parse(item.target)
+      let type = ''
+      switch (target.type) {
+        case PublishType.CALENDAR:
+          type = '排期'
+          break
+        case PublishType.EVENT:
+          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          break
+        default:
+          break
+      }
+      return {
+        type,
+        target,
+        desc: this.getDesc(target)
+      }
+    },
+    getDesc (target) {
+      if (
+        target.type === PublishType.EVENT
+        && target.detail.priority === EventPriority.INSERTED
+      ) {
+        return getEventDescription(target.detail)
+      }
+      return ''
+    },
+    onToggle (row) {
+      this.$refs.table.getInst().toggleRowExpansion(row)
+    },
+    onView ({ target: { type, detail } }) {
+      switch (type) {
+        case PublishType.CALENDAR:
+          this.viewSchedule(detail)
+          break
+        case PublishType.EVENT:
+          if (detail.target.type === EventTarget.RECUR) {
+            this.viewSchedule(detail.target.id)
+          } else {
+            this.viewProgram(detail.target.id)
+          }
+          break
+        default:
+          break
+      }
+    },
+    viewSchedule (id) {
+      this.$refs.scheduleDialog.show(id)
+    },
+    viewProgram (id) {
+      window.open(
+        this.$router.resolve({
+          name: 'program',
+          params: { id }
+        }).href,
+        '_blank'
+      )
+    },
+    review (item) {
+      this.$router.push({
+        name: 'review-detail',
+        params: {
+          id: item.workflowId
+        }
+      })
+    },
+    restart (item) {
+      calendarPublishRestart(item.workflowId, item.name).then(() => {
+        this.$refs.table.onPagination()
+      })
+    }
+  }
+}
+</script>
+
+<style></style>

+ 255 - 0
src/views/review/components/ReviewDialog.vue

@@ -0,0 +1,255 @@
+<template>
+  <el-dialog
+    :visible.sync="isReviewing"
+    custom-class="c-review"
+    append-to-body
+    title="审核"
+  >
+    <div class="l-flex">
+      <div class="c-review-assest l-flex--col c-sibling-item">
+        <div class="l-flex--row center c-review-assest__block">
+          <auto-image
+            v-if="isImage"
+            class="o-image--preview"
+            :src="assetUrl"
+            retry
+          />
+          <video
+            v-if="isVideo"
+            class="o-video--preview"
+            :src="assetUrl"
+            controls
+            @click.stop
+          />
+          <audio
+            v-if="isAudio"
+            :src="assetUrl"
+            controls
+            @click.stop
+          />
+        </div>
+        <div class="l-flex--col has-top-padding">
+          <div class="c-grid-form u-align-self--center has-bottom-padding">
+            <span class="c-grid-form__label">审核意见:</span>
+            <el-select
+              v-model="review.type"
+              placeholder="请选择"
+            >
+              <el-option
+                v-for="option in reviewOptions"
+                :key="option.label"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+            <template v-if="review.type === 'reject'">
+              <span class="c-grid-form__label required">原因:</span>
+              <el-input
+                v-model.trim="review.reason"
+                type="textarea"
+                placeholder="请填写驳回原因"
+                maxlength="50"
+                :rows="3"
+                resize="none"
+                show-word-limit
+              />
+            </template>
+          </div>
+          <div class="l-flex--row">
+            <el-button
+              type="primary"
+              class="o-button c-sibling-item"
+              :disabled="!showOpt"
+              @click="resolve"
+            >通过</el-button>
+            <el-button
+              type="primary"
+              class="o-button c-sibling-item"
+              :disabled="!showOpt"
+              @click="reject"
+            >驳回</el-button>
+            <el-button
+              type="primary"
+              class="o-button c-sibling-item"
+              :disabled="list.length < 2"
+              @click="next(-1)"
+            >
+              上一张
+            </el-button>
+            <el-button
+              type="primary"
+              class="o-button c-sibling-item"
+              :disabled="list.length < 2"
+              @click="next(1)"
+            >
+              下一张
+            </el-button>
+          </div>
+        </div>
+      </div>
+      <div class="c-review-form l-flex--col c-sibling-item far">
+        <div
+          v-for="line in lines"
+          :key="line.key"
+          class="l-flex--row l-flex__fill"
+        >
+          <div class="c-review-form__label">{{ line.label }}:</div>
+          <div>
+            {{
+              line.transform ? line.transform(form[line.key]) : form[line.key]
+            }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- <preview-dialog ref="previewDialog" /> -->
+  </el-dialog>
+</template>
+
+<script>
+import { getAssetUrl } from '@/api/asset'
+import { AssetType } from '@/constant'
+export default {
+  name: 'ReviewDialog',
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data () {
+    return {
+      lines: [
+        { label: '文件名称', key: 'originalName' },
+        { label: '分辨率', key: 'a' },
+        { label: '大小', key: 'size' },
+        { label: '上传人', key: 'createBy' },
+        { label: '时长', key: 'duration' },
+        { label: '上传时间', key: 'createTime' },
+        { label: 'AI审核通过', key: 'q' },
+        {
+          label: '审核状态',
+          key: 'status',
+          transform: i => ['未审核', '未审核', '通过', '驳回'][i]
+        }
+      ],
+      isReviewing: false,
+      index: 0,
+      isFull: false,
+      // review
+      reviewOptions: [
+        { value: 'reject', label: '驳回' },
+        { value: '图文不符' },
+        { value: '内容不合规' }
+      ],
+      review: {
+        type: '',
+        reason: ''
+      }
+    }
+  },
+  computed: {
+    showOpt () {
+      return ![2, 3].includes(this.form.status)
+    },
+    form () {
+      return this.list[this.index] || {}
+    },
+    source () {
+      console.log({ ...this.list[this.index] }, this.list[this.index]['file'])
+      return this.list[this.index].file || {}
+    },
+    isImage () {
+      return this.source?.type === AssetType.IMAGE
+    },
+    isVideo () {
+      return this.source?.type === AssetType.VIDEO
+    },
+    isAudio () {
+      return this.source?.type === AssetType.AUDIO
+    },
+    assetUrl () {
+      return this.source && getAssetUrl(this.source.url)
+    }
+  },
+  methods: {
+    show (index) {
+      this.index = index
+      this.isReviewing = true
+    },
+    close () {
+      this.isReviewing = false
+    },
+    onCloseReviewDialog () {
+      console.log('close')
+    },
+    full () {
+      this.$refs.previewDialog.show(this.source)
+    },
+    resolve () {
+      this.$emit('resolve', this.form)
+    },
+    reject () {
+      this.$emit(
+        'reject',
+        () => {
+          this.$message.success('驳回成功!')
+        },
+        { item: this.form, review: this.review }
+      )
+    },
+    next (gap) {
+      const position = this.index
+      let index = (position + gap) % this.list.length
+      if (index < 0) {
+        index += this.list.length
+      }
+      this.index = index
+      if (this.form.review) {
+        this.review = { ...this.form.review }
+      } else {
+        this.review = {
+          type: '',
+          reason: ''
+        }
+      }
+    }
+
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-review {
+  &-assest {
+    flex: 0 0 464px;
+    &__block {
+      height: 278px;
+    }
+
+    .o-image--preview,
+    .o-video--preview {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  &-form {
+    flex: 1 0 250px;
+    height: 278px;
+    font-size: 16px;
+    &__label {
+      flex: 0 0 90px;
+      font-weight: 700;
+    }
+  }
+}
+.fl-1 {
+  flex: 1;
+}
+</style>
+<style>
+.c-review {
+  min-width: 800px;
+}
+</style>

+ 161 - 0
src/views/review/components/ReviewWorkflow.vue

@@ -0,0 +1,161 @@
+<template>
+  <schema-table
+    ref="table"
+    :schema="schema"
+    @row-click="onToggle"
+  />
+</template>
+
+<script>
+import { getPublishWorkflows } from '@/api/workflow'
+import {
+  PublishType, EventPriority
+} from '@/constant'
+import { getEventDescription } from '@/utils/event'
+export default {
+  data () {
+    return {
+      schema: {
+        condition: { status: 1 },
+        list: getPublishWorkflows,
+        transform: this.transform,
+        cols: [
+          {
+            prop: 'expand',
+            type: 'expand',
+            render (data, h) {
+              return h(
+                'div',
+                {
+                  staticClass: 'o-info'
+                },
+                [
+                  h('div', null, data.desc),
+                  h('div', null, `设备:${data.device}`)
+                ]
+              )
+            }
+          },
+          { prop: 'type', label: '类型', width: 100 },
+          { prop: 'name', label: '名称', 'min-width': 100 },
+          { prop: 'resolutionRatio', label: '分辨率' },
+          { prop: 'createBy', label: '申请人' },
+          { prop: 'createTime', label: '提交时间' },
+          {
+            label: '审核状态',
+            type: 'tag',
+            render ({ status }) {
+              return {
+                type: ['', 'warning', 'success', 'danger'][status],
+                label: ['草稿', '待审核', '通过', '驳回'][status]
+              }
+            }
+          },
+          {
+            type: 'invoke',
+            width: 80,
+            render: [{ label: '审核', on: this.review }]
+          }
+        ]
+      }
+    }
+  },
+  methods: {
+    transform (item) {
+      const same = this.getSame(item.calendarRelease)
+      const diff = this.getDiff(item.calendarRelease)
+      return { ...same, ...diff, workflowId: item.id, status: item.status }
+    },
+    getSame ({
+      id,
+      programCalendarName,
+      resolutionRatio,
+      createBy,
+      createByUsername,
+      createTime,
+      calendarReleaseDeviceList
+    }) {
+      return {
+        id,
+        name: programCalendarName,
+        resolutionRatio,
+        createBy: createByUsername || createBy,
+        createTime,
+        device: calendarReleaseDeviceList
+          ?.map(item => item.deviceName)
+          .join(',')
+      }
+    },
+    getDiff (item) {
+      const target = JSON.parse(item.target)
+      let type = ''
+      switch (target.type) {
+        case PublishType.CALENDAR:
+          type = '排期'
+          break
+        case PublishType.EVENT:
+          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          break
+        default:
+          break
+      }
+      return {
+        type,
+        target,
+        desc: this.getDesc(target)
+      }
+    },
+    getDesc (target) {
+      if (
+        target.type === PublishType.EVENT
+        && target.detail.priority === EventPriority.INSERTED
+      ) {
+        return getEventDescription(target.detail)
+      }
+      return ''
+    },
+    onToggle (row) {
+      this.$refs.table.getInst().toggleRowExpansion(row)
+    },
+    onView ({ target: { type, detail } }) {
+      switch (type) {
+        case PublishType.CALENDAR:
+          this.viewSchedule(detail)
+          break
+        case PublishType.EVENT:
+          if (detail.target.type === EventTarget.RECUR) {
+            this.viewSchedule(detail.target.id)
+          } else {
+            this.viewProgram(detail.target.id)
+          }
+          break
+        default:
+          break
+      }
+    },
+    viewSchedule (id) {
+      this.$refs.scheduleDialog.show(id)
+    },
+    viewProgram (id) {
+      window.open(
+        this.$router.resolve({
+          name: 'program',
+          params: { id }
+        }).href,
+        '_blank'
+      )
+    },
+    review (item) {
+      this.$router.push({
+        name: 'review-detail',
+        params: {
+          id: item.workflowId,
+          name: item.name
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style></style>

+ 814 - 0
src/views/review/detail/index.vue

@@ -0,0 +1,814 @@
+<template>
+  <wrapper
+    v-loading="!dataMap.length"
+    fill
+    margin
+    padding
+    background
+  >
+    <status-wrapper
+      slot="empty"
+      :error="error"
+      @click="getPublishWorkflowDetail"
+    />
+    <div v-if="dataMap.length">
+      <div class="l-flex--row has-padding o-title">{{ title }}</div>
+      <div class="l-flex--row has-padding">
+        <el-steps
+          :active="active"
+          finish-status="success"
+          class="l-flex__fill"
+          align-center
+        >
+          <el-step
+            v-if="dataMap.includes('minios')"
+            title="媒资审核"
+          />
+          <el-step
+            v-if="dataMap.includes('items')"
+            title="节目审核"
+          />
+          <el-step
+            v-if="dataMap.includes('carousels')"
+            title="轮播审核"
+          />
+          <el-step
+            v-if="dataMap.includes('programCalendar')"
+            title="排期审核"
+          />
+          <el-step
+            v-if="dataMap.includes('calendarReleaseScheduling')"
+            title="发布审核"
+          />
+          <el-step title="完成" />
+        </el-steps>
+      </div>
+
+      <div class="l-flex--row fl-end has-padding">
+        <!-- <el-button
+          v-if="active === totalStep - 2"
+          class="o-button"
+          @click="onPublish"
+          >确认发布</el-button
+        > -->
+        <el-button
+          v-if="showTempReject && tempRejectInfo.length"
+          class="o-button"
+          @click="reject"
+        >{{ tempRejectMap[backDataType]["text"] }}</el-button>
+      </div>
+      <schema-table
+        v-if="active < totalStep"
+        ref="table"
+        :key="tableKey"
+        :schema="schema"
+        @row-click="onToggle"
+      >
+        <preview-dialog ref="previewDialog" />
+        <review-dialog
+          v-if="dataMap[active] === 'minios'"
+          ref="reviewDialog"
+          :list="tableData"
+          @resolve="onResolve"
+          @reject="onConfirmReject"
+        />
+        <schedule-dialog ref="scheduleDialog" />
+      </schema-table>
+      <div
+        v-if="active === totalStep"
+        class="o-card has-padding"
+      >
+        <i class="el-icon-success o-card__icon" />
+        <div class="o-card__title">发布成功</div>
+        <div class="o-card__desc">排期发布成功!</div>
+      </div>
+      <confirm-dialog
+        ref="rejectDialog"
+        title="驳回"
+        @confirm="onConfirmReject"
+      >
+        <div class="c-grid-form u-align-self--center">
+          <span class="c-grid-form__label">审核意见:</span>
+          <el-select
+            v-model="review.type"
+            placeholder="请选择"
+          >
+            <el-option
+              v-for="option in reviewOptions"
+              :key="option.label"
+              :label="option.label"
+              :value="option.value"
+            />
+          </el-select>
+          <template v-if="review.type === 'reject'">
+            <span class="c-grid-form__label required">原因:</span>
+            <el-input
+              v-model.trim="review.reason"
+              type="textarea"
+              placeholder="请填写驳回原因"
+              maxlength="50"
+              :rows="3"
+              resize="none"
+              show-word-limit
+            />
+          </template>
+        </div>
+      </confirm-dialog>
+    </div>
+  </wrapper>
+</template>
+
+<script>
+import ReviewDialog from '../components/ReviewDialog'
+import {
+  getPublishWorkflowDetail,
+  calendarPublishReject
+} from '@/api/workflow'
+import { resolveAsset } from '@/api/asset'
+import { resolveProgram } from '@/api/program'
+import { resolveSchedule } from '@/api/calendar'
+import { resolvePublish } from '@/api/platform'
+import {
+  parseByte, parseDuration
+} from '@/utils'
+import mediaMixin from '@/views/platform/media/mixin.js'
+import {
+  State,
+  ScheduleType,
+  EventPriority,
+  EventFreq,
+  EventTarget,
+  PublishType
+} from '@/constant'
+// 前端命名 和后端数据命名
+const front2back = {
+  assets: 'minios',
+  program: 'items',
+  programRecur: 'carousels',
+  schedule: 'programCalendar',
+  publish: 'calendarReleaseScheduling'
+}
+// 数据 是否单个还是list
+const singleMap = {
+  minios: false,
+  items: false,
+  carousels: false,
+  programCalendar: true,
+  calendarReleaseScheduling: true
+}
+// 数据 转 驳回type
+const data2type = {
+  minios: 'minio',
+  items: 'item',
+  carousels: 'carousel',
+  programCalendar: 'program',
+  calendarReleaseScheduling: 'calendar'
+}
+// label
+const data2label = {
+  minios: '媒资',
+  items: '节目',
+  carousels: '轮播',
+  programCalendar: '排期',
+  calendarReleaseScheduling: '发布'
+}
+function showOpt (status) {
+  return ![2, 3].includes(status)
+}
+const allDataMap = [
+  'minios',
+  'items',
+  'carousels',
+  'programCalendar',
+  'calendarReleaseScheduling'
+]
+export default {
+  name: 'ReviewDetail',
+  components: {
+    ReviewDialog
+  },
+  props: {},
+  data () {
+    return {
+      error: false,
+      dataMap: [],
+      active: 0,
+      tableKey: 1,
+      sourceMap: {}, // 总数据
+      loaded: false,
+      reviewOptions: [
+        { value: 'reject', label: '驳回' },
+        { value: '图文不符' },
+        { value: '内容不合规' }
+      ],
+      review: {
+        type: '',
+        reason: ''
+      },
+      tempRejectInfo: [],
+      tempRejectMap: {}
+    }
+  },
+  computed: {
+    backDataType () {
+      return this.dataMap[this.active]
+    },
+    tableData () {
+      return this.sourceMap[this.backDataType] || []
+    },
+    id () {
+      return this.$route.params.id
+    },
+    title () {
+      return this.$route.params.name
+    },
+    totalStep () {
+      return this.dataMap.length + 1
+    },
+    showTempReject () {
+      if (
+        this.tempRejectMap[this.backDataType]
+        && this.tempRejectMap[this.backDataType].length > 1
+      ) {
+        return true
+      }
+      return false
+    },
+    schema () {
+      switch (this.backDataType) {
+        case front2back['assets']:
+          return {
+            singlePage: true,
+            condition: { status: State.REVIEW },
+            list: this.getList('assets'),
+            // transform: this.transform,
+            cols: [
+              { prop: 'typeName', label: '类型', align: 'center', width: 80 },
+              { prop: 'file', type: 'asset', on: this.onViewAsset },
+              { prop: 'originalName', label: '' },
+              { prop: 'duration', label: '时长' },
+              { prop: 'size', label: '文件大小' },
+              { prop: 'createBy', label: '申请人' },
+              { prop: 'createTime', label: '提交时间', 'min-width': 100 },
+              { prop: 'ai', label: 'AI审核', type: 'tag', width: 100 },
+              {
+                label: '审核状态',
+                type: 'tag',
+                render ({ status }) {
+                  return {
+                    type: ['warning', 'warning', 'success', 'danger'][status],
+                    label: ['未审核', '未审核', '通过', '驳回'][status]
+                  }
+                }
+              },
+              {
+                type: 'invoke',
+                width: 80,
+                render: [{ label: '审核', on: this.onView }]
+              }
+            ]
+          }
+        case front2back['program']:
+          return {
+            singlePage: true,
+            condition: { status: State.REVIEW },
+            list: this.getList('program'),
+            // transform: this.transform,
+            cols: [
+              { prop: 'name', label: '节目名称', 'min-width': 100 },
+              { prop: 'resolutionRatio', label: '分辨率' },
+              { prop: 'createBy', label: '申请人' },
+              { prop: 'createTime', label: '提交时间' },
+              {
+                label: '审核状态',
+                type: 'tag',
+                render ({ status }) {
+                  return {
+                    type: ['warning', 'warning', 'success', 'danger'][status],
+                    label: ['未审核', '未审核', '通过', '驳回'][status]
+                  }
+                }
+              },
+              {
+                type: 'invoke',
+                width: 160,
+                render: [
+                  { label: '查看', on: this.onView },
+                  {
+                    label: '通过',
+                    render ({ status }) {
+                      return showOpt(status)
+                    },
+                    on: this.onResolve
+                  },
+                  {
+                    label: '驳回',
+                    render ({ status }) {
+                      return showOpt(status)
+                    },
+                    on: this.onReject
+                  }
+                ]
+              }
+            ]
+          }
+        case front2back['programRecur']:
+        case front2back['schedule']:
+          return {
+            singlePage: true,
+            condition: {
+              type: ScheduleType.COMPLEX,
+              status: State.REVIEW
+            },
+            list: this.getList('schedule'),
+            cols: [
+              { prop: 'name', label: '排期名称', 'min-width': 100 },
+              { prop: 'resolutionRatio', label: '分辨率' },
+              { prop: 'createBy', label: '申请人' },
+              { prop: 'createTime', label: '提交时间' },
+              {
+                label: '审核状态',
+                type: 'tag',
+                render ({ status }) {
+                  return {
+                    type: ['warning', 'warning', 'success', 'danger'][status],
+                    label: ['未审核', '未审核', '通过', '驳回'][status]
+                  }
+                }
+              },
+              {
+                type: 'invoke',
+                width: 160,
+                render: [
+                  { label: '查看', on: this.onView },
+                  {
+                    label: '通过',
+                    render ({ status }) {
+                      return showOpt(status)
+                    },
+                    on: this.onResolve
+                  },
+                  {
+                    label: '驳回',
+                    render ({ status }) {
+                      return showOpt(status)
+                    },
+                    on: this.onReject
+                  }
+                ]
+              }
+            ]
+          }
+        case front2back['publish']:
+          return {
+            singlePage: true,
+            condition: { status: State.REVIEW },
+            list: this.getList('publish'),
+            // transform: this.transform,
+            cols: [
+              {
+                prop: 'expand',
+                type: 'expand',
+                render (data, h) {
+                  return h(
+                    'div',
+                    {
+                      staticClass: 'o-info'
+                    },
+                    [
+                      h('div', null, data.desc),
+                      h('div', null, `设备:${data.device}`)
+                    ]
+                  )
+                }
+              },
+              { prop: 'type', label: '类型', width: 100 },
+              { prop: 'name', label: '名称', 'min-width': 100 },
+              { prop: 'resolutionRatio', label: '分辨率' },
+              { prop: 'createBy', label: '申请人' },
+              { prop: 'createTime', label: '提交时间' },
+              {
+                type: 'invoke',
+                width: 160,
+                render: [
+                  { label: '查看', on: this.onView },
+                  {
+                    label: '通过',
+                    render ({ status }) {
+                      return showOpt(status)
+                    },
+                    on: this.onPublish
+                  },
+                  {
+                    label: '驳回',
+                    render ({ status }) {
+                      return showOpt(status)
+                    },
+                    on: this.onReject
+                  }
+                ]
+              }
+            ]
+          }
+        default:
+          return {}
+      }
+    }
+  },
+  mounted () {
+    this.getPublishWorkflowDetail()
+  },
+  methods: {
+    // 判断是否需要缓存驳回
+    calTempReject (item) {
+      if (!singleMap[item]) {
+        this.tempRejectMap[item] = {
+          text: `${data2label[item]}驳回`,
+          show:
+            !this.tempRejectMap.length
+            && this.sourceMap[item].filter(i => ![2, 3].includes(i.status))
+              .length
+        }
+      }
+    },
+    async getPublishWorkflowDetail () {
+      let res = await getPublishWorkflowDetail(this.id).catch(err => {
+        console.log(err)
+      })
+      if (!res || !res.success) {
+        this.error = true
+        return
+      }
+      this.error = false
+      res = res.data
+      for (const key in res) {
+        if (Object.prototype.hasOwnProperty.call(res, key)) {
+          const element = res[key]
+          if (!Array.isArray(element)) {
+            res[key] = [element]
+          }
+        }
+      }
+
+      this.sourceMap = res
+      // init
+      // 判断步骤 filter datamap 并transform row
+      const temp = []
+      for (const item of allDataMap) {
+        if (this.sourceMap[item][0]) {
+          this.sourceMap[item] = this.sourceMap[item].map(row => this.transform(row, item))
+          temp.push(item)
+          // 处理缓存驳回初始状态
+          if (!singleMap[item]) {
+            this.tempRejectMap[item] = {
+              text: `${data2label[item]}驳回`,
+              length: this.sourceMap[item].filter(
+                i => ![2, 3].includes(i.status)
+              ).length
+            }
+          }
+        }
+      }
+      this.dataMap = temp
+      // 确定节点
+      let active = 0
+      for (const item of this.dataMap) {
+        if (this.sourceMap[item].every(i => i.status === 2)) {
+          active++
+          continue
+        }
+        break
+      }
+      this.active = active
+      if (this.active === this.totalStep - 1) {
+        this.active = this.totalStep
+      }
+      // this.$refs.table.onPagination();
+    },
+    refresh () {
+      this.getPublishWorkflowDetail()
+    },
+    refreshStatus (item, status, review) {
+      const list = this.sourceMap[this.backDataType]
+      const index = list.findIndex(i => i.id === item.id)
+      list[index].status = status
+      this.$refs.table.onPagination()
+      if (status === 2 && this.tableData.every(i => i.status === 2)) {
+        this.closePreviewDialog()
+        this.nextStep()
+      }
+      if (status === 3) {
+        list[index].review = review
+      }
+    },
+    getList () {
+      return () => Promise.resolve({ data: this.tableData })
+    },
+    // 全部审批通过
+    nextStep () {
+      this.active += 1
+      if (this.active === this.totalStep - 1) {
+        this.active = this.totalStep
+      }
+      this.tempRejectInfo = []
+      this.tableKey++
+    },
+    next () {
+      if (this.active++ > 5) {
+        this.active = 0
+      }
+      this.tableKey++
+    },
+    transform (row, kind) {
+      const { type } = row
+      switch (kind) {
+        case front2back['assets']:
+          row.typeName = [null, '图片', '视频', '音频'][type]
+          row.file = {
+            type: row.type,
+            url: row.keyName,
+            thumbnail: row.thumbnail
+          }
+          row.duration = parseDuration(row.duration)
+          row.size = parseByte(row.size)
+          row.createBy = row.userName || row.createBy
+          row.ai = mediaMixin.methods.getAIState(row)
+          row.id = row.keyName
+          return row
+        case front2back['program']:
+          row.createBy = row.userName || row.createBy
+          return row
+        case front2back['publish']:
+          return { ...this.getSame(row), ...this.getDiff(row) }
+        default:
+          return row
+      }
+    },
+    // publish 排期辅助函数
+    getSame ({
+      id,
+      programCalendarName,
+      resolutionRatio,
+      createBy,
+      createByUsername,
+      createTime,
+      calendarReleaseDeviceList
+    }) {
+      return {
+        id,
+        name: programCalendarName,
+        resolutionRatio,
+        createBy: createByUsername || createBy,
+        createTime,
+        device: calendarReleaseDeviceList
+          ?.map(item => item.deviceName)
+          .join(',')
+      }
+    },
+    getDiff (item) {
+      const target = JSON.parse(item.target)
+      let type = ''
+      switch (target.type) {
+        case PublishType.CALENDAR:
+          type = '排期'
+          break
+        case PublishType.EVENT:
+          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          break
+        default:
+          break
+      }
+      return {
+        type,
+        target,
+        desc: this.getDesc(target)
+      }
+    },
+    getDesc (target) {
+      if (
+        target.type === PublishType.EVENT
+        && target.detail.priority === EventPriority.INSERTED
+      ) {
+        const { freq, start, until, byDay, startTime, endTime } = target.detail
+        switch (freq) {
+          case EventFreq.WEEKLY:
+            return `自${start.split(' ')[0]}开始${
+              until ? `${until.split(' ')[0]}前` : ''
+            } 每周${byDay
+              .split(',')
+              .map(val => ['日', '一', '二', '三', '四', '五', '六'][val])
+              .join('、')} ${startTime} - ${endTime}`
+          default:
+            return until ? `${start} - ${until}` : `自${start}开始`
+        }
+      }
+      return ''
+    },
+    onToggle (row) {
+      if (this.backDataType !== 'publish') {
+        return
+      }
+      this.$refs.table.getInst().toggleRowExpansion(row)
+    },
+    onPublishView ({ target: { type, detail } }) {
+      switch (type) {
+        case PublishType.CALENDAR:
+          this.viewSchedule(detail)
+          break
+        case PublishType.EVENT:
+          if (detail.target.type === EventTarget.RECUR) {
+            this.viewSchedule(detail.target.id)
+          } else {
+            this.viewProgram(detail.target.id)
+          }
+          break
+        default:
+          break
+      }
+    },
+    viewSchedule (id) {
+      this.$refs.scheduleDialog.show(id)
+    },
+    viewProgram (id) {
+      window.open(
+        this.$router.resolve({
+          name: 'program',
+          params: { id }
+        }).href,
+        '_blank'
+      )
+    },
+    // 查看
+    onView (row) {
+      const { id } = row
+      switch (this.backDataType) {
+        case front2back['assets']:
+          this.$refs.reviewDialog.show(
+            this.tableData.findIndex(i => i.id === id)
+          )
+          break
+        case front2back['program']:
+          window.open(
+            this.$router.resolve({
+              name: 'program',
+              params: { id }
+            }).href,
+            '_blank'
+          )
+          break
+        case front2back['schedule']:
+        case front2back['programRecur']:
+          this.$refs.scheduleDialog.show(id)
+          break
+        case front2back['publish']:
+          this.onPublishView(row)
+          break
+        default:
+          break
+      }
+    },
+    onViewAsset (asset) {
+      this.$refs.previewDialog.show(asset)
+    },
+    closePreviewDialog () {
+      if (this.backDataType === front2back['assets']) {
+        this.$refs.reviewDialog.close()
+      }
+    },
+    // 通过
+    onResolve (item) {
+      this.resolve(item).then(() => {
+        this.refreshStatus(item, 2)
+      })
+    },
+    resolve (item) {
+      switch (this.backDataType) {
+        case front2back['assets']:
+          return resolveAsset(item)
+        case front2back['program']:
+          return resolveProgram(item)
+        case front2back['schedule']:
+        case front2back['programRecur']:
+          return resolveSchedule(item)
+        case front2back['publish']:
+          return resolvePublish(item)
+        default:
+          return new Promise().reject()
+      }
+    },
+    // 行内
+    // 驳回 列表按钮触发
+    onReject (item) {
+      this.$item = item
+      this.review = {
+        type: 'reject',
+        reason: ''
+      }
+      this.$refs.rejectDialog.show()
+    },
+    // dialog 直接触发
+    onConfirmReject (done, props) {
+      if (props) {
+        const { item, review } = props
+        this.$item = item
+        this.review = review
+      }
+      const reason =
+        this.review.type === 'reject' ? this.review.reason : this.review.type
+      if (!reason) {
+        this.$message({
+          type: 'warning',
+          message: '请选择或填写驳回原因'
+        })
+        return
+      }
+      const rejectInfo = {
+        id: this.$item.id,
+        name: this.$item.name || this.$item.originalName,
+        remark: reason
+      }
+      if (this.showTempReject) {
+        this.$confirm(`驳回 ${rejectInfo.name} ?`, {
+          type: 'warning'
+        }).then(() => {
+          // 缓存驳回数据
+          done()
+          this.tempRejectInfo.push(rejectInfo)
+          this.refreshStatus(this.$item, 3, {
+            type: this.review.type,
+            reason: this.review.reason
+          })
+        })
+      } else {
+        // 取最新
+        done()
+        this.reject([rejectInfo], rejectInfo.name)
+      }
+    },
+    // 流程驳回
+    reject (
+      rejectInfo = this.tempRejectInfo,
+      name = data2label[this.backDataType]
+    ) {
+      return calendarPublishReject(
+        this.id,
+        {
+          type: data2type[this.backDataType],
+          rejectInfo
+        },
+        name
+      )
+        .then(() => {
+          this.$router.push({ name: 'review-list' })
+        })
+        .catch(err => {
+          console.log(err)
+        })
+    },
+
+    // 确认发布
+    onPublish () {
+      resolvePublish({
+        name: this.tableData[0].programCalendarName,
+        id: this.tableData[0].id
+      }).then(() => {
+        this.nextStep()
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.o-card {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  width: 100%;
+  padding-top: 100px;
+  &__icon {
+    color: #04a681;
+    font-size: 48px;
+  }
+  &__title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+    line-height: 36px;
+  }
+  &__desc {
+    font-weight: 400;
+    color: #d5d9e4;
+    line-height: 36px;
+    font-size: 14px;
+    padding-left: 15px;
+  }
+}
+.o-title {
+  font-size: 18px;
+  font-weight: bold;
+  color: #333333;
+}
+.fl-end {
+  justify-content: flex-end;
+}
+</style>

+ 13 - 1
src/views/review/index.vue

@@ -29,6 +29,14 @@
         label="发布审核"
         name="ReviewPublish"
       />
+      <el-tab-pane
+        label="流程审批"
+        name="ReviewWorkflow"
+      />
+      <el-tab-pane
+        label="我的流程"
+        name="MyWorkflow"
+      />
     </el-tabs>
     <component
       :is="active"
@@ -82,6 +90,8 @@ import ReviewProgram from './components/ReviewProgram'
 import ReviewProgramRecur from './components/ReviewProgramRecur'
 import ReviewSchedule from './components/ReviewSchedule'
 import ReviewPublish from './components/ReviewPublish'
+import ReviewWorkflow from './components/ReviewWorkflow'
+import MyWorkflow from './components/MyWorkflow'
 
 export default {
   name: 'Review',
@@ -90,7 +100,9 @@ export default {
     ReviewProgram,
     ReviewProgramRecur,
     ReviewSchedule,
-    ReviewPublish
+    ReviewPublish,
+    ReviewWorkflow,
+    MyWorkflow
   },
   data () {
     return {