Sfoglia il codice sorgente

feat: common dataset

Casper Dai 2 anni fa
parent
commit
5a546e4d1d

+ 146 - 1
src/api/asset.js

@@ -1,4 +1,4 @@
-import { tenantRequest } from '@/utils/request'
+import request, { tenantRequest } from '@/utils/request'
 import {
   getAssetThumb,
   getAssetDiff
@@ -11,6 +11,7 @@ import {
   submit,
   resolve,
   reject,
+  confirmAndSend,
   canDel,
   addStatus,
   addStatusScope,
@@ -205,3 +206,147 @@ export function deleteAssetSubTags (ids) {
     data: ids
   }, '所选标签')
 }
+
+export function getDatasets (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/media/dataset/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addDataset (data) {
+  return add({
+    url: '/media/dataset',
+    method: 'POST',
+    data: addTenant(data)
+  }, tenantRequest)
+}
+
+export function updateDataset (data) {
+  return update({
+    url: '/media/dataset',
+    method: 'PUT',
+    data
+  })
+}
+
+export function getFillDataset (id) {
+  return request({
+    url: `/media/dataset/${id}`,
+    params: {
+      type: 0,
+      flag: 1
+    }
+  })
+}
+
+export function getCommonDataset (id) {
+  return request({
+    url: '/media/dataset/order',
+    params: {
+      id,
+      flag: 1
+    }
+  })
+}
+
+export function deleteDataset ({ id, name }) {
+  return del({
+    url: `/media/dataset/${id}`,
+    method: 'DELETE'
+  }, name)
+}
+
+export function bindAssetsToDataset (datasetId, assets) {
+  return send({
+    url: '/media/dataset/batchBindAsset',
+    method: 'POST',
+    data: assets.map(item => {
+      return {
+        datasetId,
+        ...item
+      }
+    })
+  })
+}
+
+export function updateDatasetAssets (datasetId, assets) {
+  return update({
+    url: '/media/dataset/orderBindAsset',
+    method: 'POST',
+    data: {
+      id: datasetId,
+      relationList: assets
+    }
+  })
+}
+
+export function getDevicesByDataset (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/media/dataset/pageQueryDevice',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function getDatasetByDevice (id) {
+  return request({
+    url: `/media/dataset/${id}`,
+    params: {
+      type: 1,
+      flag: 1
+    }
+  })
+}
+
+export function bindDatasetToDevice (data) {
+  return send({
+    url: '/media/dataset/bindDevice',
+    method: 'POST',
+    data
+  })
+}
+
+export function unbindDatasetByDevice ({ id, name }) {
+  return confirmAndSend('解绑', name, {
+    url: '/media/dataset/batchUnbindDevice',
+    method: 'POST',
+    data: [id]
+  })
+}
+
+export function unbindDatasetByDevices (ids) {
+  return confirmAndSend('解绑', '所选设备', {
+    url: '/media/dataset/batchUnbindDevice',
+    method: 'POST',
+    data: ids
+  })
+}
+
+export function unbindAssetsFromDataset (datasetId, keyNames) {
+  return send({
+    url: `/media/dataset/batchUnbindAsset/${datasetId}`,
+    method: 'POST',
+    data: keyNames
+  })
+}
+
+export function updateDatasetAssetDuration (relationId, adDuration) {
+  return send({
+    url: '/media/dataset/assetChangDuration',
+    method: 'POST',
+    data: {
+      relationId,
+      adDuration
+    }
+  })
+}

+ 7 - 5
src/views/ad/automation/dataset/components/DatasetConfigDialog.vue → src/components/dialog/DatasetConfigDialog/index.vue

@@ -43,7 +43,8 @@
 <script>
 import {
   AssetTagInfo,
-  AssetTypeInfo
+  AssetTypeInfo,
+  Dataset
 } from '@/constant'
 import {
   parseDuration,
@@ -54,8 +55,8 @@ import {
   bindDatasetToDevice,
   getDatasets,
   unbindDatasetByDevice,
-  getDataset
-} from '../api'
+  getFillDataset
+} from '@/api/asset'
 
 export default {
   name: 'DatasetConfigDialog',
@@ -84,6 +85,7 @@ export default {
       },
       schema: {
         list: this.getDatasets,
+        condition: { type: Dataset.FILL },
         cols: [
           { prop: 'name', label: '名称', 'align': 'center' },
           { type: 'invoke', render: [
@@ -157,7 +159,7 @@ export default {
       asset.file = {
         type: asset.type,
         url: asset.keyName,
-        thumb: getAssetThumb(asset.minioData)
+        thumb: asset.minioData ? getAssetThumb(asset.minioData) : null
       }
       asset.name = asset.minioData.originalName
       asset.duration = parseDuration(asset.adDuration)
@@ -200,7 +202,7 @@ export default {
       if (this.$datasetAssets) {
         return Promise.resolve({ data: this.$datasetAssets })
       }
-      return getDataset(this.$datasetId).then(({ data: { mediaList } }) => {
+      return getFillDataset(this.$datasetId).then(({ data: { mediaList } }) => {
         this.$datasetAssets = mediaList
         return {
           data: mediaList

+ 5 - 0
src/constant.js

@@ -215,3 +215,8 @@ export const TaskFromTypeInfo = {
   [TaskFromType.ASSET]: '素材',
   [TaskFromType.CONTRACT]: '合同'
 }
+
+export const Dataset = {
+  FILL: 0,
+  COMMON: 1
+}

+ 5 - 0
src/router/index.js

@@ -65,6 +65,11 @@ export const asyncRoutes = [
         access: [Access.MANAGE_CALENDAR, Access.MANAGE_GROUP],
         meta: { title: '资源' }
       },
+      {
+        path: 'dataset',
+        component: () => import('@/views/screen/material/dataset/index'),
+        meta: { title: '素材包' }
+      },
       {
         path: 'program',
         component: () => import('@/views/screen/material/program/index'),

+ 0 - 132
src/views/ad/automation/dataset/api.js

@@ -1,132 +0,0 @@
-import request, { tenantRequest } from '@/utils/request'
-import {
-  send,
-  add,
-  del,
-  update,
-  addTenant,
-  confirmAndSend
-} from '@/api/base'
-
-export function getDatasets (query) {
-  const { pageNum: pageIndex, pageSize, ...params } = query
-  return tenantRequest({
-    url: '/media/dataset/pageQuery',
-    method: 'GET',
-    params: addTenant({
-      pageIndex, pageSize,
-      ...params
-    })
-  })
-}
-
-export function addDataset (data) {
-  return add({
-    url: '/media/dataset',
-    method: 'POST',
-    data: addTenant(data)
-  }, tenantRequest)
-}
-
-export function updateDataset (data) {
-  return update({
-    url: '/media/dataset',
-    method: 'PUT',
-    data
-  })
-}
-
-export function getDataset (id) {
-  return request({
-    url: `/media/dataset/${id}`,
-    params: {
-      type: 0,
-      flag: 1
-    }
-  })
-}
-
-export function deleteDataset ({ id, name }) {
-  return del({
-    url: `/media/dataset/${id}`,
-    method: 'DELETE'
-  }, name)
-}
-
-export function bindAssetsToDataset (datasetId, assets) {
-  return send({
-    url: '/media/dataset/batchBindAsset',
-    method: 'POST',
-    data: assets.map(item => {
-      return {
-        datasetId,
-        ...item
-      }
-    })
-  })
-}
-
-export function getDevicesByDataset (query) {
-  const { pageNum: pageIndex, pageSize, ...params } = query
-  return request({
-    url: '/media/dataset/pageQueryDevice',
-    method: 'GET',
-    params: {
-      pageIndex, pageSize,
-      ...params
-    }
-  })
-}
-
-export function getDatasetByDevice (id) {
-  return request({
-    url: `/media/dataset/${id}`,
-    params: {
-      type: 1,
-      flag: 1
-    }
-  })
-}
-
-export function bindDatasetToDevice (data) {
-  return send({
-    url: '/media/dataset/bindDevice',
-    method: 'POST',
-    data
-  })
-}
-
-export function unbindDatasetByDevice ({ id, name }) {
-  return confirmAndSend('解绑', name, {
-    url: '/media/dataset/batchUnbindDevice',
-    method: 'POST',
-    data: [id]
-  })
-}
-
-export function unbindDatasetByDevices (ids) {
-  return confirmAndSend('解绑', '所选设备', {
-    url: '/media/dataset/batchUnbindDevice',
-    method: 'POST',
-    data: ids
-  })
-}
-
-export function unbindAssetsFromDataset (datasetId, keyNames) {
-  return send({
-    url: `/media/dataset/batchUnbindAsset/${datasetId}`,
-    method: 'POST',
-    data: keyNames
-  })
-}
-
-export function updateDatasetAssetDuration (relationId, adDuration) {
-  return send({
-    url: '/media/dataset/assetChangDuration',
-    method: 'POST',
-    data: {
-      relationId,
-      adDuration
-    }
-  })
-}

+ 79 - 41
src/views/ad/automation/dataset/index.vue

@@ -24,23 +24,31 @@
         />
       </div>
     </confirm-dialog>
-    <table-dialog
+    <c-dialog
       ref="assetDialog"
       title="上播内容"
-      :schema="assetSchema"
-    />
-    <selection-table-dialog
-      ref="assetTableDialog"
-      title="上播内容选择"
-      message="请选择上播内容"
-      :schema="assetTableSchema"
-      @confirm="onChoosenAsset"
-    />
+      size="xl fixed"
+    >
+      <c-transfer
+        class="l-flex__auto"
+        :source-schema="assetTableSchema"
+        :selectable="assetSelectable"
+        :add="onAddAssets"
+        @changed="onAssetChanged"
+      >
+        <schema-table
+          ref="assetContentTable"
+          :schema="assetSchema"
+          @row-click="onAssetRowClick"
+          @selection-change="onAssetSelectionChange"
+        />
+      </c-transfer>
+    </c-dialog>
     <table-dialog
       ref="deviceDialog"
       title="关联的设备"
       :schema="deviceSchema"
-      @row-click="onToggleSelection"
+      @row-click="onRowClick"
       @selection-change="onSelectionChange"
     />
     <radio-table-dialog
@@ -59,7 +67,8 @@
 import {
   AssetTagInfo,
   AssetType,
-  AssetTypeInfo
+  AssetTypeInfo,
+  Dataset
 } from '@/constant'
 import {
   parseDuration,
@@ -71,7 +80,7 @@ import {
   getDatasets,
   addDataset,
   updateDataset,
-  getDataset,
+  getFillDataset,
   deleteDataset,
   bindAssetsToDataset,
   unbindAssetsFromDataset,
@@ -80,19 +89,23 @@ import {
   bindDatasetToDevice,
   unbindDatasetByDevices,
   unbindDatasetByDevice
-} from './api'
+} from '@/api/asset'
 import { assetPublicTableMixin } from '@/mixins/asset-table'
 
 export default {
-  name: 'DatasetTenant',
+  name: 'DatasetFill',
   mixins: [assetPublicTableMixin],
   data () {
     return {
       schema: {
+        list: getDatasets,
+        condition: { type: Dataset.FILL },
         buttons: [
           { type: 'add', on: this.onAdd }
         ],
-        list: getDatasets,
+        filters: [
+          { key: 'name', type: 'search', placeholder: '名称' }
+        ],
         cols: [
           { prop: 'name', label: '名称', render: (data, h) => h('edit-input', {
             props: {
@@ -100,12 +113,6 @@ export default {
             },
             on: { edit: val => this.onEditName(data, val) }
           }), 'class-name': 'c-edit-column' },
-          // { label: '使用情况', type: 'tag', render: ({ delFlag }) => {
-          //   return {
-          //     type: ['primary', 'success'][delFlag],
-          //     label: ['暂未使用', '已使用'][delFlag]
-          //   }
-          // }, align: 'center' },
           { type: 'invoke', render: [
             { label: '上播内容', on: this.onEdit },
             { label: '关联设备', on: this.onViewDevices },
@@ -118,9 +125,10 @@ export default {
         list: this.getAssetsByDataset,
         transform: this.transformDatasetAsset,
         buttons: [
-          { type: 'add', on: this.onAddAsset }
+          { type: 'del', label: '移除', on: this.onDelAssets }
         ],
         cols: [
+          { type: 'selection' },
           { prop: 'tagInfo', label: '类型', align: 'center', width: 80 },
           { prop: 'typeName', label: '文件', align: 'center', width: 80 },
           { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
@@ -168,7 +176,8 @@ export default {
           { prop: 'address', label: '地址' }
         ]
       },
-      dataset: {}
+      dataset: {},
+      assetMap: null
     }
   },
   methods: {
@@ -185,10 +194,13 @@ export default {
         })
         return
       }
-      addDataset({ name }).then(({ data }) => {
+      addDataset({
+        type: Dataset.FILL,
+        name
+      }).then(({ data }) => {
         done()
         this.onEdit({ id: data })
-        this.$refs.table.pageTo(1)
+        this.$refs.table.resetCondition({ name })
       })
     },
     onEditName (dataset, { newVal, oldVal }) {
@@ -215,28 +227,35 @@ export default {
       this.$refs.assetDialog.show()
     },
     getAssetsByDataset () {
-      return getDataset(this.$datasetId).then(({ data: { mediaList } }) => {
+      this.assetMap = null
+      this.$selectionItems = null
+      return getFillDataset(this.$datasetId).then(({ data: { mediaList } }) => {
+        const map = {}
+        mediaList.forEach(({ keyName }) => {
+          map[keyName] = true
+        })
+        this.assetMap = map
         return {
           data: mediaList
         }
       })
     },
+    assetSelectable ({ keyName }) {
+      return this.assetMap ? !this.assetMap[keyName] : false
+    },
     transformDatasetAsset (asset) {
       asset.tagInfo = AssetTagInfo[asset.tag]
       asset.typeName = AssetTypeInfo[asset.type]
       asset.file = {
         type: asset.type,
         url: asset.keyName,
-        thumb: getAssetThumb(asset.minioData)
+        thumb: asset.minioData ? getAssetThumb(asset.minioData) : null
       }
       asset.name = asset.minioData.originalName
       return asset
     },
-    onAddAsset () {
-      this.$refs.assetTableDialog.show()
-    },
-    onChoosenAsset ({ value, done }) {
-      bindAssetsToDataset(this.$datasetId, value.map(asset => {
+    onAddAssets (value) {
+      return bindAssetsToDataset(this.$datasetId, value.map(asset => {
         const { tag, type, keyName } = asset
         return {
           tag,
@@ -244,10 +263,10 @@ export default {
           keyName,
           adDuration: getAssetDuration(asset)
         }
-      })).then(() => {
-        done()
-        this.$refs.assetDialog.getTable().pageTo()
-      })
+      }))
+    },
+    onAssetChanged () {
+      this.$refs.assetContentTable.pageTo()
     },
     onEditAssetDuration (asset, { newVal, oldVal }) {
       if (!newVal || !/^\d+$/.test(newVal) || Number(newVal) < 1) {
@@ -260,10 +279,29 @@ export default {
         })
       }
     },
+    onAssetRowClick (row) {
+      this.$refs.assetContentTable.getInst().toggleRowSelection(row)
+    },
+    onAssetSelectionChange (val) {
+      this.$selectionItems = val
+    },
+    onDelAssets () {
+      if (this.$selectionItems?.length) {
+        this.$confirm(
+          '移除所选素材?',
+          '操作确认',
+          { type: 'warning' }
+        ).then(() => unbindAssetsFromDataset(this.$datasetId, this.$selectionItems.map(({ keyName }) => keyName)))
+          .then(this.onAssetChanged)
+      } else {
+        this.$message({
+          type: 'warning',
+          message: '请选择需移除的素材'
+        })
+      }
+    },
     onDelAsset ({ keyName }) {
-      unbindAssetsFromDataset(this.$datasetId, [keyName]).then(() => {
-        this.$refs.assetDialog.getTable().pageTo()
-      })
+      unbindAssetsFromDataset(this.$datasetId, [keyName]).then(this.onAssetChanged)
     },
     onViewDevices ({ id }) {
       this.$datasetId = id
@@ -293,7 +331,7 @@ export default {
         })
       }
     },
-    onToggleSelection (row) {
+    onRowClick (row) {
       this.$refs.table.getInst().toggleRowSelection(row)
     },
     onSelectionChange (val) {

+ 0 - 4
src/views/ad/automation/scheduling/index.vue

@@ -76,7 +76,6 @@ import {
   getDeviceScheduling,
   setSchedulingEnable
 } from './api'
-import DatasetConfigDialog from '../dataset/components/DatasetConfigDialog.vue'
 
 const SchedulingStatus = {
   ERROR: 0,
@@ -87,9 +86,6 @@ const SchedulingStatus = {
 
 export default {
   name: 'AdScheduling',
-  components: {
-    DatasetConfigDialog
-  },
   data () {
     const canEdit = this.$store.getters.isOperator
     const canAudit = this.$store.getters.isGroupAdmin

+ 0 - 2
src/views/ad/automation/task/ScreenTask.vue

@@ -112,13 +112,11 @@ import {
   deleteTask,
   updateTask
 } from './api'
-import DatasetConfigDialog from '../dataset/components/DatasetConfigDialog.vue'
 import AssetTaskDialog from './components/AssetTaskDialog.vue'
 
 export default {
   name: 'AdScreenTask',
   components: {
-    DatasetConfigDialog,
     AssetTaskDialog
   },
   data () {

+ 0 - 4
src/views/device/index.vue

@@ -45,13 +45,9 @@ import {
   activateDevice,
   deactivateDevice
 } from '@/api/device'
-import DatasetConfigDialog from '@/views/ad/automation/dataset/components/DatasetConfigDialog.vue'
 
 export default {
   name: 'DeviceList',
-  components: {
-    DatasetConfigDialog
-  },
   data () {
     const canEdit = this.$store.getters.isOperator
 

+ 1 - 3
src/views/platform/tenant/device/settings/components/DeviceNormalConfig.vue

@@ -37,15 +37,13 @@ import { mapGetters } from 'vuex'
 import AttributeConfigDialog from './AttributeConfigDialog'
 import ContentProtectionConfigDialog from './ContentProtectionConfigDialog'
 import AdConfigDialog from './AdConfigDialog'
-import DatasetConfigDialog from '@/views/ad/automation/dataset/components/DatasetConfigDialog.vue'
 
 export default {
   name: 'DeviceNormalConfig',
   components: {
     AttributeConfigDialog,
     ContentProtectionConfigDialog,
-    AdConfigDialog,
-    DatasetConfigDialog
+    AdConfigDialog
   },
   props: {
     device: {

+ 254 - 0
src/views/screen/material/dataset/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <wrapper
+    fill
+    margin
+    padding
+    background
+  >
+    <schema-table
+      ref="table"
+      :schema="schema"
+    />
+    <confirm-dialog
+      ref="editDialog"
+      title="新增素材包"
+      @confirm="onSave"
+    >
+      <div class="c-grid-form u-align-self--center">
+        <span class="c-grid-form__label u-required">名称</span>
+        <el-input
+          v-model.trim="dataset.name"
+          placeholder="最多30个字符"
+          maxlength="30"
+          clearable
+        />
+      </div>
+    </confirm-dialog>
+    <confirm-dialog
+      ref="assetDialog"
+      title="上播内容"
+      size="xl fixed"
+      @confirm="onSaveAssets"
+    >
+      <c-transfer
+        class="l-flex__auto"
+        :source-schema="assetTableSchema"
+        :add="onAddAssets"
+      >
+        <el-empty
+          v-if="!assets.length"
+          class="l-flex__auto l-flex--row center"
+          description="请添加素材"
+        />
+        <draggable
+          v-else
+          v-model="assets"
+          class="l-flex__auto l-flex--col u-font-size--sm u-overflow-y--auto"
+          handle=".mover"
+          animation="300"
+        >
+          <draggable-item
+            v-for="(asset, index) in assets"
+            :key="asset.key"
+            :item="asset"
+            @view="onViewAsset"
+            @del="onDelAsset(index)"
+          />
+        </draggable>
+      </c-transfer>
+    </confirm-dialog>
+    <preview-dialog ref="previewDialog" />
+  </wrapper>
+</template>
+
+<script>
+import {
+  AssetTagInfo,
+  AssetType,
+  AssetTypeInfo,
+  Dataset
+} from '@/constant'
+import {
+  getAssetThumb,
+  getAssetDuration
+} from '@/utils'
+import {
+  getDatasets,
+  addDataset,
+  updateDataset,
+  getCommonDataset,
+  deleteDataset,
+  updateDatasetAssets
+} from '@/api/asset'
+import { assetTableMixin } from '@/mixins/asset-table'
+import Draggable from 'vuedraggable'
+
+export default {
+  name: 'DatasetCommon',
+  components: {
+    Draggable
+  },
+  mixins: [assetTableMixin],
+  data () {
+    return {
+      schema: {
+        list: getDatasets,
+        condition: { type: Dataset.COMMON },
+        buttons: [
+          { type: 'add', on: this.onAdd }
+        ],
+        filters: [
+          { key: 'name', type: 'search', placeholder: '名称' }
+        ],
+        cols: [
+          { prop: 'name', label: '名称', render: (data, h) => h('edit-input', {
+            props: {
+              value: `${data.name}`
+            },
+            on: { edit: val => this.onEditName(data, val) }
+          }), 'class-name': 'c-edit-column' },
+          { type: 'invoke', render: [
+            { label: '上播内容', on: this.onEdit },
+            { label: '删除', on: this.onDel }
+          ] }
+        ]
+      },
+      assets: [],
+      dataset: {}
+    }
+  },
+  methods: {
+    onAdd () {
+      this.dataset = { name: '' }
+      this.$refs.editDialog.show()
+    },
+    onSave (done) {
+      const { name } = this.dataset
+      if (!name) {
+        this.$message({
+          type: 'warning',
+          message: '请填写素材包名称'
+        })
+        return
+      }
+      addDataset({
+        type: Dataset.COMMON,
+        name
+      }).then(({ data }) => {
+        done()
+        this.onEdit({ id: data })
+        this.$refs.table.resetCondition({ name })
+      })
+    },
+    onEditName (dataset, { newVal, oldVal }) {
+      if (newVal === oldVal) {
+        return
+      }
+      if (!newVal) {
+        this.$message({
+          type: 'warning',
+          message: '请填写素材包名称'
+        })
+        return
+      }
+      dataset.name = newVal
+      updateDataset({
+        id: dataset.id,
+        name: newVal
+      }).catch(() => {
+        dataset.name = oldVal
+      })
+    },
+    onDel (dataset) {
+      deleteDataset(dataset).then(() => {
+        this.$refs.table.decrease(1)
+      })
+    },
+    onEdit ({ id }) {
+      this.$datasetId = id
+      const loading = this.$showLoading()
+      getCommonDataset(this.$datasetId).finally(() => {
+        this.$closeLoading(loading)
+      }).then(({ data: { mediaList } }) => {
+        this.assets = mediaList.map(({ id, tag, type, keyName, adDuration, minioData }) => minioData
+          ? {
+            key: id,
+            id,
+            info: `${AssetTagInfo[tag]} ${AssetTypeInfo[type]}`,
+            tag,
+            type,
+            keyName,
+            name: minioData.originalName,
+            duration: adDuration,
+            disabled: type === AssetType.VIDEO,
+            file: {
+              type,
+              url: keyName
+            }
+          }
+          : {
+            key: id,
+            id,
+            tag,
+            type,
+            name: '素材已删除',
+            keyName,
+            duration: adDuration,
+            disabled: true
+          })
+        this.$refs.assetDialog.show()
+      })
+    },
+    transformDatasetAsset (asset) {
+      asset.tagInfo = AssetTagInfo[asset.tag]
+      asset.typeName = AssetTypeInfo[asset.type]
+      asset.file = {
+        type: asset.type,
+        url: asset.keyName,
+        thumb: getAssetThumb(asset.minioData)
+      }
+      asset.name = asset.minioData.originalName
+      return asset
+    },
+    onAddAssets (value) {
+      value.forEach(asset => {
+        const { tag, type, keyName, originalName, file } = asset
+        this.assets.push({
+          key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
+          info: `${AssetTagInfo[tag]} ${AssetTypeInfo[type]}`,
+          disabled: type === AssetType.VIDEO,
+          name: originalName,
+          tag,
+          type,
+          keyName,
+          duration: getAssetDuration(asset),
+          file
+        })
+      })
+      return Promise.resolve()
+    },
+    onEditAssetDuration (asset, { newVal, oldVal }) {
+      if (!newVal || !/^\d+$/.test(newVal) || Number(newVal) < 1) {
+        return
+      }
+      if (newVal !== oldVal) {
+        asset.adDuration = Number(newVal)
+      }
+    },
+    onDelAsset (index) {
+      this.assets.splice(index)
+    },
+    onSaveAssets (done) {
+      updateDatasetAssets(this.$datasetId, this.assets.map(({ tag, type, keyName, duration }, index) => {
+        return {
+          tag,
+          type,
+          keyName,
+          adDuration: duration,
+          orderNo: index
+        }
+      })).then(done)
+    }
+  }
+}
+</script>