Parcourir la source

feat: publish the material package according to the aspect ratio

Casper Dai il y a 3 ans
Parent
commit
07c6a1c3c6
44 fichiers modifiés avec 1006 ajouts et 399 suppressions
  1. 3 2
      src/api/device.js
  2. 18 6
      src/api/platform.js
  3. 2 2
      src/components/dialog/EventTargetDialog/index.vue
  4. 71 7
      src/components/dialog/MaterailDialog/index.vue
  5. 15 45
      src/components/dialog/TaskTargetDialog/index.vue
  6. 2 2
      src/components/service/Schedule/ScheduleSwiper/index.vue
  7. 3 3
      src/components/table/Table/Column.vue
  8. 0 1
      src/components/tree/DeviceTreeSingle/index.vue
  9. 127 0
      src/components/tree/RatioTreeSingle/index.vue
  10. 8 3
      src/constant.js
  11. 1 1
      src/layout/components/Sidebar/index.vue
  12. 82 0
      src/mixins/asset-table.js
  13. 18 11
      src/router/index.js
  14. 4 0
      src/scss/base/_cover.scss
  15. 18 0
      src/scss/bem/_ishas.scss
  16. 5 0
      src/scss/bem/_utility.scss
  17. 11 48
      src/views/ad/contract/index.vue
  18. 4 7
      src/views/ad/history/index.vue
  19. 2 5
      src/views/ad/review-asset/index.vue
  20. 3 6
      src/views/ad/review-order/index.vue
  21. 24 14
      src/views/ad/scheduling/index.vue
  22. 35 38
      src/views/ad/task/index.vue
  23. 9 10
      src/views/dashboard/v1/MessageNotice.vue
  24. 3 3
      src/views/device/detail/components/DeviceAlarm.vue
  25. 9 9
      src/views/device/detail/dashboard/Timeline.vue
  26. 21 12
      src/views/device/index.vue
  27. 4 21
      src/views/device/timeline/index.vue
  28. 15 3
      src/views/realm/ai-timing/components/GroupTimingTable.vue
  29. 7 38
      src/views/realm/dataset/components/DatasetTable.vue
  30. 2 5
      src/views/realm/device/settings/components/DatasetConfigDialog.vue
  31. 37 31
      src/views/screen/deploy/device/index.vue
  32. 366 0
      src/views/screen/deploy/ratio/index.vue
  33. 1 0
      src/views/screen/material/media/MediaDesigner.vue
  34. 3 1
      src/views/screen/material/media/MediaViewer.vue
  35. 1 8
      src/views/screen/material/media/index.vue
  36. 4 7
      src/views/screen/material/media/mixin.js
  37. 7 5
      src/views/screen/review/history/index.vue
  38. 5 8
      src/views/screen/review/single/components/ReviewAsset.vue
  39. 33 10
      src/views/screen/review/utils.js
  40. 2 5
      src/views/screen/review/workflow/detail/components/ReviewAssets.vue
  41. 1 1
      src/views/screen/review/workflow/detail/components/ReviewPrograms.vue
  42. 5 3
      src/views/screen/review/workflow/detail/components/ReviewPublish.vue
  43. 6 11
      src/views/screen/review/workflow/index.vue
  44. 9 7
      src/views/screen/review/workflow/mine/index.vue

+ 3 - 2
src/api/device.js

