Jelajahi Sumber

feat: c-transfer

Casper Dai 2 tahun lalu
induk
melakukan
82e164b3d2

+ 3 - 2
src/components/service/DraggableItem/index.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="l-flex--row c-sibling-item--v o-draggable-item">
-    <div class="l-flex__auto l-flex--row mover">
+    <div class="l-flex__auto l-flex--row c-sibling-item mover">
       <i class="l-flex__none o-draggable-item__mover el-icon-sort has-active" />
       <div
-        class="l-flex__auto c-sibling-item has-padding--v u-ellipsis has-active"
+        class="l-flex__auto has-padding--v u-ellipsis has-active"
         @click="onView"
       >
         {{ label }}
@@ -61,6 +61,7 @@ export default {
 
 <style lang="scss" scoped>
 .o-draggable-item {
+  min-width: $width--xl;
   border: 1px solid $gray;
   border-radius: $radius--sm;
 

+ 191 - 28
src/components/service/EventTargetPicker/index.vue

@@ -5,8 +5,8 @@
         <span class="c-sibling-item u-font-size--sm">上播内容</span>
         <i
           v-if="isAssets"
-          class="c-sibling-item el-icon-circle-plus-outline u-color--blue u-pointer"
-          @click="onAddAsset"
+          class="c-sibling-item el-icon-edit u-color--blue u-pointer"
+          @click="onEditAssets"
         />
       </div>
       <div
@@ -25,6 +25,13 @@
             :value="option.value"
           />
         </el-select>
+        <div
+          v-if="isAssets"
+          class="c-sibling-item o-button"
+          @click="onImportDataset"
+        >
+          导入
+        </div>
         <div
           class="l-flex--row c-sibling-item far has-active u-ellipsis"
           @click="onViewCurrentTarget"
@@ -33,22 +40,29 @@
         </div>
       </div>
     </div>
-    <draggable
-      v-if="isAssets"
-      v-model="currentTarget.assets"
-      class="l-flex__fill c-sibling-item--v u-overflow-y--auto"
-      handle=".mover"
-      animation="300"
-    >
-      <draggable-item
-        v-for="(asset, index) in currentTarget.assets"
-        :key="asset.key"
-        :item="asset"
-        :hide-duration="hideDuration"
-        @view="onViewAsset"
-        @del="onDelAsset(index)"
+    <template v-if="isAssets">
+      <draggable
+        v-if="currentTarget.assets.length"
+        v-model="currentTarget.assets"
+        class="l-flex__fill c-sibling-item--v u-overflow-y--auto"
+        handle=".mover"
+        animation="300"
+      >
+        <draggable-item
+          v-for="(asset, index) in currentTarget.assets"
+          :key="asset.key"
+          :item="asset"
+          :hide-duration="hideDuration"
+          @view="onViewAsset"
+          @del="onDelAsset(index)"
+        />
+      </draggable>
+      <el-empty
+        v-else
+        class="l-flex__fill l-flex--row center"
+        description="请添加素材"
       />
-    </draggable>
+    </template>
     <schema-table
       v-else
       :key="currentTarget.type"
@@ -59,13 +73,53 @@
       highlight-current-row
       @row-click="onClickRow"
     />
-    <selection-table-dialog
+    <confirm-dialog
       ref="assetTableDialog"
-      title="上播内容选择"
-      message="请选择上播内容"
-      :schema="assetTableSchema"
+      title="上播内容"
+      size="xl fixed"
+      append-to-body
+      @confirm="onSaveAssets"
+    >
+      <c-transfer
+        class="l-flex__auto"
+        :source-schema="assetTableSchema"
+        :add="onAddAssets"
+      >
+        <el-empty
+          v-if="!selectionAssets.length"
+          class="l-flex__auto l-flex--row center"
+          description="请添加素材"
+        />
+        <draggable
+          v-else
+          v-model="selectionAssets"
+          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 selectionAssets"
+            :key="asset.key"
+            :item="asset"
+            @view="onViewAsset"
+            @del="onDelSelectionAsset(index)"
+          />
+        </draggable>
+      </c-transfer>
+    </confirm-dialog>
+    <radio-table-dialog
+      ref="datasetDialog"
+      title="素材包"
+      message="请选择素材包"
+      :schema="datasetSchema"
+      append-to-body
+      @confirm="onChoosenDataset"
+    />
+    <table-dialog
+      ref="contentDialog"
+      title="素材包"
+      :schema="contentSchema"
       append-to-body
-      @confirm="onChoosenAsset"
     />
     <preview-dialog ref="previewDialog" />
     <material-dialog ref="materialDialog" />
@@ -80,10 +134,19 @@ import {
   EventTargetInfo,
   AssetTagInfo,
   AssetType,
-  AssetTypeInfo
+  AssetTypeInfo,
+  Dataset
 } from '@/constant'
-import { getAssetDuration } from '@/utils'
+import {
+  parseDuration,
+  getAssetThumb,
+  getAssetDuration
+} from '@/utils'
 import { getRatios } from '@/api/device'
+import {
+  getDatasets,
+  getCommonDataset
+} from '@/api/asset'
 import { getSchedules } from '@/api/calendar'
 import { getPrograms } from '@/api/program'
 import { assetTableMixin } from '@/mixins/asset-table'
@@ -116,7 +179,32 @@ export default {
         { value: EventTarget.RECUR, label: EventTargetInfo[EventTarget.RECUR] },
         { value: EventTarget.ASSETS, label: EventTargetInfo[EventTarget.ASSETS] }
       ],
-      currentTarget: null
+      currentTarget: null,
+      selectionAssets: [],
+      datasetSchema: {
+        list: getDatasets,
+        condition: { type: Dataset.COMMON },
+        cols: [
+          { prop: 'name', label: '名称', 'align': 'center' },
+          { type: 'invoke', render: [
+            { label: '查看', on: this.onViewDataset }
+          ] }
+        ]
+      },
+      contentSchema: {
+        singlePage: true,
+        list: this.getAssets,
+        cols: [
+          { prop: 'tagInfo', label: '类型', align: 'center', width: 80 },
+          { prop: 'typeName', label: '文件', align: 'center', width: 80 },
+          { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
+          { prop: 'name', label: '' },
+          { prop: 'duration', label: '上播时长', 'align': 'center', width: 100 },
+          { type: 'invoke', render: [
+            { label: '查看', on: this.onViewAsset }
+          ] }
+        ]
+      }
     }
   },
   computed: {
@@ -251,13 +339,21 @@ export default {
     onClickRow (row) {
       this.currentTarget.detail = row
     },
-    onAddAsset () {
+    onEditAssets () {
+      this.selectionAssets = this.currentTarget.assets.map(asset => {
+        return {
+          ...asset
+        }
+      })
       this.$refs.assetTableDialog.show()
     },
-    onChoosenAsset ({ value, done }) {
+    onDelSelectionAsset (index) {
+      this.selectionAssets.splice(index, 1)
+    },
+    onAddAssets (value) {
       value.forEach(item => {
         const { tag, type, keyName, originalName, size, md5, file } = item
-        this.currentTarget.assets.push({
+        this.selectionAssets.push({
           key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
           tag,
           type,
@@ -271,8 +367,75 @@ export default {
           file
         })
       })
+      return Promise.resolve()
+    },
+    onSaveAssets (done) {
+      this.currentTarget.assets = this.selectionAssets.map(asset => {
+        return {
+          ...asset
+        }
+      })
       done()
     },
+    onImportDataset () {
+      this.$datasetAssets = null
+      this.$refs.datasetDialog.show()
+    },
+    onViewDataset ({ id }) {
+      this.$datasetId = id
+      this.$datasetAssets = null
+      this.$refs.contentDialog.show()
+    },
+    getAssets () {
+      if (this.$datasetAssets) {
+        return Promise.resolve({ data: this.$datasetAssets })
+      }
+      return getCommonDataset(this.$datasetId).then(({ data: { mediaList } }) => {
+        this.$datasetAssets = mediaList.map(this.transformDatasetAsset)
+        return {
+          data: this.$datasetAssets
+        }
+      })
+    },
+    transformDatasetAsset (asset) {
+      asset.tagInfo = AssetTagInfo[asset.tag]
+      asset.typeName = AssetTypeInfo[asset.type]
+      asset.file = {
+        type: asset.type,
+        url: asset.keyName,
+        thumb: asset.minioData ? getAssetThumb(asset.minioData) : null
+      }
+      asset.name = asset.minioData?.originalName || '素材已删除'
+      asset.duration = parseDuration(asset.adDuration)
+      return asset
+    },
+    onChoosenDataset ({ value, done }) {
+      if (this.$datasetId !== value.id) {
+        this.$datasetId = value.id
+        this.$datasetAssets = null
+      }
+      this.getAssets().then(({ data }) => {
+        data.forEach(item => {
+          const { tag, type, keyName, adDuration, name, file, minioData } = item
+          if (minioData) {
+            this.currentTarget.assets.push({
+              key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
+              tag,
+              type,
+              keyName,
+              size: minioData.size,
+              md5: minioData.md5,
+              info: `${AssetTagInfo[tag]} ${AssetTypeInfo[type]}`,
+              name,
+              duration: adDuration,
+              disabled: type === AssetType.VIDEO,
+              file
+            })
+          }
+        })
+        done()
+      })
+    },
     onDelAsset (index) {
       this.currentTarget.assets.splice(index, 1)
     },

+ 19 - 39
src/components/service/Schedule/ScheduleSwiper/index.vue

@@ -9,34 +9,20 @@
     @back="onBack"
   >
     <template v-if="editable">
-      <div class="l-flex__auto l-flex">
-        <div class="l-flex__none l-flex--row has-top-padding">
-          <schema-table
-            ref="table"
-            class="c-schedule-swiper__table"
-            :schema="programSchema"
-            @row-click="onRowClick"
-            @selection-change="onSelectionChange"
-          />
-        </div>
-        <div class="l-flex__none l-flex--row c-schedule-swiper__btn">
-          <el-button
-            type="primary"
-            :disabled="!programs.length"
-            @click="onAdd"
-          >
-            <i class="el-icon-arrow-right" />
-          </el-button>
-        </div>
+      <c-transfer
+        class="l-flex__auto c-sibling-item--v"
+        :source-schema="programSchema"
+        :add="onAdd"
+      >
         <el-empty
-          v-if="isEmpty"
+          v-if="!scheduleOptions.events.length"
           class="l-flex__auto l-flex--row center"
-          description="请添加节目"
+          description="请添加素材"
         />
         <draggable
           v-else
           v-model="scheduleOptions.events"
-          class="l-flex__auto l-flex--col has-padding--v u-font-size--sm u-overflow-y--auto"
+          class="l-flex__auto l-flex--col u-font-size--sm u-overflow--auto"
           handle=".mover"
           animation="300"
         >
@@ -48,14 +34,14 @@
             @del="onDel(index)"
           />
         </draggable>
-      </div>
+      </c-transfer>
     </template>
     <template v-else>
       <el-empty
         v-if="isEmpty"
         description="暂无节目"
       />
-      <div class="l-flex--col has-padding--v u-font-size--sm u-overflow-y--auto">
+      <div class="l-flex--col c-sibling-item--v u-font-size--sm u-overflow-y--auto">
         <div
           v-for="program in events"
           :key="program.key"
@@ -91,23 +77,23 @@ export default {
   data () {
     return {
       record: null,
-      programs: [],
       programSchema: {
-        condition: { status: State.AVAILABLE, resolutionRatio: this.detail.resolutionRatio },
         list: getPrograms,
+        condition: { status: State.AVAILABLE, resolutionRatio: this.detail.resolutionRatio },
         filters: [
           { key: 'name', type: 'search', placeholder: '节目名称' }
         ],
         cols: [
-          { type: 'selection' },
           { label: '缩略图', type: 'asset', render ({ img }) {
             return img
               ? { thumb: img }
               : null
           }, on: this.onView },
-          { prop: 'name', label: '节目名称' }
-        ],
-        pagination: { small: true, layout: 'prev,pager,next' }
+          { prop: 'name', label: '节目名称' },
+          { type: 'invoke', render: [
+            { label: '查看', on: this.onView }
+          ] }
+        ]
       }
     }
   },
@@ -171,15 +157,9 @@ export default {
         count: this.events.length
       }
     },
-    onRowClick (row) {
-      this.$refs.table.getInst().toggleRowSelection(row)
-    },
-    onSelectionChange (programs) {
-      this.programs = programs
-    },
-    onAdd () {
-      this.programs.forEach(this.add)
-      this.$refs.table.getInst().clearSelection()
+    onAdd (value) {
+      value.forEach(this.add)
+      return Promise.resolve()
     },
     add ({ id, name, duration, buckets, itemConfigName }) {
       this.events.push({

+ 1 - 1
src/components/service/Schedule/components/ScheduleWrapper.vue

@@ -5,7 +5,7 @@
   >
     <div
       v-if="!hideHeader"
-      class="l-flex__none l-flex--row c-schedule-wrapper__header"
+      class="l-flex__none l-flex--row c-sibling-item--v c-schedule-wrapper__header"
     >
       <i
         v-if="editable"

+ 92 - 0
src/components/transfer/Transfer/index.vue

@@ -0,0 +1,92 @@
+<template>
+  <div class="l-flex c-transfer">
+    <div class="l-flex__none l-flex--col c-transfer__list">
+      <schema-table
+        ref="sourceTable"
+        :schema="sourceTableSchema"
+        @row-click="onSourceRowClick"
+        @selection-change="onSourceSelectionChange"
+      />
+    </div>
+    <div class="l-flex__none l-flex--row c-transfer__btn">
+      <el-button
+        type="primary"
+        :disabled="!sourceSelectionVal.length"
+        @click="onAdd"
+      >
+        <i class="el-icon-arrow-right" />
+      </el-button>
+    </div>
+    <div class="l-flex__fill l-flex--col u-overflow-x--auto">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CTransfer',
+  props: {
+    sourceSchema: {
+      type: Object,
+      required: true
+    },
+    selectable: {
+      type: Function,
+      default: null
+    },
+    add: {
+      type: Function,
+      required: true
+    }
+  },
+  data () {
+    return {
+      sourceSelectionVal: [],
+      targetSelectionVal: []
+    }
+  },
+  computed: {
+    sourceTableSchema () {
+      const { cols, ...data } = this.sourceSchema
+      return {
+        pagination: { small: true },
+        ...data,
+        cols: [
+          { type: 'selection', selectable: this.selectable },
+          ...cols
+        ]
+      }
+    }
+  },
+  methods: {
+    onSourceRowClick (row) {
+      this.$refs.sourceTable.getInst().toggleRowSelection(row)
+    },
+    onSourceSelectionChange (val) {
+      this.sourceSelectionVal = val
+    },
+    onAdd () {
+      this.add(this.sourceSelectionVal).then(() => {
+        this.$refs.sourceTable.getInst().clearSelection()
+        this.$emit('changed')
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-transfer {
+  &__list {
+    width: 520px;
+  }
+
+  &__btn {
+    padding: 0 $spacing $spacing;
+    margin: 0 $spacing;
+    border-left: 1px solid $border;
+    border-right: 1px solid $border;
+  }
+}
+</style>

+ 4 - 4
src/mixins/asset-table.js

@@ -11,8 +11,8 @@ export const assetTableMixin = {
   data () {
     return {
       assetTableSchema: {
-        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.AD, type: AssetType.IMAGE, originalName: '' },
         list: getAssets,
+        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.AD, type: AssetType.IMAGE },
         filters: [
           { key: 'tag', type: 'select', options: [
             { value: AssetTag.AD, label: AssetTagInfo[AssetTag.AD] },
@@ -29,7 +29,7 @@ export const assetTableMixin = {
         cols: [
           { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
           { prop: 'originalName', label: '' },
-          { prop: 'diff', label: '其他', 'align': 'right' },
+          { prop: 'diff', label: '其他', width: 100, 'align': 'right' },
           { type: 'invoke', render: [
             { label: '查看', on: this.onViewAsset }
           ] }
@@ -48,8 +48,8 @@ export const assetPublicTableMixin = {
   data () {
     return {
       assetTableSchema: {
-        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.PUBLICITY, type: AssetType.IMAGE, originalName: '' },
         list: getAssets,
+        condition: { status: State.AVAILABLE_TENANT, tag: AssetTag.PUBLICITY, type: AssetType.IMAGE },
         filters: [
           { key: 'tag', type: 'select', options: [
             { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
@@ -66,7 +66,7 @@ export const assetPublicTableMixin = {
         cols: [
           { prop: 'file', label: '文件', type: 'asset', on: this.onViewAsset },
           { prop: 'originalName', label: '' },
-          { prop: 'diff', label: '其他', 'align': 'right' },
+          { prop: 'diff', label: '其他', width: 100, 'align': 'right' },
           { type: 'invoke', render: [
             { label: '查看', on: this.onViewAsset }
           ] }

+ 5 - 1
src/scss/bem/_component.scss

@@ -14,9 +14,13 @@
     height: 88%;
   }
 
+  &.xl {
+    width: 1280px + 2 * $spacing;
+    min-height: 480px;
+  }
+
   &.lg {
     width: 960px + 2 * $spacing;
-    min-width: 960px;
     min-height: 480px;
   }
 

+ 1 - 1
src/views/ad/automation/contract/index.vue

@@ -425,7 +425,7 @@ export default {
         const { tag, type, keyName, originalName, file } = asset
         this.assets.push({
           key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
-          tagInfo: AssetTagInfo[tag],
+          info: `${AssetTagInfo[tag]} ${AssetTypeInfo[type]}`,
           disabled: type === AssetType.VIDEO,
           name: originalName,
           type,