Parcourir la source

feat: automatic scheduling

Casper Dai il y a 3 ans
Parent
commit
85820273bd

+ 37 - 11
src/components/form/EditInput/index.vue

@@ -9,6 +9,7 @@
       v-if="disabled"
       ref="target"
       class="o-editor u-ellipsis"
+      :class="align"
     >
       {{ value }}
     </div>
@@ -17,6 +18,7 @@
       ref="target"
       :value="value"
       class="o-editor input u-ellipsis"
+      :class="align"
       maxlength="50"
       @focus="onFocus"
       @blur="onBlur"
@@ -37,6 +39,10 @@ export default {
     disabled: {
       type: [Boolean, String],
       default: false
+    },
+    align: {
+      type: String,
+      default: ''
     }
   },
   data () {
@@ -63,16 +69,23 @@ export default {
   },
   methods: {
     onFocus () {
+      this._oldVal = this.value
+      this._newVal = this.value
       this.manual = true
       this.showTip = false
     },
     onBlur () {
       this.manual = false
-      this.$emit('edit')
+      this.$emit('edit', {
+        newVal: typeof this._newVal === 'string' ? this._newVal.trim() : '',
+        oldVal: this._oldVal
+      })
       this.$nextTick(this.checkSize)
     },
     onInput (event) {
-      this.$emit('input', event.target.value)
+      const val = event.target.value
+      this._newVal = val
+      this.$emit('input', val)
     },
     checkSize () {
       if (this.manual) {
@@ -98,22 +111,35 @@ export default {
   line-height: 38px;
   border: 1px solid transparent;
 
+  &.left {
+    text-align: left;
+  }
+
+  &.center {
+    text-align: center;
+  }
+
+  &.right {
+    text-align: right;
+  }
+
   &.input {
     line-height: 40px;
     outline: 0;
-    -webkit-appearance: none;
     border-radius: $radius--mini;
     background-color: transparent;
+  }
 
-    &:hover {
-      border-color: $gray--dark;
-    }
+  &.border:hover,
+  &.input:hover {
+    border-color: $gray--dark;
+  }
 
-    &:focus {
-      color: $black;
-      border-color: #409eff;
-      background-color: $blue--light;
-    }
+  &.border,
+  &.input:focus {
+    color: $black;
+    border-color: #409eff;
+    background-color: $blue--light;
   }
 }
 </style>

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

@@ -127,7 +127,7 @@ export default {
     onDeviceToggle (device) {
       if (!this.selectedDevice || this.selectedDevice.id !== device.id) {
         this.selectedDevice = device
-        this.$emit('change', this.deviceId)
+        this.$emit('change', { ...device })
       }
     },
     getDevices () {

+ 1 - 1
src/router/index.js

@@ -363,7 +363,7 @@ export const asyncRoutes = [
         name: 'ad-order-task',
         path: 'order/task',
         component: () => import('@/views/ad/task/index'),
-        meta: { title: '上播管理' }
+        meta: { title: '屏体上播管理' }
       },
       {
         name: 'ad-order-scheduling',

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

@@ -15,3 +15,7 @@
 .el-message-box__message {
   word-wrap: break-word;
 }
+
+.el-range-separator {
+  box-sizing: content-box;
+}

+ 59 - 1
src/views/ad/api.js

@@ -2,9 +2,19 @@ import request from '@/utils/request'
 import {
   resolve,
   reject,
-  send
+  send,
+  add,
+  del,
+  update,
+  confirmAndSend
 } from '@/api/base'
 
+export const TaskType = {
+  ORDER: 1,
+  IMAGE: 2,
+  VIDEO: 3
+}
+
 export function getOrders (query, options) {
   const { pageNum: pageIndex, pageSize, ...params } = query
   return request({
@@ -90,6 +100,45 @@ export function getOrdersByDevice (query, options) {
   })
 }
 
+export function getTasks (query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/ad/task/list',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params,
+      ...options
+    }
+  })
+}
+
+export function addTask (data, options) {
+  return add({
+    url: '/ad/task',
+    method: 'POST',
+    data,
+    ...options
+  })
+}
+
+export function deleteTask (id, options) {
+  return del({
+    url: `/ad/task/${id}`,
+    method: 'DELETE',
+    ...options
+  })
+}
+
+export function updateTask (data, options) {
+  return update({
+    url: '/ad/task',
+    method: 'PUT',
+    data,
+    ...options
+  })
+}
+
 export function createScheduling (deviceIds, data) {
   return send({
     url: '/ad/tenant/order/scheduling',
@@ -107,6 +156,7 @@ export function getDeviceSchedulings (query, options) {
     url: '/ad/tenant/order/scheduling/list',
     method: 'GET',
     params: {
+      order: 'startDate',
       pageIndex, pageSize,
       ...params,
       ...options
@@ -126,3 +176,11 @@ export function getDeviceScheduling (query, options) {
     }
   })
 }
+
+export function setSchedulingEnable (id, enable) {
+  return confirmAndSend(enable ? '发布排期' : '停用排期', null, {
+    url: `/ad/tenant/order/scheduling/${enable ? 'enable' : 'disable'}`,
+    method: 'POST',
+    data: { id }
+  })
+}

+ 107 - 0
src/views/ad/components/TaskTargetDialog/index.vue

@@ -0,0 +1,107 @@
+<template>
+  <el-dialog
+    ref="tableDialog"
+    :visible.sync="choosing"
+    title="素材选择"
+    custom-class="c-dialog medium"
+    :close-on-click-modal="false"
+    v-bind="$attrs"
+  >
+    <grid-table :schema="schema">
+      <grid-table-item v-slot="item">
+        <media-card
+          :source="item"
+          @dblclick="onChoosen"
+        >
+          <i
+            class="o-card__play el-icon-video-play has-active u-pointer"
+            @click.stop="onView(item)"
+          />
+        </media-card>
+      </grid-table-item>
+    </grid-table>
+    <materail-dialog ref="materailDialog" />
+  </el-dialog>
+</template>
+
+<script>
+import {
+  getAssets,
+  getThumbnailUrl
+} from '@/api/asset'
+import {
+  State,
+  AssetType
+} from '@/constant'
+
+export default {
+  name: 'TaskTargetDialog',
+  data () {
+    return {
+      choosing: false,
+      schema: {
+        condition: { originalName: '', type: AssetType.IMAGE, status: State.AVAILABLE },
+        list: getAssets,
+        transform: this.transformAsset,
+        filters: [
+          { key: 'type', type: 'select', options: [
+            { value: AssetType.IMAGE, label: '图片' },
+            { value: AssetType.VIDEO, label: '视频' }
+          ] },
+          { key: 'originalName', type: 'search', placeholder: '素材名称' }
+        ]
+      }
+    }
+  },
+  methods: {
+    show () {
+      this.choosing = true
+    },
+    transformAsset ({ type, originalName, keyName, thumbnail, size, duration, md5 }) {
+      const asset = {
+        type,
+        name: originalName,
+        keyName,
+        size,
+        md5
+      }
+      switch (type) {
+        case AssetType.IMAGE:
+          asset.thumb = getThumbnailUrl(thumbnail)
+          break
+        default:
+          asset.duration = duration
+          if (thumbnail) {
+            asset.thumb = getThumbnailUrl(thumbnail)
+          } else {
+            asset.icon = `${type === AssetType.VIDEO ? 'video' : 'audio'}-bg`
+          }
+          break
+      }
+      return asset
+    },
+    onChoosen (asset) {
+      this.$emit('choosen', asset)
+      this.choosing = false
+    },
+    onView ({ type, keyName }) {
+      this.$emit('view', { type, url: keyName })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.o-card {
+  &__play {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    padding: 2px;
+    font-size: 24px;
+    border-radius: 50%;
+    background-color: rgba(#000, 0.4);
+    transform: translate(-50%, -50%);
+  }
+}
+</style>

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

@@ -27,6 +27,38 @@
       title="广告内容"
       :schema="adSchema"
     />
+    <preview-dialog ref="previewDialog" />
+    <confirm-dialog
+      ref="schedulingOptionsDialog"
+      title="一键排单"
+      @confirm="onConfirm"
+    >
+      <div class="c-grid-form mini u-align-self--center">
+        <div class="c-grid-form__label required">开始日期</div>
+        <el-date-picker
+          v-model="startDate"
+          class="c-event__option"
+          type="date"
+          placeholder="请选择开始日期"
+          value-format="yyyy-MM-dd"
+          :editable="false"
+          :clearable="false"
+          :picker-options="pickerOptions"
+          @change="onDateTimeChange('startDate')"
+        />
+        <div class="c-grid-form__label required">结束日期</div>
+        <el-date-picker
+          v-model="endDate"
+          class="c-grid-form__info c-event__option"
+          type="date"
+          placeholder="请选择结束日期"
+          value-format="yyyy-MM-dd"
+          :editable="false"
+          :clearable="false"
+          :picker-options="endPickerOptions"
+        />
+      </div>
+    </confirm-dialog>
   </wrapper>
 </template>
 
@@ -41,7 +73,8 @@ import {
   createScheduling,
   getDeviceSchedulings,
   getDeviceScheduling,
-  getOrderDetail
+  getOrderDetail,
+  setSchedulingEnable
 } from '../api'
 
 export default {
@@ -64,7 +97,10 @@ export default {
             { label: '查看', render ({ file }) { return !!file }, on: this.onViewSource }
           ] }
         ]
-      }
+      },
+      minDate: '',
+      startDate: '',
+      endDate: ''
     }
   },
   computed: {
@@ -80,9 +116,9 @@ export default {
           { type: 'refresh' },
           { prop: 'startDate', label: '日期' },
           { prop: 'createTime', label: '生成时间' },
-          { prop: 'statusTag', type: 'tag', align: 'center' },
+          { prop: 'statusTag', type: 'tag', align: 'center', on: this.onToggle },
           { type: 'invoke', render: [
-            { label: '详情', on: this.onView }
+            { label: '详情', render ({ status }) { return status > 1 }, on: this.onView }
           ] }
         ]
       }
@@ -99,30 +135,69 @@ export default {
           ] }
         ]
       }
+    },
+    pickerOptions () {
+      return {
+        disabledDate: this.isDisableDate
+      }
+    },
+    endPickerOptions () {
+      return {
+        disabledDate: this.isDisableEndDate
+      }
     }
   },
   methods: {
-    onChange (deviceId) {
-      this.deviceId = deviceId
+    isDisableDate (date) {
+      return date < this.minDate
+    },
+    isDisableEndDate (date) {
+      return date < new Date(`${this.startDate} 00:00:00`)
+    },
+    onChange ({ id }) {
+      this.deviceId = id
     },
     onScheduling () {
-      const startDate = parseTime(new Date(), '{y}-{m}-{d}')
+      const date = parseTime(new Date(), '{y}-{m}-{d}')
+      this.minDate = new Date(`${date} 00:00:00`)
+      this.startDate = date
+      this.endDate = date
+      this.$refs.schedulingOptionsDialog.show()
+    },
+    onDateTimeChange () {
+      if (this.startDate > this.endDate) {
+        this.endDate = this.startDate
+      }
+    },
+    onConfirm (done) {
+      this.$confirm(
+        '新生成的排期将覆盖原有排期,确认继续?',
+        '一键排单',
+        { type: 'warning' }
+      ).then(() => this.createScheduling(this.startDate, this.endDate)).then(done)
+    },
+    createScheduling (startDate, endDate) {
       createScheduling([this.deviceId], {
         startDate,
-        endDate: startDate
+        endDate: endDate || startDate
       }).then(() => {
         this.$refs.table.pageTo()
       })
     },
     transform (scheduling) {
-      const { status } = scheduling
-      scheduling.statusTag = {
-        label: ['生成中', '待发布', '已发布'][status],
-        type: ['primary', 'warning', 'success'][status]
-      }
+      scheduling.statusTag = this.getStatusTag(scheduling.status, scheduling.startDate < parseTime(new Date(), '{y}-{m}-{d}'))
       return scheduling
     },
-    onView ({ id, startDate }) {
+    getStatusTag (status, hasExpired) {
+      return {
+        label: ['生成失败', '生成中', hasExpired ? '未发布' : '待审核', hasExpired ? '已生效' : '生效中'][status],
+        type: hasExpired ? null : ['error', 'primary', 'warning', 'success'][status]
+      }
+    },
+    onView ({ id, startDate, status }) {
+      if (status === 0) {
+        return
+      }
       this.title = startDate
       this.schedulingId = id
       this.$refs.tableDialog.show()
@@ -176,6 +251,22 @@ export default {
     },
     onViewAsset (asset) {
       this.$refs.previewDialog.show(asset)
+    },
+    onToggle (scheduling) {
+      if (scheduling.status === 0) {
+        this.$confirm('重新排单?').then(() => {
+          this.createScheduling(parseTime(new Date(), '{y}-{m}-{d}'))
+        })
+        return
+      }
+      if (scheduling.status === 1) {
+        return
+      }
+      const enable = scheduling.status === 2
+      setSchedulingEnable(scheduling.id, enable).then(() => {
+        scheduling.status = enable ? 3 : 2
+        scheduling.statusTag = this.getStatusTag(scheduling.status)
+      })
     }
   }
 }

+ 525 - 24
src/views/ad/task/index.vue

@@ -12,7 +12,7 @@
       @change="onChange"
     />
     <schema-table
-      v-if="deviceId"
+      v-if="device"
       ref="table"
       row-key="id"
       :schema="schema"
@@ -22,28 +22,162 @@
       title="广告内容"
       :schema="adSchema"
     />
+    <confirm-dialog
+      ref="taskDialog"
+      title="新增任务"
+      @confirm="onAddTask"
+    >
+      <div class="c-grid-form mini u-align-self--center">
+        <div class="c-grid-form__label required">素材</div>
+        <div
+          class="c-grid-form__info c-grid-form__option c-task u-pointer"
+          :data-info="assetInfo"
+        >
+          <div
+            class="c-task__name has-padding--h"
+            @click="onChooseAsset"
+          >
+            <div class="u-ellipsis">{{ taskAssetName }}</div>
+          </div>
+        </div>
+        <div class="c-grid-form__label">上刊日期</div>
+        <div class="l-flex--row c-event__option">
+          <el-date-picker
+            v-model="taskDate"
+            type="daterange"
+            range-separator="至"
+            value-format="yyyy-MM-dd"
+            :editable="false"
+            :clearable="false"
+          />
+        </div>
+        <div class="c-grid-form__label">上播方式</div>
+        <schema-select
+          v-model="taskTime.type"
+          :schema="taskTimeTypeSelectSchema"
+        />
+        <template v-if="taskTime.type === 2">
+          <div class="c-grid-form__label">时段</div>
+          <div class="l-flex--row c-grid-form__option">
+            <el-time-picker
+              key="range-picker"
+              v-model="taskTime.val"
+              is-range
+              format="HH:mm"
+              value-format="HH:mm"
+              :clearable="false"
+            />
+          </div>
+        </template>
+        <template v-if="taskTime.type === 3">
+          <div class="c-grid-form__label">时间点</div>
+          <div class="l-flex--row c-grid-form__option">
+            <el-time-picker
+              key="time-picker"
+              v-model="taskTime.point"
+              value-format="HH:mm:ss"
+              :clearable="false"
+            />
+          </div>
+        </template>
+        <div class="c-grid-form__label">上播时长(s)</div>
+        <div class="l-flex--row c-grid-form__option">
+          <el-input-number
+            v-model="taskDuration"
+            :min="1"
+            :max="86400"
+            step-strictly
+          />
+        </div>
+        <div class="c-grid-form__label">保底次数</div>
+        <div class="l-flex--row c-grid-form__option">
+          <el-input-number
+            v-model="taskCount"
+            :min="1"
+            step-strictly
+          />
+        </div>
+        <div class="c-grid-form__label">上播次数</div>
+        <div class="l-flex--row c-grid-form__option">
+          <el-input-number
+            v-model="taskAuditCount"
+            :min="1"
+            step-strictly
+          />
+        </div>
+      </div>
+    </confirm-dialog>
+    <task-target-dialog
+      ref="taskTargetDialog"
+      @view="onViewAsset"
+      @choosen="onChoosenAsset"
+    />
+    <preview-dialog ref="previewDialog" />
+    <confirm-dialog
+      ref="timeDialog"
+      title="上播时段"
+      @confirm="onConfirmTime"
+    >
+      <div class="c-grid-form mini u-align-self--center">
+        <div class="c-grid-form__label">方式</div>
+        <schema-select
+          v-model="taskTime.type"
+          :schema="taskTimeTypeSelectSchema"
+        />
+        <template v-if="taskTime.type === 2">
+          <div class="c-grid-form__label">时段</div>
+          <div class="l-flex--row c-grid-form__option">
+            <el-time-picker
+              key="task-range-picker"
+              v-model="taskTime.val"
+              is-range
+              format="HH:mm"
+              value-format="HH:mm"
+              :clearable="false"
+            />
+          </div>
+        </template>
+        <template v-if="taskTime.type === 3">
+          <div class="c-grid-form__label">时间点</div>
+          <div class="l-flex--row c-grid-form__option">
+            <el-time-picker
+              key="task-time-picker"
+              v-model="taskTime.point"
+              value-format="HH:mm:ss"
+              :clearable="false"
+            />
+          </div>
+        </template>
+      </div>
+    </confirm-dialog>
   </wrapper>
 </template>
 
 <script>
+import { AssetType } from '@/constant'
 import {
-  State,
-  AssetType
-} from '@/constant'
-import {
+  parseTime,
   parseByte,
   parseDuration
 } from '@/utils'
 import {
-  getOrdersByDevice,
+  TaskType,
+  getTasks,
+  addTask,
+  deleteTask,
+  updateTask,
   getOrderDetail
 } from '../api'
+import TaskTargetDialog from '../components/TaskTargetDialog'
 
 export default {
   name: 'AdOrderTask',
+  components: {
+    TaskTargetDialog
+  },
   data () {
     return {
-      deviceId: '',
+      device: null,
       adSchema: {
         list: this.getSources,
         cols: [
@@ -57,44 +191,162 @@ export default {
             { label: '查看', render ({ file }) { return !!file }, on: this.onViewSource }
           ] }
         ]
+      },
+      minDate: '',
+      taskDate: '',
+      taskDuration: 5,
+      taskCount: 100,
+      taskAuditCount: 100,
+      taskAsset: null,
+      taskTime: {},
+      taskTimeTypeSelectSchema: {
+        options: [
+          { value: 1, label: '开机期间' },
+          { value: 2, label: '时段' },
+          { value: 3, label: '时间点' }
+        ]
       }
     }
   },
   computed: {
     schema () {
       return {
-        condition: { deviceId: this.deviceId, orderStatus: State.RESOLVED },
-        list: getOrdersByDevice,
+        buttons: [{ type: 'add', on: this.onAdd }],
+        condition: { deviceId: this.device.id },
+        list: getTasks,
         transform: this.transform,
         cols: [
           { type: 'refresh' },
-          { prop: 'startDate', label: '起始日期' },
-          { prop: 'range', label: '时段' },
-          { prop: 'freq', label: '频率' },
+          { prop: 'type', label: '类型', width: 60, align: 'center' },
+          { label: '上刊日期', 'min-width': 220, render: (data, h) => data.from > TaskType.ORDER
+            ? h('el-date-picker', {
+              staticClass: 'o-date-picker',
+              props: {
+                value: [data.startDate, data.endDate],
+                type: 'daterange',
+                rangeSeparator: '至',
+                valueFormat: 'yyyy-MM-dd',
+                editable: false,
+                clearable: false
+              },
+              on: {
+                input: val => this.onDateEdit(data, val)
+              }
+            })
+            : `${data.startDate} 至 ${data.endDate}`, align: 'center' },
+          { label: '上播时段', render: (data, h) => data.from > TaskType.ORDER
+            ? h('div', {
+              staticClass: 'o-date-picker jc',
+              on: {
+                click: () => this.onEditTime(data)
+              }
+            }, data.range)
+            : data.range, 'min-width': 110, align: 'center' },
+          { label: '上播时长(s)', render: (data, h) => data.from === TaskType.IMAGE
+            ? h('edit-input', {
+              staticClass: 'border',
+              props: {
+                value: `${data.duration}`,
+                align: 'center'
+              },
+              on: {
+                edit: val => this.onSimpleEdit(data, 'duration', val)
+              }
+            })
+            : data.duration, 'min-width': 100, align: 'center' },
+          { label: '保底次数', render: (data, h) => data.from > TaskType.ORDER
+            ? h('edit-input', {
+              staticClass: 'border',
+              props: {
+                value: `${data.count}`,
+                align: 'center'
+              },
+              on: {
+                edit: val => this.onSimpleEdit(data, 'count', val)
+              }
+            })
+            : data.count, align: 'center' },
+          { label: '上播次数', render: (data, h) => data.from > TaskType.ORDER
+            ? h('edit-input', {
+              staticClass: 'border',
+              props: {
+                value: `${data.auditCount}`,
+                align: 'center'
+              },
+              on: {
+                edit: val => this.onSimpleEdit(data, 'auditCount', val)
+              }
+            })
+            : data.auditCount, align: 'center' },
+          { prop: 'tag', type: 'tag', width: 100, on: this.onAudit },
           { type: 'invoke', render: [
-            { label: '内容', on: this.onView }
+            { label: '内容', on: this.onView },
+            { label: '删除', on: this.onDel }
           ] }
         ]
       }
+    },
+    assetInfo () {
+      if (this.taskAsset) {
+        return ['', '图片', '视频'][this.taskAsset.type]
+      }
+      return '请选择需要播放的素材'
+    },
+    taskAssetName () {
+      return this.taskAsset ? this.taskAsset.name : '点击选择'
+    },
+    disableTaskDuration () {
+      return this.taskAsset ? this.taskAsset.type === AssetType.IMAGE : true
     }
   },
   methods: {
-    onChange (deviceId) {
-      this.deviceId = deviceId
+    onChange (device) {
+      this.device = device
     },
-    transform ({ orderId, startDate, startTime, endTime, day, duration, count }) {
+    transform ({ id, from, fromId, startDate, startTime, endTime, day, duration, count, auditCount, enable }) {
       return {
-        orderId,
+        id,
+        from,
+        fromId,
+        type: ['', '订单', '图片', '视频'][from],
         startDate,
-        range: `${startTime}-${endTime}`,
-        freq: `${day}天 x ${duration}秒 x ${count}次`
+        endDate: this.offsetDate(startDate, day),
+        startTime,
+        endTime,
+        range: this.getRange(startTime, endTime),
+        count,
+        duration,
+        auditCount,
+        enable,
+        tag: this.getEnableTag(enable)
       }
     },
-    onView ({ orderId }) {
-      getOrderDetail(orderId).then(({ data }) => {
-        this.$order = data
-        this.$refs.adDialog.show()
-      })
+    getRange (startTime, endTime) {
+      return startTime === '1' && endTime === '1' ? '开机期间' : endTime ? `${startTime}-${endTime}` : startTime
+    },
+    getEnableTag (enable) {
+      return {
+        type: enable ? 'success' : 'primary',
+        label: enable ? '已审核' : '待审核'
+      }
+    },
+    offsetDate (date, offset) {
+      const targetDate = new Date(date)
+      targetDate.setDate(targetDate.getDate() + offset - 1)
+      return parseTime(targetDate, '{y}-{m}-{d}')
+    },
+    onView ({ from, fromId }) {
+      if (from === TaskType.ORDER) {
+        getOrderDetail(fromId).then(({ data }) => {
+          this.$order = data
+          this.$refs.adDialog.show()
+        })
+      } else {
+        this.onViewAsset({
+          type: from - 1,
+          url: fromId
+        })
+      }
     },
     getSources () {
       const sources = this.$order.assets
@@ -132,6 +384,195 @@ export default {
     },
     onViewAsset (asset) {
       this.$refs.previewDialog.show(asset)
+    },
+    onDel ({ id }) {
+      deleteTask(id).then(() => {
+        this.$refs.table.decrease(1)
+      })
+    },
+    onAdd () {
+      const date = parseTime(new Date(), '{y}-{m}-{d}')
+      const hour = parseTime(new Date(), '{h}:{s}')
+      const time = parseTime(new Date(), '{h}:{i}:{s}')
+      this.taskDate = [date, date]
+      this.taskTime = {
+        type: 1,
+        val: [hour, hour],
+        point: time
+      }
+      this.taskDuration = 5
+      this.taskCount = 100
+      this.taskAuditCount = 100
+      this.taskAsset = null
+      this.$refs.taskDialog.show()
+    },
+    onChooseAsset () {
+      this.$refs.taskTargetDialog.show()
+    },
+    onChoosenAsset (asset) {
+      if (asset.type === AssetType.VIDEO) {
+        this.taskDuration = asset.duration
+      } else if (this.taskAsset && this.taskAsset.type === AssetType.VIDEO) {
+        this.taskDuration = 5
+      }
+      this.taskAsset = asset
+    },
+    onAddTask (done) {
+      if (!this.taskAsset) {
+        this.$message({
+          type: 'warning',
+          message: '请选择需要播放的素材'
+        })
+        return
+      }
+      const { id } = this.device
+      const [startDate, endDate] = this.taskDate
+      addTask({
+        from: this.taskAsset.type + 1,
+        fromId: this.taskAsset.keyName,
+        deviceIdList: [id],
+        startTime: '1',
+        endTime: '1',
+        startDate,
+        day: (new Date(endDate) - new Date(startDate)) / 86400000 + 1,
+        duration: this.taskDuration,
+        count: this.taskCount,
+        auditCount: this.taskAuditCount,
+        enable: false
+      }).then(() => {
+        done()
+        this.$refs.table.pageTo(1)
+      })
+    },
+    onAudit (task) {
+      const { enable, startTime, endTime, duration, auditCount } = task
+      if (!enable) {
+        const totalDuration = duration * auditCount * 1000
+        if (startTime && endTime && endTime !== '1') {
+          const remind = new Date(`2000-01-01 ${endTime}`) - new Date(`2000-01-01 ${startTime}`)
+          if (remind < totalDuration) {
+            this.$message({
+              type: 'warning',
+              message: '所选时段无法满足上播次数'
+            })
+            return
+          }
+        }
+      }
+      this.$confirm(
+        enable ? '停止使用后将不可参与自动编单' : '启用后将可参与自动编单',
+        enable ? '停用任务' : '启用任务',
+        { type: 'warning' }
+      ).then(() => {
+        this.onUpdateEnable(task)
+      })
+    },
+    onUpdateEnable (task) {
+      const { id, enable } = task
+      updateTask({
+        id,
+        enable: !enable
+      }).then(() => {
+        task.enable = !enable
+        task.tag = this.getEnableTag(!enable)
+      })
+    },
+    onSimpleEdit (task, key, { newVal, oldVal }) {
+      if (!newVal || !/^\d+$/.test(newVal) || Number(newVal) < 1) {
+        return
+      }
+      if (newVal !== oldVal) {
+        task[key] = Number(newVal)
+        updateTask({
+          id: task.id,
+          enable: false,
+          [key]: newVal
+        }).then(
+          () => {
+            if (task.enable) {
+              task.enable = false
+              task.tag = this.getEnableTag(false)
+            }
+          },
+          () => {
+            task[key] = oldVal
+          }
+        )
+      }
+    },
+    onDateEdit (task, val) {
+      const { id, enable, startDate, endDate } = task
+      if (startDate !== val[0] || endDate !== val[1]) {
+        const day = (new Date(val[0]) - new Date(val[1])) / 86400000 + 1
+        updateTask({
+          id,
+          enable: false,
+          startDate: val[0],
+          day
+        }).then(
+          () => {
+            if (enable) {
+              task.enable = false
+              task.tag = this.getEnableTag(false)
+            }
+            task.startDate = val[0]
+            task.endDate = val[1]
+            task.day = day
+          }
+        )
+      }
+    },
+    onEditTime (task) {
+      const { startTime, endTime } = task
+      this.$task = task
+      this.taskTime = {
+        type: startTime === '1' && endTime === '1' ? 1 : endTime ? 2 : 3,
+        val: [startTime === '1' ? '00:00' : startTime, !endTime || endTime === '1' ? '23:59' : endTime],
+        point: startTime === '1' || endTime ? '00:00:00' : startTime
+      }
+      this.$refs.timeDialog.show()
+    },
+    onConfirmTime (done) {
+      const { id, enable, startTime, endTime } = this.$task
+      const { type, val, point } = this.taskTime
+      let targetStartTime = ''
+      let targetEndTime = ''
+      switch (type) {
+        case 1:
+          targetStartTime = '1'
+          targetEndTime = '1'
+          break
+        case 2:
+          targetStartTime = val[0]
+          targetEndTime = val[1]
+          break
+        case 3:
+          targetStartTime = point
+          break
+        default:
+          break
+      }
+      if (startTime !== targetStartTime || endTime !== targetEndTime) {
+        updateTask({
+          id,
+          enable: false,
+          startTime: targetStartTime,
+          endTime: targetEndTime
+        }).then(
+          () => {
+            if (enable) {
+              this.$task.enable = false
+              this.$task.tag = this.getEnableTag(false)
+            }
+            this.$task.startTime = targetStartTime
+            this.$task.endTime = targetEndTime
+            this.$task.range = this.getRange(targetStartTime, targetEndTime)
+            done()
+          }
+        )
+      } else {
+        done()
+      }
     }
   }
 }
@@ -144,4 +585,64 @@ export default {
   margin-right: $spacing;
   border-right: 1px solid $gray--light;
 }
+
+.c-task {
+  position: relative;
+  height: 40px;
+  color: $blue;
+  font-size: 14px;
+  line-height: 1;
+  border-radius: $radius--mini;
+  border: 1px solid #dcdfe6;
+
+  &:hover {
+    border-color: #c0c4cc;
+  }
+
+  &__name {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+  }
+}
+
+::v-deep .o-date-picker {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: auto;
+  min-height: 40px;
+  padding: 3px 0;
+  border: 1px solid #409eff;
+  border-radius: $radius--mini;
+  background-color: $blue--light;
+
+  &.jc {
+    justify-content: center;
+  }
+
+  &:hover {
+    border-color: $gray--dark;
+  }
+
+  .el-range-input {
+    flex: 1 1 auto;
+    width: auto;
+    min-width: 0;
+    background-color: $blue--light;
+  }
+
+  .el-range-separator {
+    flex: 0 0 auto;
+    width: auto;
+    min-width: 0;
+  }
+
+  .el-input__icon {
+    display: none;
+  }
+}
 </style>