@@ -365,11 +365,12 @@ export function getDeviceAlarms (query) {
       data: data.map(({ id, deviceName, level, pic, picUrl, type, handle, status, happenTime, ...item }) => {
         const alarm = {
           id, deviceName, level, happenTime,
-          asset: pic && picUrl
+          file: pic && picUrl
             ? {
               type: AssetType.IMAGE,
               url: picUrl,
-              thumb: picUrl
+              thumb: picUrl,
+              origin: true
             }
             : null,
           type: getType(type),

+ 18 - 6
src/api/platform.js

@@ -1,3 +1,4 @@
+import { PublishType } from '@/constant'
 import request, { tenantRequest } from '@/utils/request'
 import {
   send,
@@ -10,15 +11,26 @@ import {
 } from './base'
 
 // 发布
-export function publish (deviceIds, target, options) {
+export function publish (type, ids, publishTarget, data) {
+  const requestData = addOrg({
+    type,
+    target: JSON.stringify(publishTarget),
+    ...data
+  })
+  switch (type) {
+    case PublishType.DEVICE:
+      requestData.deviceIds = ids
+      break
+    case PublishType.PRODUCT_TYPE:
+      requestData.productTypeIds = ids
+      break
+    default:
+      break
+  }
   return messageSend({
     url: '/orchestration/calendarReleaseScheduling',
     method: 'POST',
-    data: addOrg({
-      deviceIds,
-      target: JSON.stringify(target),
-      ...options
-    })
+    data: requestData
   }, '发布', tenantRequest)
 }
 

+ 2 - 2
src/components/dialog/EventTargetDialog/index.vue

@@ -42,9 +42,9 @@ export default {
           { key: 'name', type: 'search', placeholder: '节目名称' }
         ],
         cols: [
-          { label: '缩略图', type: 'asset', render ({ id, img }) {
+          { label: '缩略图', type: 'asset', render ({ img }) {
             return img
-              ? { id, thumb: img }
+              ? { thumb: img }
               : null
           }, on: this.onView },
           { prop: 'name', label: '节目名称' },

+ 71 - 7
src/components/dialog/MaterailDialog/index.vue

@@ -1,6 +1,7 @@
 <template>
   <el-dialog
     :visible.sync="isPreviewing"
+    :title="title"
     :custom-class="customClass"
     :before-close="onCloseDialog"
     :close-on-click-modal="false"
@@ -17,6 +18,16 @@
       class="l-flex__auto has-padding"
       :schedule="scheduleId"
     />
+    <schema-table
+      v-if="assets"
+      class="c-sibling-item far"
+      :schema="contentSchema"
+    >
+      <preview-dialog
+        ref="previewDialog"
+        append-to-body
+      />
+    </schema-table>
     <i
       class="l-flex__none c-dialog--preview__close el-icon-close has-active u-bold u-pointer"
       @click="onCloseDialog"
@@ -25,11 +36,15 @@
 </template>
 
 <script>
-import { getProgram } from '@/api/program'
 import {
-  PublishType,
-  EventTarget
+  PublishTargetType,
+  EventTarget,
+  AssetTagInfo,
+  AssetType,
+  AssetTypeInfo
 } from '@/constant'
+import { parseDuration } from '@/utils'
+import { getProgram } from '@/api/program'
 import Program from '@/views/screen/material/program/ast/Previewer'
 
 export default {
@@ -42,11 +57,30 @@ export default {
     return {
       program: null,
       scheduleId: null,
-      isPreviewing: false
+      assets: null,
+      isPreviewing: false,
+      contentSchema: {
+        list: this.getContentAssets,
+        cols: [
+          { prop: 'tagType', label: '文件', width: 100, align: 'center' },
+          { prop: 'fileType', label: '', width: 80 },
+          { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
+          { prop: 'adDuration', label: '上播时长', align: 'center' },
+          { type: 'invoke', render: [
+            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
+          ] }
+        ]
+      }
     }
   },
   computed: {
+    title () {
+      return this.assets ? '上播内容' : ''
+    },
     customClass () {
+      if (this.assets) {
+        return 'c-dialog'
+      }
       return `c-dialog--preview ${this.program ? 'program' : ''}`
     }
   },
@@ -84,17 +118,17 @@ export default {
     },
     showPublishTarget ({ type, detail }) {
       switch (type) {
-        case PublishType.CALENDAR:
+        case PublishTargetType.CALENDAR:
           this.showSchedule(detail)
           break
-        case PublishType.EVENT:
+        case PublishTargetType.EVENT:
           this.showEventTarget(detail.target)
           break
         default:
           break
       }
     },
-    showEventTarget ({ type, id }) {
+    showEventTarget ({ type, id, sources }) {
       switch (type) {
         case EventTarget.PROGRAM:
           this.showProgram(id)
@@ -102,16 +136,46 @@ export default {
         case EventTarget.RECUR:
           this.showSchedule(id)
           break
+        case EventTarget.ASSETS:
+          this.showAssets(sources)
+          break
         default:
           break
       }
     },
+    showAssets (assets) {
+      assets = assets.map(this.transformAsset)
+      if (assets.length === 1) {
+        assets[0].adDuration = '独占'
+      }
+      this.assets = assets
+      this.isPreviewing = true
+    },
+    getContentAssets () {
+      return Promise.resolve({ data: this.assets })
+    },
+    transformAsset ({ tag, type, keyName, duration }) {
+      return {
+        tagType: AssetTagInfo[tag],
+        fileType: AssetTypeInfo[type],
+        file: {
+          type,
+          url: keyName,
+          thumb: type === AssetType.IMAGE ? keyName : null
+        },
+        adDuration: parseDuration(duration)
+      }
+    },
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
+    },
     onCloseDialog () {
       this.isPreviewing = false
     },
     onClosed () {
       this.program = null
       this.scheduleId = null
+      this.assets = null
     }
   }
 }

+ 15 - 45
src/components/dialog/TaskTargetDialog/index.vue

@@ -1,6 +1,5 @@
 <template>
   <el-dialog
-    ref="tableDialog"
     :visible.sync="choosing"
     title="上播内容选择"
     custom-class="c-dialog"
@@ -20,49 +19,23 @@
 </template>
 
 <script>
-import {
-  State,
-  AssetTag,
-  AssetType,
-  AssetTypeInfo,
-  TaskFromType
-} from '@/constant'
+import { TaskFromType } from '@/constant'
 import {
   parseDuration,
   getTaskTimeInfo,
-  offsetDate
+  offsetDate,
+  getAssetDuration
 } from '@/utils'
-import {
-  getAssets,
-  getContracts
-} from '@/api/asset'
+import { getContracts } from '@/api/asset'
+import { assetTableMixin } from '@/mixins/asset-table'
 
 export default {
   name: 'TaskTargetDialog',
+  mixins: [assetTableMixin],
   data () {
     return {
       type: TaskFromType.ASSET,
       choosing: false,
-      assetSchema: {
-        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.PUBLICITY, type: AssetType.IMAGE, originalName: '' },
-        list: getAssets,
-        filters: [
-          { key: 'type', type: 'select', options: [
-            { value: AssetType.IMAGE, label: AssetTypeInfo[AssetType.IMAGE] },
-            { value: AssetType.VIDEO, label: AssetTypeInfo[AssetType.VIDEO] },
-            { value: AssetType.STREAMING_MEDIA, label: AssetTypeInfo[AssetType.STREAMING_MEDIA] }
-          ] },
-          { key: 'originalName', type: 'search', placeholder: '素材名称' }
-        ],
-        cols: [
-          { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
-          { prop: 'originalName', label: '' },
-          { prop: 'diff', label: '其他', 'align': 'right' },
-          { type: 'invoke', render: [
-            { label: '查看', on: this.onViewAssetItem }
-          ] }
-        ]
-      },
       contractSchema: {
         condition: { name: '' },
         list: getContracts,
@@ -87,7 +60,7 @@ export default {
     schema () {
       switch (this.type) {
         case TaskFromType.ASSET:
-          return this.assetSchema
+          return this.assetTableSchema
         case TaskFromType.CONTRACT:
           return this.contractSchema
         default:
@@ -96,7 +69,7 @@ export default {
     }
   },
   methods: {
-    show (type) {
+    show (type = TaskFromType.ASSET) {
       this.type = type
       this.choosing = true
     },
@@ -112,15 +85,18 @@ export default {
         case TaskFromType.ASSET:
           data.fromId = item.keyName
           data.name = item.originalName
-          data.type = item.type
-          if (item.duration) {
-            data.info = { duration: item.duration }
+          data.info = {
+            tag: item.tag,
+            type: item.type,
+            name: item.originalName,
+            size: item.size,
+            md5: item.md5,
+            duration: getAssetDuration(item)
           }
           break
         case TaskFromType.CONTRACT:
           data.fromId = item.id
           data.name = item.name
-          data.type = -1
           data.info = {
             startDate: item.startDate,
             day: item.day,
@@ -139,12 +115,6 @@ export default {
           this.choosing = false
         }
       })
-    },
-    onViewAssetItem ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
     }
   }
 }

+ 2 - 2
src/components/service/Schedule/ScheduleSwiper/index.vue

@@ -124,9 +124,9 @@ export default {
         ],
         cols: [
           { type: 'selection' },
-          { label: '缩略图', type: 'asset', render ({ id, img }) {
+          { label: '缩略图', type: 'asset', render ({ img }) {
             return img
-              ? { id, thumb: img }
+              ? { thumb: img }
               : null
           }, on: this.onView },
           { prop: 'name', label: '节目名称' }

+ 3 - 3
src/components/table/Table/Column.vue

@@ -85,12 +85,12 @@ export default {
         return this.$createElement('auto-image', {
           staticClass: `o-thumbnail u-font-size--lg u-color--blue ${on ? 'u-pointer' : ''}`,
           props: {
-            src: value.origin ? getAssetUrl(value.thumb) : getThumbnailUrl(value.thumb, '64,fit'),
+            src: value.origin ? getAssetUrl(value.thumb) : getThumbnailUrl(value.thumb, '80,fit'),
             broken: 'image-broken'
           },
           nativeOn: on && { click: $event => {
             $event.stopPropagation()
-            on(value, data)
+            on(data, value)
           } }
         })
       }
@@ -108,7 +108,7 @@ export default {
         props: { 'icon-class': svgIcon },
         on: on && { click: $event => {
           $event.stopPropagation()
-          on(value, data)
+          on(data, value)
         } }
       })
     },

+ 0 - 1
src/components/tree/DeviceTreeSingle/index.vue

@@ -183,7 +183,6 @@ export default {
   flex: 1 1 0;
   min-width: 200px;
   padding-right: $spacing;
-  margin-right: $spacing;
   border-right: 1px solid $gray--light;
 }
 

+ 127 - 0
src/components/tree/RatioTreeSingle/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <div
+    v-loading="loading"
+    class="l-flex__none l-flex--col c-tree"
+    :class="size"
+  >
+    <div class="l-flex__none c-sibling-item--v u-bold">宽高比</div>
+    <warning
+      v-if="error"
+      @click="getProductTypes"
+    />
+    <template v-else>
+      <div
+        v-if="isEmpty"
+        class="c-tree__empty"
+      >
+        暂无设备
+      </div>
+      <div
+        v-else
+        class="l-flex__auto l-flex--col c-sibling-item--v u-overflow-y--auto"
+      >
+        <div
+          v-for="productType in productTypes"
+          :key="productType.id"
+          :class="{ selected: productType.selected }"
+          class="l-flex__none c-tree__content u-ellipsis u-pointer"
+          @click="onToggle(productType)"
+        >
+          <span class="l-flex__fill">{{ productType.name }}</span>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { getProductTypes } from '@/api/device'
+
+export default {
+  name: 'RatioTreeSingle',
+  props: {
+    size: {
+      type: String,
+      default: ''
+    }
+  },
+  data () {
+    return {
+      loading: false,
+      error: false,
+      productTypes: [],
+      selectedItem: null
+    }
+  },
+  computed: {
+    isEmpty () {
+      return this.productTypes.length === 0
+    }
+  },
+  watch: {
+    active: {
+      handler () {
+        this.getProductTypes()
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    onToggle (productType) {
+      if (!this.selectedItem || this.selectedItem.id !== productType.id) {
+        if (this.selectedItem) {
+          this.selectedItem.selected = false
+        }
+        this.selectedItem = productType
+        this.selectedItem.selected = true
+        this.$emit('change', { ...productType })
+      }
+    },
+    getProductTypes () {
+      this.loading = true
+      this.error = false
+      getProductTypes({
+        pageSize: 10,
+        pageNum: 1
+      }).then(({ data, totalCount }) => {
+        if (totalCount <= 10) {
+          return { data }
+        }
+        return getProductTypes({
+          pageSize: totalCount,
+          pageNum: 1
+        })
+      }).then(
+        ({ data }) => {
+          this.loading = false
+          this.productTypes = data.map(({ id, name }) => {
+            return {
+              id,
+              name,
+              selected: false
+            }
+          })
+        },
+        () => {
+          this.error = true
+          this.loading = false
+        }
+      )
+    },
+    reset () {
+      if (this.selectedItem) {
+        this.selectedItem.selected = false
+        this.selectedItem = null
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-tree {
+  width: 200px;
+  padding-right: $spacing;
+  border-right: 1px solid $gray--light;
+}
+</style>

+ 8 - 3
src/constant.js

@@ -72,6 +72,11 @@ export const ScheduleType = {
 }
 
 export const PublishType = {
+  DEVICE: 1,
+  PRODUCT_TYPE: 3
+}
+
+export const PublishTargetType = {
   CALENDAR: 1,
   EVENT: 2
 }
@@ -86,9 +91,9 @@ export const EventPriority = {
 }
 
 export const EventPriorityDescription = {
-  [EventPriority.SHIM]: '默认播放',
-  [EventPriority.SCHEDULING]: '排期',
-  [EventPriority.INSERTED]: '插播',
+  [EventPriority.SHIM]: '默认',
+  [EventPriority.SCHEDULING]: '常规',
+  [EventPriority.INSERTED]: '临时',
   [EventPriority.AUTO_PROGRAMME]: '节目单',
   [EventPriority.EMBEDDED]: '预埋',
   [EventPriority.EMERGENT]: '紧急'

+ 1 - 1
src/layout/components/Sidebar/index.vue

@@ -167,7 +167,7 @@ export default {
   }
 }
 
-@media screen and (max-width: 1440px) {
+@media screen and (max-width: 1920px) {
   .c-sidebar {
     width: 200px;
 

+ 82 - 0
src/mixins/asset-table.js

@@ -0,0 +1,82 @@
+import {
+  State,
+  AssetTag,
+  AssetTagInfo,
+  AssetType,
+  AssetTypeInfo
+} from '@/constant'
+import { getAssets } from '@/api/asset'
+
+export const assetTableMixin = {
+  data () {
+    return {
+      assetTableSchema: {
+        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.AD, type: AssetType.IMAGE, originalName: '' },
+        list: getAssets,
+        filters: [
+          { key: 'tag', type: 'select', options: [
+            { value: AssetTag.AD, label: AssetTagInfo[AssetTag.AD] },
+            { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
+            { value: AssetTag.LOCAL_PUBLICITY, label: AssetTagInfo[AssetTag.LOCAL_PUBLICITY] }
+          ] },
+          { key: 'type', type: 'select', options: [
+            { value: AssetType.IMAGE, label: AssetTypeInfo[AssetType.IMAGE] },
+            { value: AssetType.VIDEO, label: AssetTypeInfo[AssetType.VIDEO] },
+            { value: AssetType.STREAMING_MEDIA, label: AssetTypeInfo[AssetType.STREAMING_MEDIA] }
+          ] },
+          { key: 'originalName', type: 'search', placeholder: '素材名称' }
+        ],
+        cols: [
+          { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
+          { prop: 'originalName', label: '' },
+          { prop: 'diff', label: '其他', 'align': 'right' },
+          { type: 'invoke', render: [
+            { label: '查看', on: this.onViewAsset }
+          ] }
+        ]
+      }
+    }
+  },
+  methods: {
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
+    }
+  }
+}
+
+export const assetPublicTableMixin = {
+  data () {
+    return {
+      assetTableSchema: {
+        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.PUBLICITY, type: AssetType.IMAGE, originalName: '' },
+        list: getAssets,
+        filters: [
+          { key: 'tag', type: 'select', options: [
+            { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
+            { value: AssetTag.LOCAL_PUBLICITY, label: AssetTagInfo[AssetTag.LOCAL_PUBLICITY] },
+            { value: AssetTag.SHIM, label: AssetTagInfo[AssetTag.SHIM] }
+          ] },
+          { key: 'type', type: 'select', options: [
+            { value: AssetType.IMAGE, label: AssetTypeInfo[AssetType.IMAGE] },
+            { value: AssetType.VIDEO, label: AssetTypeInfo[AssetType.VIDEO] },
+            { value: AssetType.STREAMING_MEDIA, label: AssetTypeInfo[AssetType.STREAMING_MEDIA] }
+          ] },
+          { key: 'originalName', type: 'search', placeholder: '素材名称' }
+        ],
+        cols: [
+          { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
+          { prop: 'originalName', label: '' },
+          { prop: 'diff', label: '其他', 'align': 'right' },
+          { type: 'invoke', render: [
+            { label: '查看', on: this.onViewAsset }
+          ] }
+        ]
+      }
+    }
+  },
+  methods: {
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
+    }
+  }
+}

+ 18 - 11
src/router/index.js

@@ -142,11 +142,18 @@ export const asyncRoutes = [
     meta: { title: '智能信发', icon: 'cm' },
     children: [
       {
-        name: 'deploy',
-        path: 'deploy',
-        component: () => import('@/views/screen/deploy/index'),
+        name: 'deploy-device',
+        path: 'deploy/device',
+        component: () => import('@/views/screen/deploy/device/index'),
+        access: Access.MANAGE_CALENDAR,
+        meta: { title: '按设备发布' }
+      },
+      {
+        name: 'deploy-ratio',
+        path: 'deploy/ratio',
+        component: () => import('@/views/screen/deploy/ratio/index'),
         access: Access.MANAGE_CALENDAR,
-        meta: { title: '发布' }
+        meta: { title: '按宽高比发布' }
       },
       {
         path: 'mine',
@@ -233,7 +240,7 @@ export const asyncRoutes = [
     path: '/task',
     component: Layout,
     meta: { title: '自动编排', icon: 'ad' },
-    access: [Access.MANAGE_TENANTS, Access.MANAGE_TENANT],
+    access: [Access.MANAGE_CALENDAR, Access.MANAGE_GROUP],
     children: [
       {
         name: 'contract',
@@ -252,6 +259,12 @@ export const asyncRoutes = [
         path: 'scheduling',
         component: () => import('@/views/ad/scheduling/index'),
         meta: { title: '节目单' }
+      },
+      {
+        name: 'dataset',
+        path: 'dataset',
+        component: () => import('@/views/realm/dataset/index'),
+        meta: { title: '填充素材包' }
       }
     ]
   },
@@ -345,12 +358,6 @@ export const asyncRoutes = [
         component: () => import('@/views/realm/tag/index'),
         meta: { title: '素材标签' }
       },
-      {
-        name: 'asset-dataset',
-        path: 'asset/dataset',
-        component: () => import('@/views/realm/dataset/index'),
-        meta: { title: '填充素材包' }
-      },
       {
         name: 'ai-stock',
         path: 'ai/stock',

+ 4 - 0
src/scss/base/_cover.scss

@@ -23,3 +23,7 @@
 .el-pagination__jump {
   margin-left: 10px;
 }
+
+.el-result {
+  padding: 32px 16px;
+}

+ 18 - 0
src/scss/bem/_ishas.scss

@@ -62,3 +62,21 @@
 .has-gap {
   border: $spacing solid transparent;
 }
+
+.is-priority1,
+.is-priority2 {
+  color: #8e929c;
+  background-color: #edf0f6;
+}
+
+.is-priority3,
+.is-priority4 {
+  color: #7642fd;
+  background-color: #eae2fe;
+}
+
+.is-priority5,
+.is-priority99 {
+  color: #ff2222;
+  background-color: #ffecec;
+}

+ 5 - 0
src/scss/bem/_utility.scss

@@ -138,3 +138,8 @@
 .u-font-size--3xl {
   font-size: $font-size--3xl  !important;
 }
+
+.u-required::before {
+  content: '*';
+  color: #ff0000;
+}

+ 11 - 48
src/views/ad/contract/index.vue

@@ -172,10 +172,9 @@
       </div>
     </confirm-dialog>
     <table-dialog
-      ref="assetDialog"
-      title="上播内容选择"
-      :schema="assetSchema"
-      @choosen="onChoosenAsset"
+      ref="assetTableDialog"
+      :schema="assetTableSchema"
+      @row-dblclick="onChoosenAsset"
     />
     <preview-dialog ref="previewDialog" />
   </wrapper>
@@ -183,11 +182,8 @@
 
 <script>
 import {
-  State,
-  AssetTag,
   AssetTagInfo,
   AssetType,
-  AssetTypeInfo,
   TimeType,
   SCREEN_TIME_KEY
 } from '@/constant'
@@ -202,7 +198,6 @@ import {
   getAssetDuration
 } from '@/utils'
 import {
-  getAssets,
   getAssetUrl,
   getContracts,
   addContract,
@@ -212,6 +207,7 @@ import {
   bindFileToContract,
   deleteFileFromContract
 } from '@/api/asset'
+import { assetTableMixin } from '@/mixins/asset-table'
 import Draggable from 'vuedraggable'
 
 export default {
@@ -219,6 +215,7 @@ export default {
   components: {
     Draggable
   },
+  mixins: [assetTableMixin],
   data () {
     return {
       schema: {
@@ -255,31 +252,6 @@ export default {
         ]
       },
       files: [],
-      assetSchema: {
-        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.AD, type: AssetType.IMAGE, originalName: '' },
-        list: getAssets,
-        filters: [
-          { key: 'tag', type: 'select', options: [
-            { value: AssetTag.AD, label: AssetTagInfo[AssetTag.AD] },
-            { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
-            { value: AssetTag.LOCAL_PUBLICITY, label: AssetTagInfo[AssetTag.LOCAL_PUBLICITY] }
-          ] },
-          { key: 'type', type: 'select', options: [
-            { value: AssetType.IMAGE, label: AssetTypeInfo[AssetType.IMAGE] },
-            { value: AssetType.VIDEO, label: AssetTypeInfo[AssetType.VIDEO] },
-            { value: AssetType.STREAMING_MEDIA, label: AssetTypeInfo[AssetType.STREAMING_MEDIA] }
-          ] },
-          { key: 'originalName', type: 'search', placeholder: '素材名称' }
-        ],
-        cols: [
-          { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
-          { prop: 'originalName', label: '' },
-          { prop: 'diff', label: '其他', 'align': 'right' },
-          { type: 'invoke', render: [
-            { label: '查看', on: this.onViewAssetItem }
-          ] }
-        ]
-      },
       assets: []
     }
   },
@@ -430,27 +402,18 @@ export default {
       })
     },
     onAddAsset () {
-      this.$refs.assetDialog.show()
-    },
-    onViewAssetItem ({ type, keyName }) {
-      this.onViewAsset({
-        type,
-        url: keyName
-      })
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
+      this.$refs.assetTableDialog.show()
     },
     onChoosenAsset ({ value, done }) {
-      const { keyName, tag, type, originalName } = value
+      const { tag, type, keyName, originalName } = value
       this.assets.push({
         key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
-        keyName,
         tagInfo: AssetTagInfo[tag],
-        type,
+        disabled: type === AssetType.VIDEO,
         name: originalName,
-        duration: getAssetDuration(value),
-        disabled: type === AssetType.VIDEO
+        type,
+        keyName,
+        duration: getAssetDuration(value)
       })
       done()
     },

+ 4 - 7
src/views/ad/history/index.vue

@@ -61,7 +61,7 @@ export default {
           { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
           { prop: 'adDuration', label: '上播时长', align: 'center' },
           { type: 'invoke', render: [
-            { label: '查看', allow: ({ file }) => !!file, on: this.onViewSource }
+            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
           ] }
         ]
       }
@@ -100,7 +100,7 @@ export default {
           file: {
             type,
             url: keyName,
-            thumb: isImage ? keyName : thumb || null,
+            thumb: isImage ? keyName : thumb,
             origin: !isImage
           },
           adDuration: parseDuration(adDuration)
@@ -110,11 +110,8 @@ export default {
           adDuration: parseDuration(adDuration)
         }
     },
-    onViewSource ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
     }
   }
 }

+ 2 - 5
src/views/ad/review-asset/index.vue

@@ -73,7 +73,7 @@ export default {
         cols: [
           { type: 'refresh' },
           { prop: 'fileType', label: '文件', align: 'center' },
-          { prop: 'file', label: '', type: 'asset', on: this.onView },
+          { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
           { prop: 'size', label: '文件大小', align: 'right' },
           { prop: 'ratio', label: '分辨率', align: 'right' },
           { prop: 'diff', label: '其他', align: 'right' },
@@ -140,10 +140,7 @@ export default {
         this.$refs.table.decrease(1)
       })
     },
-    onViewAsset (asset) {
-      this.onView(asset.file)
-    },
-    onView (file) {
+    onViewAsset ({ file }) {
       this.$refs.previewDialog.show(file)
     }
   }

+ 3 - 6
src/views/ad/review-order/index.vue

@@ -104,7 +104,7 @@ export default {
           { prop: 'ratio', label: '分辨率', align: 'right' },
           { prop: 'statusTag', type: 'tag' },
           { type: 'invoke', render: [
-            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAssetItem }
+            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
           ] }
         ]
       }
@@ -188,11 +188,8 @@ export default {
           adDuration: parseDuration(adDuration)
         }
     },
-    onViewAssetItem ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
     }
   }
 }

+ 24 - 14
src/views/ad/scheduling/index.vue

@@ -7,12 +7,14 @@
     background
   >
     <device-tree-single
+      class="c-sibling-item"
       size="mini"
       @change="onChange"
     />
     <schema-table
       v-if="deviceId"
       ref="table"
+      class="c-sibling-item far"
       row-key="id"
       :schema="schema"
     />
@@ -50,6 +52,7 @@
 
 <script>
 import {
+  Access,
   TaskFromType,
   TaskFromTypeInfo,
   AssetTagInfo,
@@ -78,12 +81,17 @@ const SchedulingStatus = {
 export default {
   name: 'AdScheduling',
   data () {
+    const canEditt = this.accessSet.has(Access.MANAGE_CALENDAR)
+    const canAudit = this.accessSet.has(Access.MANAGE_GROUP)
+
     return {
       deviceId: '',
       schema: {
-        buttons: [
-          { label: '一键排单', on: this.onScheduling }
-        ],
+        buttons: canEditt
+          ? [
+            { label: '一键排单', on: this.onScheduling }
+          ]
+          : null,
         condition: { order: 'startDate' },
         list: this.getDeviceSchedulings,
         transform: this.transform,
@@ -97,11 +105,14 @@ export default {
           { type: 'refresh' },
           { prop: 'startDate', label: '日期' },
           { prop: 'createTime', label: '生成时间' },
-          { prop: 'statusTag', type: 'tag', on: this.onToggle },
+          { prop: 'statusTag', type: 'tag', on: (canEditt || canAudit) && this.onToggle },
           { prop: 'remark', label: '备注', 'align': 'right' },
           { type: 'invoke', render: [
+            canAudit ? { label: '上架', render: ({ status }) => status === SchedulingStatus.RESOLVED, allow: ({ expired }) => !expired, on: this.onToggle } : null,
+            canAudit ? { label: '下架', render: ({ status }) => status === SchedulingStatus.CREATED, allow: ({ expired }) => !expired, on: this.onToggle } : null,
+            canEditt ? { label: '重编', render: ({ status }) => status === SchedulingStatus.ERROR, on: this.onToggle } : null,
             { label: '详情', on: this.onView }
-          ] }
+          ].filter(Boolean) }
         ]
       },
       schedulingId: '',
@@ -112,11 +123,11 @@ export default {
         list: this.getContentAssets,
         cols: [
           { prop: 'tagType', label: '文件', width: 100, align: 'center' },
-          { prop: 'fileType', label: '', width: 60 },
+          { prop: 'fileType', label: '', width: 80 },
           { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
           { prop: 'adDuration', label: '上播时长', align: 'center' },
           { type: 'invoke', render: [
-            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAssetItem }
+            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
           ] }
         ]
       }
@@ -187,7 +198,9 @@ export default {
       })
     },
     transform (scheduling) {
-      scheduling.statusTag = this.getStatusTag(scheduling.status, scheduling.startDate < parseTime(new Date(), '{y}-{m}-{d}'))
+      const expired = scheduling.startDate < parseTime(new Date(), '{y}-{m}-{d}')
+      scheduling.expired = expired
+      scheduling.statusTag = this.getStatusTag(scheduling.status, expired)
       return scheduling
     },
     getStatusTag (status, hasExpired) {
@@ -266,7 +279,7 @@ export default {
           break
         case TaskFromType.FILL:
         case TaskFromType.ASSET:
-          this.onViewAssetItem(detail)
+          this.onViewAsset(detail)
           break
         default:
           break
@@ -336,11 +349,8 @@ export default {
         adDuration: parseDuration(duration)
       }
     },
-    onViewAssetItem ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
     },
     onToggle (scheduling) {
       if (scheduling.status === SchedulingStatus.ERROR) {

+ 35 - 38
src/views/ad/task/index.vue

@@ -7,12 +7,14 @@
     background
   >
     <device-tree-single
+      class="c-sibling-item"
       size="mini"
       @change="onChange"
     />
     <schema-table
       v-if="device"
       ref="table"
+      class="c-sibling-item far"
       row-key="id"
       :schema="schema"
     />
@@ -137,6 +139,7 @@
 
 <script>
 import {
+  Access,
   TimeType,
   SCREEN_TIME_KEY,
   TaskFromType,
@@ -173,20 +176,14 @@ export default {
         list: this.getContentAssets,
         cols: [
           { prop: 'tagType', label: '文件', width: 100, align: 'center' },
-          { prop: 'fileType', label: '', width: 60 },
+          { prop: 'fileType', label: '', width: 80 },
           { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
           { prop: 'adDuration', label: '上播时长', align: 'center' },
           { type: 'invoke', render: [
-            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAssetItem }
+            { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
           ] }
         ]
       },
-      taskDataSelectOptions: {
-        options: [
-          { value: TaskFromType.ASSET, label: TaskFromTypeInfo[TaskFromType.ASSET] },
-          { value: TaskFromType.CONTRACT, label: TaskFromTypeInfo[TaskFromType.CONTRACT] }
-        ]
-      },
       taskTimeTypeSelectSchema: {
         options: [
           { value: TimeType.SCREEN, label: '开机期间' },
@@ -205,18 +202,23 @@ export default {
   },
   computed: {
     schema () {
+      const canEditt = this.accessSet.has(Access.MANAGE_CALENDAR)
+      const canAudit = this.accessSet.has(Access.MANAGE_GROUP)
+
       return {
-        buttons: [
-          { type: 'add', label: '素材任务', on: this.onAddAssetTask },
-          { type: 'add', label: '合同任务', on: this.onAddContractTask }
-        ],
+        buttons: canEditt
+          ? [
+            { type: 'add', label: '素材任务', on: this.onAddAssetTask },
+            { type: 'add', label: '合同任务', on: this.onAddContractTask }
+          ]
+          : null,
         condition: { deviceId: this.device.id },
         list: getTasks,
         transform: this.transform,
         cols: [
           { prop: 'type', type: 'refresh', width: 80 },
           { prop: 'name' },
-          { label: '上刊日期', 'min-width': 220, render: (data, h) => data.allowed && data.from === TaskFromType.ASSET
+          { label: '上刊日期', 'min-width': 220, render: (data, h) => canEditt && data.allowed && data.from === TaskFromType.ASSET
             ? h('el-date-picker', {
               staticClass: 'o-date-picker',
               props: {
@@ -232,7 +234,7 @@ export default {
               }
             })
             : `${data.startDate} 至 ${data.endDate}`, align: 'center' },
-          { label: '上播时间', render: (data, h) => data.allowed && data.from === TaskFromType.ASSET
+          { label: '上播时间', render: (data, h) => canEditt && data.allowed && data.from === TaskFromType.ASSET
             ? h('div', {
               staticClass: 'o-date-picker jc',
               on: {
@@ -240,7 +242,7 @@ export default {
               }
             }, data.range)
             : data.range, 'min-width': 120, align: 'center' },
-          { label: '上播时长(s)', render: (data, h) => this.canEditDuration(data)
+          { label: '上播时长(s)', render: (data, h) => canEditt && this.canEditDuration(data)
             ? h('edit-input', {
               staticClass: 'border',
               props: {
@@ -252,7 +254,7 @@ export default {
               }
             })
             : data.duration, 'min-width': 100, align: 'center' },
-          { label: '上播次数', render: (data, h) => data.allowed && data.from === TaskFromType.ASSET
+          { label: '上播次数', render: (data, h) => canEditt && data.allowed && data.from === TaskFromType.ASSET
             ? h('edit-input', {
               staticClass: 'border',
               props: {
@@ -264,7 +266,7 @@ export default {
               }
             })
             : data.count, align: 'center' },
-          { label: '审核次数', render: (data, h) => data.allowed
+          { label: '审核次数', render: (data, h) => canAudit && data.allowed
             ? h('edit-input', {
               staticClass: 'border',
               props: {
@@ -276,7 +278,7 @@ export default {
               }
             })
             : '-', align: 'center' },
-          { prop: 'tag', type: 'tag', on: this.onAudit },
+          { prop: 'tag', type: 'tag', on: canAudit && this.onAudit },
           { type: 'invoke', render: [
             { label: '查看', allow: ({ allowed }) => allowed, on: this.onViewTask },
             { label: '删除', allow: ({ from }) => from !== TaskFromType.ORDER, on: this.onDel }
@@ -288,7 +290,7 @@ export default {
       if (this.taskData) {
         switch (this.taskData.from) {
           case TaskFromType.ASSET:
-            return AssetTypeInfo[this.taskData.type]
+            return AssetTypeInfo[this.taskData.info.type]
           default:
             return TaskFromTypeInfo[this.taskData.from]
         }
@@ -302,7 +304,7 @@ export default {
       return this.taskDataFrom === TaskFromType.CONTRACT
     },
     disableTaskDuration () {
-      return !this.disableTask && this.taskData ? this.taskData.type === AssetType.VIDEO : this.disableTask
+      return !this.disableTask && this.taskData ? this.taskData.info.type === AssetType.VIDEO : this.disableTask
     }
   },
   methods: {
@@ -378,8 +380,10 @@ export default {
           break
         case TaskFromType.ASSET:
           this.onViewAsset({
-            type: task.fromInfo.type,
-            url: task.fromId
+            file: {
+              type: task.fromInfo.type,
+              url: task.fromId
+            }
           })
           break
         case TaskFromType.CONTRACT:
@@ -443,11 +447,8 @@ export default {
         adDuration: parseDuration(duration)
       }
     },
-    onViewAssetItem ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
     },
     onDel ({ id, type, name }) {
       deleteTask(id, `${type} ${name}`).then(() => {
@@ -492,22 +493,18 @@ export default {
     onChooseTaskTarget () {
       this.$refs.taskTargetDialog.show(this.taskDataFrom)
     },
-    onChoosenTaskTarget ({ value: { info, ...taskTarget }, done }) {
-      switch (taskTarget.from) {
+    onChoosenTaskTarget ({ value, done }) {
+      switch (value.from) {
         case TaskFromType.ASSET:
-          if (info?.duration) {
-            this.taskDuration = info.duration
-          } else if (this.taskData && this.taskData.type !== taskTarget.type) {
-            this.taskDuration = 5
-          }
-          this.taskData = taskTarget
+          this.taskDuration = value.info.duration
+          this.taskData = value
           done()
           break
         case TaskFromType.CONTRACT:
           this.addTask({
-            from: taskTarget.from,
-            fromId: taskTarget.fromId,
-            ...info
+            from: value.from,
+            fromId: value.fromId,
+            ...value.info
           }).then(done)
           break
         default:

+ 9 - 10
src/views/dashboard/v1/MessageNotice.vue

@@ -180,16 +180,15 @@ export default {
             data = this.listData.slice()
             const newAlarm = this.deviceList[Math.floor(Math.random() * this.deviceList.length)]
             data.push({
-              'id': new Date().valueOf(),
-              'deviceName': newAlarm.name,
-              'level': 2,
-              'happenTime': parseTime(new Date()),
-              'deviceId': newAlarm.id,
-              'asset': null,
-              'type': '设备上线',
-              'handle': '未干预',
-              'status': {
-                'label': '-'
+              id: new Date().valueOf(),
+              deviceName: newAlarm.name,
+              level: 2,
+              happenTime: parseTime(new Date()),
+              deviceId: newAlarm.id,
+              type: '设备上线',
+              handle: '未干预',
+              status: {
+                label: '-'
               }
             })
           } else {

+ 3 - 3
src/views/device/detail/components/DeviceAlarm.vue

@@ -28,7 +28,7 @@ export default {
         list: getDeviceAlarms,
         cols: [
           { type: 'refresh' },
-          { prop: 'asset', label: '截图', type: 'asset', on: this.onView },
+          { prop: 'file', label: '截图', type: 'asset', on: this.onView },
           { prop: 'type', label: '类型', 'min-width': 120 },
           { prop: 'handle', label: '处理方式' },
           { prop: 'status', label: '处理结果', type: 'tag' },
@@ -45,8 +45,8 @@ export default {
     }
   },
   methods: {
-    onView (asset) {
-      this.$refs.previewDialog.show(asset)
+    onView ({ file }) {
+      this.$refs.previewDialog.show(file)
     }
   }
 }

+ 9 - 9
src/views/device/detail/dashboard/Timeline.vue

@@ -9,21 +9,21 @@
       :class="{ fullscreen }"
     >
       <div class="l-flex--row prioritys">
-        <div class="l-flex__fill priority">
+        <div class="l-flex__fill u-font-size--xs">
           <template v-if="fullscreen">
             {{ date }}
           </template>
         </div>
-        <div class="l-flex--row priority">
-          <div class="priority3 priority__block" />
+        <div class="l-flex--row o-priority">
+          <div class="is-priority99 o-priority__block" />
           <div>高优先级</div>
         </div>
-        <div class="l-flex--row priority">
-          <div class="priority2 priority__block" />
+        <div class="l-flex--row o-priority">
+          <div class="is-priority3 o-priority__block" />
           <div>中优先级</div>
         </div>
-        <div class="l-flex--row priority">
-          <div class="priority1 priority__block" />
+        <div class="l-flex--row o-priority">
+          <div class="is-priority1 o-priority__block" />
           <div>低优先级</div>
         </div>
       </div>
@@ -470,8 +470,8 @@ export default {
   background-color: #ffecec;
 }
 
-.priority {
-  font-size: 12px;
+.o-priority {
+  font-size: $font-size--xs;
 
   & + & {
     margin-left: 1em;

+ 21 - 12
src/views/device/index.vue

@@ -16,27 +16,32 @@
       :ratio="ratio"
       @choosen="onChoosen"
     />
+    <dataset-config-dialog ref="datasetConfigDialog" />
   </wrapper>
 </template>
 
 <script>
-import {
-  getDevices,
-  getSubDevices,
-  activateDevice,
-  deactivateDevice
-} from '@/api/device'
-import { publish } from '@/api/platform'
 import {
   Access,
   EventPriority,
   EventFreq,
-  PublishType
+  PublishTargetType
 } from '@/constant'
 import { toDateStr } from '@/utils/event'
+import { publish } from '@/api/platform'
+import {
+  getDevices,
+  getSubDevices,
+  activateDevice,
+  deactivateDevice
+} from '@/api/device'
+import DatasetConfigDialog from '@/views/realm/device/settings/components/DatasetConfigDialog'
 
 export default {
   name: 'DeviceList',
+  components: {
+    DatasetConfigDialog
+  },
   data () {
     const canEdit = this.accessSet.has(Access.MANAGE_DEVICE)
 
@@ -75,9 +80,10 @@ export default {
                   : { type: 'danger', label: '离线' }
               : { type: 'warning', label: '未激活' },
             on: this.onTagClick, 'width': 120, 'align': 'center' },
-          { type: 'invoke', width: canEdit ? 160 : 80, render: [
-            { label: '查看', render: ({ empty }) => !empty, on: this.onViewDevice },
-            canEdit ? { label: '默认播放', render: ({ isMaster, activate }) => isMaster && activate, on: this.onSetDefaultProgram } : null
+          { type: 'invoke', width: canEdit ? 240 : 80, render: [
+            canEdit ? { label: '默认播放', render: ({ isMaster, activate }) => isMaster && activate, on: this.onSetDefaultProgram } : null,
+            canEdit ? { label: '素材包绑定', render: ({ isMaster, activate }) => isMaster && activate, on: this.onSetDataset } : null,
+            { label: '查看', render: ({ empty }) => !empty, on: this.onViewDevice }
           ].filter(Boolean) }
         ]
       }
@@ -181,7 +187,7 @@ export default {
       ).then(() => publish(
         [this.$device.id],
         {
-          type: PublishType.EVENT,
+          type: PublishTargetType.EVENT,
           detail: {
             priority: EventPriority.SHIM,
             freq: EventFreq.ONCE,
@@ -195,6 +201,9 @@ export default {
           resolutionRatio: this.ratio
         }
       )).then(done)
+    },
+    onSetDataset (device) {
+      this.$refs.datasetConfigDialog.show(device)
     }
   }
 }

+ 4 - 21
src/views/device/timeline/index.vue

@@ -56,9 +56,9 @@
       </div>
       <div class="l-flex__none l-flex c-timeline__row header">
         <div class="l-flex__none c-timeline__left">
-          <span class="o-priority priority3">高</span>
-          <span class="o-priority priority2">中</span>
-          <span class="o-priority priority1">低</span>
+          <span class="o-priority is-priority99">高</span>
+          <span class="o-priority is-priority3">中</span>
+          <span class="o-priority is-priority1">低</span>
         </div>
         <div class="l-flex__auto l-flex--row c-timeline__right">
           <i
@@ -121,7 +121,7 @@
                     v-for="program in programs"
                     :key="program.key"
                     class="l-flex__none l-flex--row c-event-program u-pointer"
-                    :class="[{ 'selected': program.event.selected }, `priority${program.event.priority}`]"
+                    :class="[{ 'selected': program.event.selected }, `is-priority${program.event.priority}`]"
                     :style="program.style"
                     @click.stop="chooseProgramProxy(item, program)"
                   >
@@ -669,23 +669,6 @@ export default {
   }
 }
 
-.priority1 {
-  color: #8e929c;
-  background-color: #edf0f6;
-}
-
-.priority2 {
-  color: #7642fd;
-  background-color: #eae2fe;
-}
-
-.priority3,
-.priority4,
-.priority99 {
-  color: #ff2222;
-  background-color: #ffecec;
-}
-
 .o-program {
   display: inline-block;
   font-size: 0;

+ 15 - 3
src/views/realm/ai-timing/components/GroupTimingTable.vue

@@ -11,12 +11,17 @@
       size="medium"
     />
     <device-service-config-dialog ref="deviceServiceConfigDialog" />
+    <preview-dialog
+      ref="previewDialog"
+      append-to-body
+    />
   </schema-table>
 </template>
 
 <script>
-import { getDevicesByAdmin } from '@/api/device'
+import { AssetType } from '@/constant'
 import { getAIState } from '@/utils'
+import { getDevicesByAdmin } from '@/api/device'
 import { getHistory } from '../api'
 import StratConfigDialog from './StratConfigDialog'
 import DeviceServiceConfigDialog from './DeviceServiceConfigDialog'
@@ -76,7 +81,7 @@ export default {
           ] }
         ],
         cols: [
-          { prop: 'file', label: '截图', type: 'asset' },
+          { prop: 'file', label: '截图', type: 'asset', on: this.onView },
           { prop: 'ai', label: 'AI审核', type: 'tag' },
           this.device ? null : { prop: 'deviceName', label: '设备名称' },
           { prop: 'screenshotTime', label: '回采时间', align: 'center' }
@@ -106,7 +111,11 @@ export default {
     transform ({ deviceName, screenshotTime, screenshotUrl, screenshotDeleted, auditState, auditMsg }) {
       return {
         file: screenshotUrl && !screenshotDeleted
-          ? { thumb: screenshotUrl }
+          ? {
+            type: AssetType.IMAGE,
+            url: screenshotUrl,
+            thumb: screenshotUrl
+          }
           : null,
         ai: getAIState({ aiAuditState: auditState, aiAuditMsg: auditMsg }),
         deviceName,
@@ -129,6 +138,9 @@ export default {
     onDeviceHistory ({ id, name }) {
       this.device = { id, name }
       this.$refs.historyDialog.show()
+    },
+    onView ({ file }) {
+      this.$refs.previewDialog.show(file)
     }
   }
 }

+ 7 - 38
src/views/realm/dataset/components/DatasetTable.vue

@@ -24,11 +24,10 @@
       size="medium fixed"
       :schema="assetSchema"
     />
-    <preview-dialog ref="previewDialog" />
     <table-dialog
-      ref="assetAddDialog"
+      ref="assetTableDialog"
       title="上播内容选择"
-      :schema="assetAddSchema"
+      :schema="assetTableSchema"
       @choosen="onChoosenAsset"
     />
     <table-dialog
@@ -36,13 +35,12 @@
       :title="deviceDialog"
       :schema="deviceSchema"
     />
+    <preview-dialog ref="previewDialog" />
   </schema-table>
 </template>
 
 <script>
 import {
-  State,
-  AssetTag,
   AssetTagInfo,
   AssetType,
   AssetTypeInfo
@@ -53,7 +51,6 @@ import {
   getAssetDuration
 } from '@/utils'
 import {
-  getAssets,
   getDatasets,
   addDataset,
   updateDataset,
@@ -64,9 +61,11 @@ import {
   getDevicesByDataset,
   updateDatasetAssetDuration
 } from '@/api/asset'
+import { assetPublicTableMixin } from '@/mixins/asset-table'
 
 export default {
   name: 'DatasetTable',
+  mixins: [assetPublicTableMixin],
   props: {
     tenant: {
       type: String,
@@ -125,35 +124,11 @@ export default {
             })
             : parseDuration(data.adDuration), width: 100, align: 'center' },
           { type: 'invoke', render: [
-            { label: '查看', on: this.onViewAssetItem },
+            { label: '查看', on: this.onViewAsset },
             { label: '移除', on: this.onDelAsset }
           ] }
         ]
       },
-      assetAddSchema: {
-        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.PUBLICITY, type: AssetType.IMAGE, originalName: '' },
-        list: getAssets,
-        filters: [
-          { key: 'tag', type: 'select', options: [
-            { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
-            { value: AssetTag.LOCAL_PUBLICITY, label: AssetTagInfo[AssetTag.LOCAL_PUBLICITY] },
-            { value: AssetTag.SHIM, label: AssetTagInfo[AssetTag.SHIM] }
-          ] },
-          { key: 'type', type: 'select', options: [
-            { value: AssetType.IMAGE, label: AssetTypeInfo[AssetType.IMAGE] },
-            { value: AssetType.VIDEO, label: AssetTypeInfo[AssetType.VIDEO] }
-          ] },
-          { key: 'originalName', type: 'search', placeholder: '素材名称' }
-        ],
-        cols: [
-          { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
-          { prop: 'originalName', label: '' },
-          { prop: 'diff', label: '其他', 'align': 'right' },
-          { type: 'invoke', render: [
-            { label: '查看', on: this.onViewAssetItem }
-          ] }
-        ]
-      },
       deviceSchema: {
         list: getDevicesByDataset,
         cols: [
@@ -249,13 +224,7 @@ export default {
       return asset
     },
     onAddAsset () {
-      this.$refs.assetAddDialog.show()
-    },
-    onViewAssetItem ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (file) {
-      this.$refs.previewDialog.show(file)
+      this.$refs.assetTableDialog.show()
     },
     onChoosenAsset ({ value, done }) {
       const { tag, type, keyName } = value

+ 2 - 5
src/views/realm/device/settings/components/DatasetConfigDialog.vue

@@ -68,7 +68,7 @@ export default {
           { prop: 'name', label: '' },
           { prop: 'duration', label: '上播时长', 'align': 'center' },
           { type: 'invoke', render: [
-            { label: '查看', on: this.onViewAssetItem }
+            { label: '查看', on: this.onViewAsset }
           ] }
         ]
       },
@@ -121,10 +121,7 @@ export default {
       asset.duration = parseDuration(asset.adDuration)
       return asset
     },
-    onViewAssetItem ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (file) {
+    onViewAsset ({ file }) {
       this.$refs.previewDialog.show(file)
     },
     onBind () {

+ 37 - 31
src/views/screen/deploy/index.vue → src/views/screen/deploy/device/index.vue

@@ -4,7 +4,7 @@
     margin
     background
   >
-    <div class="l-flex__none l-flex--row c-step has-padding">
+    <div class="l-flex__none l-flex--row c-step">
       <button
         class="l-flex__none c-sibling-item o-button"
         :class="{ hidden: active === 0 }"
@@ -29,15 +29,15 @@
         {{ btnMsg }}
       </button>
     </div>
-    <div class="l-flex__fill l-flex">
+    <div class="l-flex__fill l-flex has-padding">
       <device-group-tree
         v-show="active === 0"
         ref="tree"
-        class="l-flex__fill has-padding"
+        class="l-flex__fill c-sibling-item"
         @change="onChange"
       />
       <template v-if="active > 0">
-        <div class="l-flex--col c-list has-padding">
+        <div class="l-flex--col c-sibling-item far c-list">
           <div class="l-flex__none c-sibling-item--v u-bold">{{ resolutionRatio }}</div>
           <div class="l-flex__fill c-sibling-item--v near u-overflow-y--auto">
             <div
@@ -49,7 +49,7 @@
             </div>
           </div>
         </div>
-        <div class="c-list large has-padding u-overflow-y--auto">
+        <div class="c-list large u-overflow-y--auto">
           <div class="c-sibling-item--v">
             <div class="c-sibling-item--v u-bold">上播类型</div>
             <el-select
@@ -68,7 +68,7 @@
             v-if="isEvent"
             class="c-sibling-item--v"
           >
-            <div class="c-sibling-item--v u-font-size--sm">级</div>
+            <div class="c-sibling-item--v u-font-size--sm">优先级</div>
             <el-select
               v-model="priority"
               class="c-sibling-item--v near"
@@ -113,7 +113,7 @@
         </div>
         <div
           v-if="eventTarget"
-          class="c-list fill has-padding u-overflow-y--auto"
+          class="c-list fill u-overflow-y--auto"
         >
           <schedule
             v-if="scheduleId"
@@ -141,22 +141,24 @@ import {
   ScheduleType,
   EventTarget,
   PublishType,
+  PublishTargetType,
   EventPriority,
   EventPriorityDescription
 } from '@/constant'
 
 export default {
-  name: 'ScheduleDeploy',
+  name: 'DeployDevice',
   data () {
     return {
       typeOptions: [
-        { value: PublishType.CALENDAR, label: '排期' },
-        { value: PublishType.EVENT, label: '节目' }
+        { value: PublishTargetType.CALENDAR, label: '排期' },
+        { value: PublishTargetType.EVENT, label: '节目' }
       ],
       active: 0,
       selectedDevices: [],
       priority: EventPriority.INSERTED,
       priorityOptions: [
+        { value: EventPriority.SCHEDULING, label: EventPriorityDescription[EventPriority.SCHEDULING] },
         { value: EventPriority.INSERTED, label: EventPriorityDescription[EventPriority.INSERTED] },
         { value: EventPriority.EMBEDDED, label: EventPriorityDescription[EventPriority.EMBEDDED] },
         { value: EventPriority.EMERGENT, label: EventPriorityDescription[EventPriority.EMERGENT] }
@@ -190,10 +192,10 @@ export default {
       return this.eventOptions?.program
     },
     isCalendar () {
-      return this.eventOptions?.type === PublishType.CALENDAR
+      return this.eventOptions?.type === PublishTargetType.CALENDAR
     },
     isEvent () {
-      return this.eventOptions?.type === PublishType.EVENT
+      return this.eventOptions?.type === PublishTargetType.EVENT
     },
     btnMsg () {
       return this.active < 1 ? '下一步' : '发布'
@@ -205,7 +207,7 @@ export default {
       return this.typeOptions[this.eventOptions?.type - 1]?.label
     },
     scheduleId () {
-      return this.eventOptions?.type === PublishType.CALENDAR || this.eventTarget?.type === EventTarget.RECUR ? this.eventTarget.id : null
+      return this.eventOptions?.type === PublishTargetType.CALENDAR || this.eventTarget?.type === EventTarget.RECUR ? this.eventTarget.id : null
     },
     programId () {
       return this.eventTarget?.type === EventTarget.PROGRAM ? this.eventTarget.id : null
@@ -274,7 +276,7 @@ export default {
     },
     createEventOptions () {
       return {
-        type: PublishType.CALENDAR,
+        type: PublishTargetType.CALENDAR,
         schedule: null,
         program: null
       }
@@ -293,16 +295,16 @@ export default {
       this.$refs.programDialog.show(this.programId)
     },
     getPublishTarget () {
-      if (this.eventOptions.type === PublishType.CALENDAR) {
+      if (this.eventOptions.type === PublishTargetType.CALENDAR) {
         return Promise.resolve({
-          type: PublishType.CALENDAR,
+          type: PublishTargetType.CALENDAR,
           detail: this.eventTarget.id
         })
       }
       const event = this.$refs.picker.getValue()
       if (event) {
         return Promise.resolve({
-          type: PublishType.EVENT,
+          type: PublishTargetType.EVENT,
           detail: {
             ...event,
             priority: this.priority
@@ -312,24 +314,26 @@ export default {
       return Promise.reject()
     },
     publish () {
-      return this.getPublishTarget().then(eventTarget => {
-        const devices = this.selectedDevices
-        return this.$confirm(
+      return this.getPublishTarget().then(
+        publishTarget => this.$confirm(
           '发布需审核生效,操作完成后请通知相关人员进行审核',
           `立即发布?`,
           {
             type: 'warning',
             confirmButtonText: '发布'
           }
-        ).then(() => publish(
-          devices.map(device => device.id),
-          eventTarget,
-          {
-            programCalendarName: this.eventTarget.name,
-            resolutionRatio: this.resolutionRatio
-          }
-        ))
-      })
+        ).then(
+          () => publish(
+            PublishType.DEVICE,
+            this.selectedDevices.map(device => device.id),
+            publishTarget,
+            {
+              programCalendarName: this.eventTarget.name,
+              resolutionRatio: this.resolutionRatio
+            }
+          )
+        )
+      )
     }
   }
 }
@@ -337,6 +341,8 @@ export default {
 
 <style lang="scss" scoped>
 .c-step {
+  padding: $spacing 0;
+  margin: 0 $spacing;
   border-bottom: 1px solid $gray--light;
 
   .hidden {
@@ -347,10 +353,10 @@ export default {
 .c-list {
   flex: 0 0 200px;
   min-width: 200px;
-  padding-right: $spacing;
   color: $black;
 
   & + & {
+    padding-left: $spacing;
     border-left: 1px solid $gray--light;
   }
 
@@ -360,7 +366,7 @@ export default {
   }
 
   &.fill {
-    flex: 1 0 200px;
+    flex: 1 0 auto;
   }
 }
 

+ 366 - 0
src/views/screen/deploy/ratio/index.vue

@@ -0,0 +1,366 @@
+<template>
+  <wrapper
+    fill
+    margin
+    background
+  >
+    <div class="l-flex__none l-flex--row c-step">
+      <div class="l-flex__none c-sibling-item o-button hidden" />
+      <div class="l-flex__fill u-text-center">
+        请选择需要上播的设备对应的宽高比并配置相关上播内容
+      </div>
+      <button
+        class="l-flex__none c-sibling-item o-button"
+        :class="{ hidden: hideNext }"
+        @click="publish"
+      >
+        发布
+      </button>
+    </div>
+    <div class="l-flex__fill l-flex has-padding">
+      <ratio-tree-single
+        ref="tree"
+        class="c-sibling-item"
+        @change="onChange"
+      />
+      <div class="c-sibling-item far c-list u-overflow-y--auto">
+        <div class="l-flex--row c-sibling-item--v u-bold">上播时间</div>
+        <div class="c-sibling-item--v far u-font-size--sm">优先级</div>
+        <el-select
+          v-model="priority"
+          class="c-sibling-item--v near"
+        >
+          <el-option
+            v-for="option in priorityOptions"
+            :key="option.value"
+            :label="option.label"
+            :value="option.value"
+          />
+        </el-select>
+        <div class="c-sibling-item--v u-font-size--sm u-required">开始时间</div>
+        <el-date-picker
+          v-model="startDate"
+          class="c-sibling-item--v near"
+          type="datetime"
+          placeholder="请选择开始时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          :picker-options="pickerOptions"
+          @change="onDateTimeChange('startDate')"
+        />
+        <div class="c-sibling-item--v u-font-size--sm u-required">结束时间</div>
+        <el-date-picker
+          v-model="endDate"
+          class="c-sibling-item--v near"
+          type="datetime"
+          popper-class="is-hide-now"
+          :disabled="!startDate"
+          placeholder="请选择结束时间"
+          :default-value="defaultEndDateDate"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          :picker-options="endPickerOptions"
+          @change="onDateTimeChange('endDate')"
+        />
+      </div>
+      <div class="l-flex--col c-sibling-item far c-list fill">
+        <div class="l-flex--row c-sibling-item--v u-bold">
+          <span class="c-sibling-item u-bold">上播内容</span>
+          <i
+            class="c-sibling-item el-icon-circle-plus-outline u-color--blue u-pointer"
+            @click="onAddAsset"
+          />
+        </div>
+        <draggable
+          v-model="assets"
+          class="l-flex__fill c-sibling-item--v far u-overflow-y--auto"
+          handle=".mover"
+          animation="300"
+        >
+          <div
+            v-for="(asset, index) in assets"
+            :key="asset.key"
+            class="l-flex--row c-sibling-item--v o-publish-asset"
+          >
+            <div class="l-flex__auto l-flex--row mover">
+              <i class="l-flex__none o-publish-asset__mover el-icon-sort has-active" />
+              <div
+                class="l-flex__auto o-publish-asset__name u-ellipsis has-active u-pointer"
+                @click="onViewAssetItem(asset)"
+              >
+                {{ asset.tagInfo }} {{ asset.name }}
+              </div>
+            </div>
+            <template v-if="needDuration">
+              <span class="l-flex__none u-color--info">上播时长(s):</span>
+              <el-input-number
+                v-model="asset.duration"
+                class="l-flex__none o-publish-asset__seconds"
+                controls-position="right"
+                :min="1"
+                :max="86400"
+                :step="1"
+                :disabled="asset.disabled"
+                step-strictly
+              />
+            </template>
+            <i
+              class="l-flex__none o-publish-asset__del el-icon-delete has-active u-pointer"
+              @click="onDelAsset(asset, index)"
+            />
+          </div>
+        </draggable>
+      </div>
+    </div>
+    <table-dialog
+      ref="assetTableDialog"
+      title="上播内容选择"
+      :schema="assetTableSchema"
+      @choosen="onChoosenAsset"
+    />
+    <preview-dialog ref="previewDialog" />
+  </wrapper>
+</template>
+
+<script>
+import {
+  AssetTagInfo,
+  AssetType,
+  EventFreq,
+  EventTarget,
+  PublishType,
+  PublishTargetType,
+  EventPriority,
+  EventPriorityDescription
+} from '@/constant'
+import {
+  parseTime,
+  getAssetDuration
+} from '@/utils'
+import { publish } from '@/api/platform'
+import { assetTableMixin } from '@/mixins/asset-table'
+import Draggable from 'vuedraggable'
+
+export default {
+  name: 'DeployRatio',
+  components: {
+    Draggable
+  },
+  mixins: [assetTableMixin],
+  data () {
+    return {
+      priority: EventPriority.INSERTED,
+      priorityOptions: [
+        { value: EventPriority.SCHEDULING, label: EventPriorityDescription[EventPriority.SCHEDULING] },
+        { value: EventPriority.INSERTED, label: EventPriorityDescription[EventPriority.INSERTED] },
+        { value: EventPriority.EMBEDDED, label: EventPriorityDescription[EventPriority.EMBEDDED] },
+        { value: EventPriority.EMERGENT, label: EventPriorityDescription[EventPriority.EMERGENT] }
+      ],
+      productType: null,
+      startDate: null,
+      endDate: null,
+      assets: []
+    }
+  },
+  computed: {
+    hideNext () {
+      return !this.productType || !this.startDate || !this.endDate || this.assets.length === 0
+    },
+    defaultEndDateDate () {
+      if (this.startDate) {
+        const date = new Date(this.startDate)
+        date.setDate(date.getDate() + 1)
+        return `${parseTime(date, '{y}-{m}-{d}')} 00:00:00`
+      }
+      return null
+    },
+    pickerOptions () {
+      return {
+        disabledDate: this.isDisableDate
+      }
+    },
+    endPickerOptions () {
+      return {
+        disabledDate: this.isDisableEndDate
+      }
+    },
+    minDate () {
+      const now = new Date()
+      return new Date(now.getFullYear(), now.getMonth(), now.getDate())
+    },
+    needDuration () {
+      return this.assets.length > 1
+    }
+  },
+  methods: {
+    isDisableDate (date) {
+      return date < this.minDate
+    },
+    isDisableEndDate (date) {
+      if (date < this.minDate) {
+        return true
+      }
+      const startDate = this.startDate
+      if (startDate) {
+        return date < new Date(startDate.replace(/\d{2}:\d{2}:\d{2}$/, '00:00:00'))
+      }
+      return false
+    },
+    onDateTimeChange (type) {
+      const { startDate, endDate } = this
+      if (startDate && endDate && startDate > endDate) {
+        if (type === 'startDate') {
+          this.endDate = startDate
+        } else {
+          this.startDate = endDate
+        }
+      }
+    },
+    onChange (productType) {
+      this.productType = productType
+    },
+    onAddAsset () {
+      this.$refs.assetTableDialog.show()
+    },
+    onChoosenAsset ({ value, done }) {
+      const { tag, type, keyName, originalName, size, md5 } = value
+      this.assets.push({
+        key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
+        tag,
+        type,
+        keyName,
+        size,
+        md5,
+        tagInfo: AssetTagInfo[tag],
+        name: originalName,
+        duration: getAssetDuration(value),
+        disabled: type === AssetType.VIDEO
+      })
+      done()
+    },
+    onDelAsset (asset, index) {
+      this.assets.splice(index, 1)
+    },
+    onError (message) {
+      this.$message({
+        type: 'warning',
+        message
+      })
+      return Promise.reject()
+    },
+    getPublishTarget () {
+      if (this.startDate === this.endDate) {
+        return this.onError('开始时间与结束时间不能一样')
+      }
+      return Promise.resolve({
+        type: PublishTargetType.EVENT,
+        detail: {
+          freq: EventFreq.ONCE,
+          start: this.startDate,
+          until: this.endDate,
+          priority: this.priority,
+          target: {
+            type: EventTarget.ASSETS,
+            sources: this.assets.map(({ tag, type, keyName, duration, size, md5 }) => {
+              return { tag, type, keyName, duration, size, md5 }
+            })
+          }
+        }
+      })
+    },
+    publish () {
+      this.getPublishTarget().then(
+        publishTarget => this.$confirm(
+          '发布需审核生效,操作完成后请通知相关人员进行审核',
+          `立即发布?`,
+          {
+            type: 'warning',
+            confirmButtonText: '发布'
+          }
+        ).then(
+          () => publish(
+            PublishType.PRODUCT_TYPE,
+            [this.productType.id],
+            publishTarget,
+            {
+              programCalendarName: this.productType.name
+            }
+          )
+        )
+      ).then(() => {
+        this.$refs.tree.reset()
+        this.productType = null
+        this.assets = []
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-step {
+  padding: $spacing 0;
+  margin: 0 $spacing;
+  border-bottom: 1px solid $gray--light;
+
+  .hidden {
+    visibility: hidden;
+  }
+}
+
+.c-list {
+  flex: 0 0 auto;
+  min-width: 200px;
+  color: $black;
+
+  & + & {
+    padding-left: $spacing;
+    border-left: 1px solid $gray--light;
+  }
+
+  &.fill {
+    flex: 1 0 auto;
+  }
+}
+
+.o-publish-asset {
+  padding: 4px 0;
+  border: 1px solid $gray;
+  border-radius: $radius--mini;
+
+  &.sortable-chosen {
+    border-color: #c6e2ff;
+    background-color: $blue--light;
+  }
+
+  &__mover {
+    padding: 10px 16px;
+    font-size: 18px;
+    cursor: move;
+  }
+
+  &__name {
+    padding: 16px 10px 16px 0;
+  }
+
+  &__seconds {
+    width: 120px;
+  }
+
+  &__del {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 50px;
+    height: 40px;
+    color: $gray;
+    font-size: 18px;
+
+    &.mini {
+      width: 40px;
+    }
+
+    &:hover {
+      color: $primary;
+    }
+  }
+}
+</style>

+ 1 - 0
src/views/screen/material/media/MediaDesigner.vue

@@ -56,6 +56,7 @@
         />
       </div>
     </confirm-dialog>
+    <preview-dialog ref="previewDialog" />
   </div>
 </template>
 

+ 3 - 1
src/views/screen/material/media/MediaViewer.vue

@@ -6,7 +6,9 @@
     :schema="schema"
     :proxy.sync="currOptions"
     :header-cell-class-name="adjustHeader"
-  />
+  >
+    <preview-dialog ref="previewDialog" />
+  </schema-table>
 </template>
 
 <script>

+ 1 - 8
src/views/screen/material/media/index.vue

@@ -6,11 +6,7 @@
     padding
     background
   >
-    <component
-      :is="activeComponent"
-      @view="onView"
-    />
-    <preview-dialog ref="previewDialog" />
+    <component :is="activeComponent" />
   </wrapper>
 </template>
 
@@ -40,9 +36,6 @@ export default {
           this.type = type
         })
       }
-    },
-    onView (asset) {
-      this.$refs.previewDialog.show(asset)
     }
   }
 }

+ 4 - 7
src/views/screen/material/media/mixin.js

@@ -75,7 +75,7 @@ export default {
           isRejected ? null : { prop: 'diff', label: '其他', 'align': 'right' },
           isRejected ? null : { prop: 'createTime', label: '上传时间', 'align': 'right' },
           { type: 'invoke', render: [
-            { label: '查看', on: this.onView },
+            { label: '查看', on: this.onViewAsset },
             { label: '删除', allow: ({ del }) => del, on: this.onDel }
           ] }
         ]
@@ -103,18 +103,15 @@ export default {
       asset.ai = getAIState(asset)
       return asset
     },
-    onView ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      if (asset.status === State.DRAFT) {
+    onViewAsset ({ status, file }) {
+      if (status === State.DRAFT) {
         this.$message({
           type: 'warning',
           message: '文件正在解析中,请稍后再试'
         })
         return
       }
-      this.$emit('view', asset)
+      this.$refs.previewDialog.show(file)
     },
     onEdit (asset) {
       if (!asset.name) {

+ 7 - 5
src/views/screen/review/history/index.vue

@@ -34,14 +34,16 @@ export default {
             return h('div', {
               staticClass: 'o-info'
             }, [
-              h('div', null, `发布人:${data.createBy}`),
-              h('div', null, `发布时间:${data.createTime}`),
+              h('div', null, data.desc),
               h('div', null, `设备:${data.device}`)
             ])
           } },
-          { prop: 'type', label: '类型', align: 'center', width: 100 },
-          { prop: 'name', label: '', 'min-width': 100 },
-          { prop: 'desc', label: '时间', 'min-width': 180 },
+          { prop: 'priority', label: '优先级', width: 80, align: 'center' },
+          { prop: 'priorityInfo', label: '', width: 80, align: 'center' },
+          { prop: 'targetInfo', label: '上播内容', width: 80, align: 'center' },
+          { prop: 'targetName', label: '', 'min-width': 100 },
+          { prop: 'createBy', label: '发布人' },
+          { prop: 'createTime', label: '发布时间', 'min-width': 100 },
           { type: 'tag', render: ({ status }) => {
             if (status === State.CANCEL) {
               return {

+ 5 - 8
src/views/screen/review/single/components/ReviewAsset.vue

@@ -34,7 +34,7 @@ export default {
         transform: this.transform,
         cols: [
           { prop: 'tagInfo', type: 'refresh', width: 80 },
-          { prop: 'typeName', label: '文件', align: 'center', width: 80 },
+          { prop: 'typeInfo', label: '文件', align: 'center', width: 80 },
           { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
           { prop: 'originalName' },
           { prop: 'ai', label: 'AI审核', type: 'tag' },
@@ -43,7 +43,7 @@ export default {
           { prop: 'createBy', label: '申请人', 'align': 'right' },
           { prop: 'createTime', label: '提交时间', 'align': 'right', 'min-width': 100 },
           { type: 'invoke', width: 160, render: [
-            { label: '查看', on: this.onView },
+            { label: '查看', on: this.onViewAsset },
             { label: '通过', on: this.onResolve },
             { label: '驳回', on: this.onReject }
           ] }
@@ -55,17 +55,14 @@ export default {
     resolve: resolveAsset,
     transform (asset) {
       asset.tagInfo = AssetTagInfo[asset.tag]
-      asset.typeName = AssetTypeInfo[asset.type]
+      asset.typeInfo = AssetTypeInfo[asset.type]
       asset.size = parseByte(asset.size)
       asset.ai = getAIState(asset)
       asset.createBy = asset.userName || asset.createBy
       return asset
     },
-    onView ({ file }) {
-      this.onViewAsset(file)
-    },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
+    onViewAsset ({ file }) {
+      this.$refs.previewDialog.show(file)
     }
   }
 }

+ 33 - 10
src/views/screen/review/utils.js

@@ -1,31 +1,54 @@
 import {
   PublishType,
+  PublishTargetType,
+  EventTarget,
   EventPriority,
   EventPriorityDescription
 } from '@/constant'
 import { getEventDescription } from '@/utils/event'
 
-export function transformCalendarRelease ({ id, target, programCalendarName, calendarReleaseDeviceList, createByUsername, createBy, createTime }) {
+export function transformCalendarRelease (data) {
+  const { id, target, programCalendarName, createByUsername, createBy, createTime } = data
   const publishTarget = JSON.parse(target)
-  let type = ''
+  let priority = 0
+  let desc = '-'
+  let targetInfo = '未知'
+  let targetName = programCalendarName
   switch (publishTarget.type) {
-    case PublishType.CALENDAR:
-      type = EventPriorityDescription[EventPriority.SCHEDULING]
+    case PublishTargetType.CALENDAR:
+      priority = EventPriority.SCHEDULING
+      targetInfo = '排期'
       break
-    case PublishType.EVENT:
-      type = EventPriorityDescription[publishTarget.detail.priority]
+    case PublishTargetType.EVENT:
+      priority = publishTarget.detail.priority
+      targetInfo = ['', '节目', '轮播', '数据集'][publishTarget.detail.target.type]
+      if (publishTarget.detail.target.type === EventTarget.ASSETS) {
+        targetName = ''
+      }
+      desc = getEventDescription(publishTarget.detail)
       break
     default:
       break
   }
   return {
     id,
-    name: programCalendarName,
-    type,
+    priority,
+    priorityInfo: EventPriorityDescription[priority] || '未知',
     target: publishTarget,
-    desc: publishTarget.type === PublishType.EVENT ? getEventDescription(publishTarget.detail) : '-',
-    device: calendarReleaseDeviceList?.map(({ deviceName }) => deviceName).join(','),
+    targetInfo,
+    targetName,
+    desc,
+    device: getDeviceDesc(data),
     createBy: createByUsername || createBy,
     createTime
   }
 }
+
+function getDeviceDesc ({ type, programCalendarName, calendarReleaseDeviceList }) {
+  switch (type) {
+    case PublishType.PRODUCT_TYPE:
+      return `宽高比为${programCalendarName}的设备`
+    default:
+      return calendarReleaseDeviceList?.map(({ deviceName }) => deviceName).join(',')
+  }
+}

+ 2 - 5
src/views/screen/review/workflow/detail/components/ReviewAssets.vue

@@ -44,7 +44,7 @@ export default {
         list: this.getList,
         cols: [
           { prop: 'typeName', label: '类型', align: 'center', width: 80 },
-          { prop: 'file', type: 'asset', on: this.onViewAsset },
+          { prop: 'file', type: 'asset', on: this.onView },
           { prop: 'originalName', label: '' },
           { prop: 'size', label: '文件大小' },
           { prop: 'diff', label: '其他' },
@@ -88,11 +88,8 @@ export default {
         status: asset.status
       }
     },
-    onViewAsset (asset) {
-      this.$refs.previewDialog.show(asset)
-    },
     onView ({ file }) {
-      this.onViewAsset(file)
+      this.$refs.previewDialog.show(file)
     }
   }
 }

+ 1 - 1
src/views/screen/review/workflow/detail/components/ReviewPrograms.vue

@@ -55,7 +55,7 @@ export default {
       return {
         pass: program.status !== State.SUBMITTED,
         id: program.id,
-        file: program.img ? { id: program.id, thumb: program.img } : null,
+        file: program.img ? { thumb: program.img } : null,
         name: program.name,
         status: program.status
       }

+ 5 - 3
src/views/screen/review/workflow/detail/components/ReviewPublish.vue

@@ -32,12 +32,14 @@ export default {
             return h('div', {
               staticClass: 'o-info'
             }, [
-              h('div', null, `内容:${data.name}`),
               h('div', null, `设备:${data.device}`)
             ])
           } },
-          { prop: 'type', label: '类型', width: 100 },
-          { prop: 'desc', label: '时间', 'min-width': 120 },
+          { prop: 'priority', label: '优先级', width: 80, align: 'center' },
+          { prop: 'priorityInfo', label: '', width: 80, align: 'center' },
+          { prop: 'targetInfo', label: '上播内容', width: 80, align: 'center' },
+          { prop: 'targetName', label: '', 'min-width': 100 },
+          { prop: 'desc', label: '上播时间', 'min-width': 180 },
           ...this.reviewCol
         ]
       }

+ 6 - 11
src/views/screen/review/workflow/index.vue

@@ -27,18 +27,13 @@ export default {
         list: getPublishWorkflows,
         transform: this.transform,
         cols: [
-          { type: 'expand', refresh: true, render (data, h) {
-            return h('div', {
-              staticClass: 'o-info'
-            }, [
-              h('div', null, `内容:${data.name}`),
-              h('div', null, `设备:${data.device}`)
-            ])
-          } },
-          { prop: 'type', label: '类型', align: 'center', width: 100 },
-          { prop: 'desc', label: '时间', 'min-width': 180 },
+          { type: 'refresh' },
+          { prop: 'priority', label: '优先级', width: 80, align: 'center' },
+          { prop: 'priorityInfo', label: '', width: 80, align: 'center' },
+          { prop: 'targetInfo', label: '内容', width: 80, align: 'center' },
+          { prop: 'targetName', label: '', 'min-width': 100 },
           { prop: 'createBy', label: '申请人' },
-          { prop: 'updateTime', label: '提交时间', 'min-width': 120 },
+          { prop: 'updateTime', label: '提交时间', 'min-width': 100 },
           { type: 'invoke', render: [
             { label: '审核', on: this.onReview }
           ] }

+ 9 - 7
src/views/screen/review/workflow/mine/index.vue

@@ -46,7 +46,7 @@ import {
   deleteWorkflow
 } from '@/api/workflow'
 import {
-  PublishType,
+  PublishTargetType,
   State,
   EventTarget
 } from '@/constant'
@@ -77,11 +77,13 @@ export default {
       return {
         condition: { status: Number(this.active) },
         list: getMyWorkflows,
-        transform: this.transform,
+        transform: this.transformWorkflow,
         cols: [
           { type: 'expand', refresh: true, render: this.renderRelative },
-          { prop: 'type', label: '类型', align: 'center', width: 100 },
-          { prop: 'name', label: '', 'min-width': 100 },
+          { prop: 'priority', label: '优先级', width: 80, align: 'center' },
+          { prop: 'priorityInfo', label: '', width: 80, align: 'center' },
+          { prop: 'targetInfo', label: '上播内容', width: 80, align: 'center' },
+          { prop: 'targetName', label: '', 'min-width': 100 },
           isRejected ? { prop: 'reason', label: '驳回原因', 'min-width': 100 } : { prop: 'updateTime', label: this.active === `${State.SUBMITTED}` ? '提交时间' : '审批时间', 'min-width': 100 },
           { type: 'invoke', width: 200, render: [
             isRejected ? { label: '编辑', allow: ({ canResubmit }) => canResubmit, on: this.onEdit } : { label: '查看', on: this.onView },
@@ -180,7 +182,7 @@ export default {
         this.active = active
       }
     },
-    transform (data) {
+    transformWorkflow (data) {
       return {
         workflowId: data.id,
         updateTime: data.updateTime,
@@ -228,13 +230,13 @@ export default {
     },
     onEdit ({ target: { type, detail } }) {
       switch (type) {
-        case PublishType.CALENDAR:
+        case PublishTargetType.CALENDAR:
           this.$router.push({
             name: 'schedule-design',
             params: { id: detail }
           })
           break
-        case PublishType.EVENT:
+        case PublishTargetType.EVENT:
           if (detail.target.type === EventTarget.RECUR) {
             this.$router.push({
               name: 'recur-design',