Casper Dai 3 年之前
父節點
當前提交
bdfe6d6241
共有 41 個文件被更改,包括 933 次插入140 次删除
  1. 2 2
      .env
  2. 0 1
      .env.xuzhou
  3. 0 1
      feature.js
  4. 4 21
      src/components/service/Schedule/ScheduleSwiper/index.vue
  5. 1 1
      src/components/service/Schedule/components/ScheduleWrapper.vue
  6. 13 2
      src/constant.js
  7. 21 0
      src/icons/svg/ad.svg
  8. 26 6
      src/router/index.js
  9. 8 1
      src/utils/cache/event.js
  10. 1 1
      src/utils/cache/screenshot.js
  11. 29 11
      src/utils/event.js
  12. 1 1
      src/utils/index.js
  13. 8 15
      src/utils/mqtt.js
  14. 70 0
      src/views/ad/api.js
  15. 148 0
      src/views/ad/review-asset/index.vue
  16. 191 0
      src/views/ad/review-order/index.vue
  17. 1 3
      src/views/dashboard/Dashboard.vue
  18. 6 3
      src/views/dashboard/components/Device.vue
  19. 4 1
      src/views/dashboard/v0/DeviceCalender.vue
  20. 6 3
      src/views/dashboard/v0/DeviceInfo.vue
  21. 4 1
      src/views/dashboard/v1/DeviceCalender.vue
  22. 1 1
      src/views/device/detail/components/DeviceInfo.vue
  23. 1 1
      src/views/device/detail/components/DeviceInvoke/DeviceNetwork/index.vue
  24. 1 1
      src/views/device/detail/components/DeviceInvoke/DeviceReboot.vue
  25. 4 2
      src/views/device/detail/components/DeviceInvoke/index.vue
  26. 1 1
      src/views/device/detail/components/DeviceInvoke/mixins/base.js
  27. 1 1
      src/views/device/detail/dashboard/DeviceInfo.vue
  28. 25 17
      src/views/device/detail/dashboard/Timeline.vue
  29. 1 1
      src/views/device/detail/index.vue
  30. 20 11
      src/views/device/timeline/index.vue
  31. 19 0
      src/views/realm/device/settings/api.js
  32. 127 0
      src/views/realm/device/settings/components/AdConfigDialog.vue
  33. 131 14
      src/views/realm/device/settings/components/AttributeConfigDialog.vue
  34. 13 1
      src/views/realm/device/settings/components/DeviceNormalConfig.vue
  35. 1 1
      src/views/realm/device/settings/components/DeviceShadow.vue
  36. 3 2
      src/views/review/components/ReviewPublish.vue
  37. 3 2
      src/views/review/history/index.vue
  38. 3 2
      src/views/review/workflow/detail/components/ReviewPublish.vue
  39. 3 2
      src/views/review/workflow/index.vue
  40. 3 2
      src/views/review/workflow/mine/index.vue
  41. 28 4
      src/views/schedule/deploy/index.vue

+ 2 - 2
.env

@@ -6,8 +6,6 @@ LOGGER = 'disabled'
 
 # 未开发的功能组件
 __PLACEHOLDER__ = 'disabled'
-# 传感器
-__SENSOR__ = 'disabled'
 # 设备仪表盘
 __DEVICE_DASHBARD__ = 'disabled'
 # 设备接管
@@ -56,3 +54,5 @@ VUE_APP_SALT = '42857cfddb33f3fddb27fff9773683f3'
 # gaode
 VUE_APP_GAODE_MAP_KEY = '9c499e7000d066c05de9af8556a890f7'
 VUE_APP_GAODE_MAP_JSCODE = 'e7b3c29a5112657edcc688a3c589bd15'
+
+VUE_APP_AD_ORDER_QR = 'https://msr.inspur.com'

+ 0 - 1
.env.xuzhou

@@ -2,7 +2,6 @@ NODE_ENV = 'production'
 
 ENV = 'xuzhou'
 
-__SENSOR__ = 'enabled'
 __DEVICE_DASHBARD__ = 'enabled'
 
 VUE_APP_SENSOR_ELK = 'https://msr.rondochina.com:15601/app/kibana/app/dashboards#/view/5ccd19f0-9f9d-11ec-80c5-8f1eb0dbee8e?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-24h%2Fh%2Cto%3Anow))&hide-filter-bar=true'

+ 0 - 1
feature.js

@@ -23,7 +23,6 @@ module.exports = {
     __DEV__: !isProd,
     __STAGING__: !isProd || isStaging,
     ...createFeature('__PLACEHOLDER__'),
-    ...createFeature('__SENSOR__'),
     ...createFeature('__DEVICE_DASHBARD__'),
     ...createFeature('__TAKEOVER__')
   }

+ 4 - 21
src/components/service/Schedule/ScheduleSwiper/index.vue

@@ -55,7 +55,7 @@
                 {{ program.name }}
               </div>
             </div>
-            <span class="l-flex__none u-color--info">持续时间(秒):</span>
+            <span class="l-flex__none u-color--info">时长(秒):</span>
             <el-input-number
               v-model="program.duration"
               class="l-flex__none c-schedule-swiper__seconds"
@@ -88,7 +88,7 @@
           <div class="c-schedule-swiper__program u-ellipsis">
             {{ program.name }}
           </div>
-          <span class="l-flex__none">持续{{ program.duration }}</span>
+          <span class="l-flex__none">{{ program.duration }}</span>
         </div>
       </div>
     </template>
@@ -99,6 +99,7 @@
 <script>
 import { getPrograms } from '@/api/program'
 import { State } from '@/constant'
+import { parseDuration } from '@/utils'
 import scheduleMixin from '../mixins/schedule'
 import Draggable from 'vuedraggable'
 import ScheduleWrapper from '../components/ScheduleWrapper'
@@ -186,25 +187,7 @@ export default {
       if (this.editable) {
         return duration
       }
-      const seconds = duration % 60
-      let minutes = duration / 60 | 0
-      let s = ''
-      if (minutes > 60) {
-        let hours = minutes / 60 | 0
-        s += `${hours / 24 | 0}天`
-        hours %= 24
-        if (hours) {
-          s += `${hours}小时`
-        }
-        minutes %= 60
-      }
-      if (minutes) {
-        s += `${minutes}分`
-      }
-      if (seconds) {
-        s += `${seconds}秒`
-      }
-      return s
+      return parseDuration(duration)
     },
     init () {
       this.record = {

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

@@ -84,7 +84,7 @@ export default {
   &__header {
     color: $black;
     font-size: 24px;
-    line-height: 1;
+    line-height: 40px;
     border-bottom: 1px solid $border;
   }
 }

+ 13 - 2
src/constant.js

@@ -50,7 +50,17 @@ export const PublishType = {
 export const EventPriority = {
   DEFAULT: 1,
   NORMAL: 2,
-  INSERTED: 3
+  INSERTED: 3,
+  AD: 4,
+  EMERGENT: 99
+}
+
+export const EventPriorityDescription = {
+  1: '默认播放',
+  2: '排期',
+  3: '插播',
+  4: '广告',
+  99: '紧急'
 }
 
 export const EventFreq = {
@@ -60,7 +70,8 @@ export const EventFreq = {
 
 export const EventTarget = {
   PROGRAM: 1,
-  RECUR: 2
+  RECUR: 2,
+  AD: 3
 }
 
 export const ThirdPartyDevice = {

+ 21 - 0
src/icons/svg/ad.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 34 34" style="enable-background:new 0 0 34 34;" xml:space="preserve">
+<style type="text/css">
+	.st0{display:none;}
+</style>
+<path class="st0" d="M26,31.1H8c-2.8,0-5-2.2-5-5V14.2C3,12.5,3.8,11,5.2,10l9-6.2c1.7-1.2,4-1.2,5.7,0l9,6.2
+	c1.4,0.9,2.2,2.5,2.2,4.1V26C31,28.8,28.8,31.1,26,31.1z M17,4.9c-0.6,0-1.2,0.2-1.7,0.5l-9,6.2C5.5,12.3,5,13.2,5,14.2v11.9
+	c0,1.7,1.3,3,3,3h18c1.7,0,3-1.3,3-3V14.2c0-1-0.5-1.9-1.3-2.5l-9-6.2C18.2,5.1,17.6,4.9,17,4.9z"/>
+<path class="st0" d="M21,27.1h-8v-6c0-2.2,1.8-4,4-4s4,1.8,4,4V27.1z M15,25.1h4v-4c0-1.1-0.9-2-2-2s-2,0.9-2,2V25.1z"/>
+<path d="M26,23.1H8c-2.8,0-5-2.2-5-5v-10c0-2.8,2.2-5,5-5h18c2.8,0,5,2.2,5,5v10C31,20.9,28.8,23.1,26,23.1z M8,5.1
+	c-1.7,0-3,1.3-3,3v10c0,1.7,1.3,3,3,3h18c1.7,0,3-1.3,3-3v-10c0-1.7-1.3-3-3-3H8z"/>
+<path d="M23,27.1H11c-0.6,0-1-0.4-1-1v0c0-0.6,0.4-1,1-1h12c0.6,0,1,0.4,1,1v0C24,26.7,23.6,27.1,23,27.1z"/>
+<path d="M19,31.1h-4c-0.6,0-1-0.4-1-1v0c0-0.6,0.4-1,1-1h4c0.6,0,1,0.4,1,1v0C20,30.7,19.6,31.1,19,31.1z"/>
+<path d="M15.9,16.7C15.9,16.7,15.9,16.7,15.9,16.7l-3-8h0c-0.1-0.4-0.5-0.6-0.9-0.6s-0.8,0.3-0.9,0.6h0l-3,8c0,0,0,0,0,0
+	C8,16.9,8,17,8,17.1c0,0.5,0.5,1,1,1c0.4,0,0.8-0.3,0.9-0.7h0l0.5-1.3h3.1l0.5,1.3h0c0.1,0.4,0.5,0.7,0.9,0.7c0.5,0,1-0.5,1-1
+	C16,17,16,16.9,15.9,16.7z M11.2,14.1L12,12l0.8,2.1H11.2z"/>
+<path d="M21,8.1h-2c-0.5,0-1,0.5-1,1v8c0,0.5,0.5,1,1,1h2c2.8,0,5-2.2,5-5S23.8,8.1,21,8.1z M21,16.1h-1v-6h1c1.7,0,3,1.3,3,3
+	C24,14.7,22.7,16.1,21,16.1z"/>
+</svg>

+ 26 - 6
src/router/index.js

@@ -274,18 +274,18 @@ export const asyncRoutes = [
     access: [Access.MANAGE_TENANTS, Access.MANAGE_TENANT],
     meta: { title: '平台管理', icon: 'pm' },
     children: [
-      {
-        name: 'org',
-        path: 'org',
-        component: () => import('@/views/realm/tenant/index'),
-        meta: { title: '组织管理' }
-      },
       {
         name: 'settings',
         path: 'settings',
         component: () => import('@/views/realm/settings/index'),
         meta: { title: '功能管理' }
       },
+      {
+        name: 'org',
+        path: 'org',
+        component: () => import('@/views/realm/tenant/index'),
+        meta: { title: '组织管理' }
+      },
       {
         name: 'account',
         path: 'account',
@@ -340,6 +340,26 @@ export const asyncRoutes = [
       }
     ]
   },
+  {
+    path: '/ad',
+    component: Layout,
+    meta: { title: '广告', icon: 'ad' },
+    access: [Access.MANAGE_TENANTS],
+    children: [
+      {
+        name: 'ad-asset-review',
+        path: 'asset/review',
+        component: () => import('@/views/ad/review-asset/index'),
+        meta: { title: '素材审核' }
+      },
+      {
+        name: 'ad-order-review',
+        path: 'order/review',
+        component: () => import('@/views/ad/review-order/index'),
+        meta: { title: '订单审核' }
+      }
+    ]
+  },
   {
     path: '/bm',
     component: Layout,

+ 8 - 1
src/utils/cache/event.js

@@ -42,5 +42,12 @@ function getPromise (type, id) {
 }
 
 export function getImage (type, id) {
-  return getPromise(type, id).then(type === EventTarget.PROGRAM ? getProgramImage : getScheduleImage)
+  switch (type) {
+    case EventTarget.PROGRAM:
+      return getPromise(type, id).then(getProgramImage)
+    case EventTarget.RECUR:
+      return getPromise(type, id).then(getScheduleImage)
+    default:
+      return Promise.resolve(null)
+  }
 }

+ 1 - 1
src/utils/cache/screenshot.js

@@ -51,7 +51,7 @@ export function screenshot (deviceId, silence) {
     inst.waiting = true
     inst.timestamp = Date.now()
     inst.base64 = null
-    publish(`${inst.productId}/${inst.deviceId}/screenshot/ask`, JSON.stringify({ timestamp: inst.timestamp })).then(
+    publish(`${inst.productId}/${inst.deviceId}/screenshot/ask`, JSON.stringify({ timestamp: `${inst.timestamp}` })).then(
       () => {
         startTimer(inst)
       },

+ 29 - 11
src/utils/event.js

@@ -1,4 +1,7 @@
-import { EventFreq } from '@/constant'
+import {
+  EventFreq,
+  EventTarget
+} from '@/constant'
 
 export const ONE_DAY = 86400000
 
@@ -114,8 +117,8 @@ export function isIn (target, start, end) {
 }
 
 export function toDateStr (date, offset = 0) {
+  date = toDate(date)
   if (offset !== 0) {
-    date = toDate(date)
     date.setDate(date.getDate() + offset)
   }
   return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
@@ -134,15 +137,15 @@ export function getStartDate (event, date) {
     case EventFreq.WEEKLY:
       return getWeeklyStartDate(event, date)
     default:
-      return new Date(event.start)
+      return toDate(event.start)
   }
 }
 
 function getWeeklyStartDate (event, date) {
   const time = toTimeStr(date)
   return time < event.startTime
-    ? new Date(`${toDateStr(date, -1)} ${event.startTime}`)
-    : new Date(`${toDateStr(date)} ${event.startTime}`)
+    ? toDate(`${toDateStr(date, -1)} ${event.startTime}`)
+    : toDate(`${toDateStr(date)} ${event.startTime}`)
 }
 
 export function getFinishDate (event, date) {
@@ -150,15 +153,15 @@ export function getFinishDate (event, date) {
     case EventFreq.WEEKLY:
       return getWeeklyFinishDate(event, date)
     default:
-      return event.until && new Date(event.until)
+      return event.until && toDate(event.until)
   }
 }
 
 function getWeeklyFinishDate (event, date) {
   const time = toTimeStr(date)
   return time < event.startTime
-    ? new Date(`${toDateStr(date)} ${event.endTime}`)
-    : new Date(`${toDateStr(date, isOverDay(event) ? 1 : 0)} ${correctEndTime(event.endTime)}`)
+    ? toDate(`${toDateStr(date)} ${event.endTime}`)
+    : toDate(`${toDateStr(date, isOverDay(event) ? 1 : 0)} ${correctEndTime(event.endTime)}`)
 }
 
 export function getConflict (a, b) {
@@ -261,18 +264,33 @@ export function correctEndTime (endTime) {
 }
 
 export function getEventDateDescription (event) {
-  const { start, until } = event
+  const { freq, start, until, target } = event
+  if (freq === EventFreq.WEEKLY) {
+    if (target.type === EventTarget.AD) {
+      return `自${toDateStr(start)}开始 持续${Math.ceil((toDate(until) - toDate(start)) / ONE_DAY)}天`
+    }
+    return until ? `${toDateStr(start)} - ${toDateStr(until, -1)}` : `自${toDateStr(start)}开始`
+  }
   return until ? `${start} - ${until}` : `自${start}开始`
 }
 
 export function getEventExtraDescription (event) {
   const { freq, byDay, start, until, startTime, endTime, target } = event
+  if (target.type === EventTarget.AD) {
+    switch (freq) {
+      case EventFreq.WEEKLY:
+        return `每日播放${(toDate(`1970/01/01 ${endTime}`) - toDate(`1970/01/01 ${startTime}`)) / target.duration / 1000}次`
+      default:
+        return `播放${(toDate(until) - toDate(start)) / target.duration / 1000}次`
+    }
+  }
   switch (freq) {
     case EventFreq.WEEKLY:
-      return `每周${byDay.split(',').map(val => ['日', '一', '二', '三', '四', '五', '六'][val]).join('、')} ${startTime} - ${endTime}`
+      return `每周${byDay.split(',').sort().map(val => ['日', '一', '二', '三', '四', '五', '六'][val])
+        .join('、')} ${startTime} - ${endTime}`
     default:
       return target?.duration
-        ? `播放${(new Date(until) - new Date(start)) / target.duration / 1000}次`
+        ? `播放${(toDate(until) - toDate(start)) / target.duration / 1000}次`
         : ''
   }
 }

+ 1 - 1
src/utils/index.js

@@ -57,7 +57,7 @@ export function createListOptions (params) {
   }
 }
 
-const units = ['bit', 'KB', 'M', 'G']
+const units = ['B', 'KB', 'M', 'G']
 export function parseByte (byte) {
   const edge = 1024
   let i = 0

+ 8 - 15
src/utils/mqtt.js

@@ -81,8 +81,6 @@ function start () {
 
   client.on('error', err => {
     console.log('MQTT connection error: ', err)
-    client.end(true)
-    mqttClient = null
   })
 
   client.on('connect', () => {
@@ -90,29 +88,24 @@ function start () {
     changeState('connect')
   })
 
-  client.on('reconnect', () => {
-    console.log('MQTT reconnecting...')
-    changeState('reconnect')
-  })
-
-  client.on('close', () => {
-    console.log('MQTT closed')
-    changeState('close')
-  })
-
   client.on('disconnect', () => {
     console.log('MQTT disconnect')
     changeState('disconnect')
   })
 
+  client.on('reconnect', () => {
+    console.log('MQTT reconnecting...')
+    changeState('reconnect')
+  })
+
   client.on('offline', () => {
     console.log('MQTT offline')
     changeState('offline')
   })
 
-  client.on('end', () => {
-    console.log('MQTT end')
-    mqttClient = null
+  client.on('close', () => {
+    console.log('MQTT closed')
+    changeState('close')
   })
 
   client.on('message', (topic, payload) => {

+ 70 - 0
src/views/ad/api.js

@@ -0,0 +1,70 @@
+import request, { tenantRequest } from '@/utils/request'
+import {
+  resolve,
+  reject,
+  addTenant
+} from '@/api/base'
+
+export function getOrders (query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/ad/tenant/order/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    }),
+    ...options
+  })
+}
+
+export function resolveOrder ({ id }) {
+  return resolve({
+    url: `/ad/tenant/order/resolve`,
+    method: 'POST',
+    data: { orderId: id }
+  })
+}
+
+export function rejectOrder ({ id }, reason) {
+  return reject({
+    url: `/ad/tenant/order/reject`,
+    method: 'POST',
+    data: {
+      orderId: id,
+      reason
+    }
+  })
+}
+
+export function getAssets (query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/ad/tenant/asset/list',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    },
+    ...options
+  })
+}
+
+export function resolveAsset ({ keyName }) {
+  return resolve({
+    url: `/ad/tenant/asset/resolve`,
+    method: 'POST',
+    data: { keyNames: [keyName] }
+  })
+}
+
+export function rejectAsset ({ keyName }, reason) {
+  return reject({
+    url: `/ad/tenant/asset/reject`,
+    method: 'POST',
+    data: {
+      keyName,
+      reason
+    }
+  })
+}

+ 148 - 0
src/views/ad/review-asset/index.vue

@@ -0,0 +1,148 @@
+import { parseDuration } from '@/utils';
+<template>
+  <wrapper
+    fill
+    margin
+    padding
+    background
+  >
+    <schema-table
+      ref="table"
+      :schema="schema"
+    />
+    <confirm-dialog
+      ref="rejectDialog"
+      title="拒绝原因"
+      @confirm="onConfirmReject"
+    >
+      <div class="c-grid-form u-align-self--center">
+        <span class="c-grid-form__label">拒绝原因</span>
+        <el-select
+          v-model="review.type"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="option in reviewOptions"
+            :key="option.label"
+            :label="option.label"
+            :value="option.value"
+          />
+        </el-select>
+        <template v-if="review.type === 'reject'">
+          <span class="c-grid-form__label required">描述</span>
+          <el-input
+            v-model.trim="review.reason"
+            type="textarea"
+            placeholder="请填写拒绝原因"
+            maxlength="50"
+            :rows="3"
+            resize="none"
+            show-word-limit
+          />
+        </template>
+      </div>
+    </confirm-dialog>
+    <preview-dialog ref="previewDialog" />
+  </wrapper>
+</template>
+
+<script>
+import {
+  State,
+  AssetType
+} from '@/constant'
+import {
+  parseByte,
+  parseDuration
+} from '@/utils'
+import {
+  getAssets,
+  resolveAsset,
+  rejectAsset
+} from '../api'
+
+export default {
+  name: 'AdAssetReview',
+  data () {
+    return {
+      schema: {
+        condition: { status: State.READY },
+        list: getAssets,
+        transform: this.transform,
+        cols: [
+          { type: 'refresh' },
+          { prop: 'file', label: '缩略图', type: 'asset', on: this.onView },
+          { prop: 'fileType', label: '文件类型', align: 'center' },
+          { prop: 'size', label: '文件大小' },
+          { prop: 'ratio', label: '分辨率' },
+          { prop: 'remark', label: '其他' },
+          { prop: 'createTime', label: '提交时间' },
+          { type: 'invoke', width: 160, render: [
+            { label: '查看', on: this.onViewAsset },
+            { label: '通过', on: this.onResolve },
+            { label: '拒绝', on: this.onReject }
+          ] }
+        ]
+      },
+      reviewOptions: [
+        { value: 'reject', label: '自定义' },
+        { value: '内容不合规' }
+      ],
+      review: {
+        type: '',
+        reason: ''
+      }
+    }
+  },
+  methods: {
+    transform ({ keyName, type, size, duration, width, height, thumb, createTime }) {
+      return {
+        keyName,
+        file: {
+          type,
+          url: keyName,
+          thumbnail: type === AssetType.IMAGE ? keyName : thumb || null
+        },
+        fileType: type === AssetType.IMAGE ? '图片' : '视频',
+        ratio: width && height ? `${width}x${height}` : '-',
+        size: parseByte(size),
+        remark: duration ? parseDuration(duration) : '-',
+        createTime
+      }
+    },
+    onResolve (asset) {
+      resolveAsset(asset).then(() => {
+        this.$refs.table.decrease(1)
+      })
+    },
+    onReject (asset) {
+      this.$asset = asset
+      this.review = {
+        type: 'reject',
+        reason: ''
+      }
+      this.$refs.rejectDialog.show()
+    },
+    onConfirmReject (done) {
+      const reason = this.review.type === 'reject' ? this.review.reason : this.review.type
+      if (!reason) {
+        this.$message({
+          type: 'warning',
+          message: '请选择或填写驳回原因'
+        })
+        return
+      }
+      rejectAsset(this.$asset, reason).then(() => {
+        done()
+        this.$refs.table.decrease(1)
+      })
+    },
+    onViewAsset (asset) {
+      this.onView(asset.file)
+    },
+    onView (file) {
+      this.$refs.previewDialog.show(file)
+    }
+  }
+}
+</script>

+ 191 - 0
src/views/ad/review-order/index.vue

@@ -0,0 +1,191 @@
+<template>
+  <wrapper
+    fill
+    margin
+    padding
+    background
+  >
+    <schema-table
+      ref="table"
+      :schema="schema"
+    />
+    <confirm-dialog
+      ref="rejectDialog"
+      title="拒绝原因"
+      @confirm="onConfirmReject"
+    >
+      <div class="c-grid-form u-align-self--center">
+        <span class="c-grid-form__label">拒绝原因</span>
+        <el-select
+          v-model="review.type"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="option in reviewOptions"
+            :key="option.label"
+            :label="option.label"
+            :value="option.value"
+          />
+        </el-select>
+        <template v-if="review.type === 'reject'">
+          <span class="c-grid-form__label required">描述</span>
+          <el-input
+            v-model.trim="review.reason"
+            type="textarea"
+            placeholder="请填写拒绝原因"
+            maxlength="50"
+            :rows="3"
+            resize="none"
+            show-word-limit
+          />
+        </template>
+      </div>
+    </confirm-dialog>
+    <table-dialog
+      ref="adDialog"
+      title="广告内容"
+      :schema="adSchema"
+    />
+    <preview-dialog ref="previewDialog" />
+  </wrapper>
+</template>
+
+<script>
+import {
+  State,
+  AssetType
+} from '@/constant'
+import {
+  parseByte,
+  parseDuration
+} from '@/utils'
+import {
+  getOrders,
+  resolveOrder,
+  rejectOrder
+} from '../api'
+
+export default {
+  name: 'AdOrderReview',
+  data () {
+    return {
+      schema: {
+        condition: { status: State.READY },
+        list: getOrders,
+        transform: this.transform,
+        cols: [
+          { type: 'refresh' },
+          { prop: 'device', label: '设备' },
+          { prop: 'address', label: '位置' },
+          { prop: 'date', label: '起始日期', 'min-width': 100 },
+          { prop: 'day', label: '持续(天)', 'min-width': 90, align: 'right' },
+          { prop: 'range', label: '时段', 'min-width': 100, align: 'right' },
+          { prop: 'count', label: '每日(次)', 'min-width': 90, align: 'right' },
+          { prop: 'price', label: '价格(元)', 'min-width': 100, align: 'right' },
+          { type: 'invoke', width: 160, render: [
+            { label: '内容', on: this.onView },
+            { label: '通过', on: this.onResolve },
+            { label: '拒绝', on: this.onReject }
+          ] }
+        ]
+      },
+      reviewOptions: [
+        { value: 'reject', label: '自定义' },
+        { value: '播放时段已被占用' },
+        { value: '内容不合规' }
+      ],
+      review: {
+        type: '',
+        reason: ''
+      },
+      adSchema: {
+        list: this.getSources,
+        cols: [
+          { prop: 'file', type: 'asset', on: this.onViewAsset },
+          { prop: 'filename', label: '', 'min-width': 100 },
+          { prop: 'duration', label: '播放时长', align: 'right' },
+          { prop: 'size', label: '大小', align: 'right' },
+          { type: 'invoke', render: [
+            { label: '查看', on: this.onViewSource }
+          ] }
+        ]
+      }
+    }
+  },
+  methods: {
+    transform ({ order, devices, assets }) {
+      const slot = order.scheduleType === 1 ? order.slot : {}
+      return {
+        id: order.id,
+        device: devices[0].name,
+        address: devices[0].address,
+        date: slot.startDate,
+        day: slot.day,
+        range: slot.range,
+        count: slot.count,
+        duration: parseDuration(order.duration),
+        price: (order.price / 100).toFixed(2),
+        status: order.status,
+        expand: order.expand,
+        assets
+      }
+    },
+    onView (order) {
+      this.$order = order
+      this.$refs.adDialog.show()
+    },
+    onResolve (order) {
+      resolveOrder(order).then(() => {
+        this.$refs.table.decrease(1)
+      })
+    },
+    onReject (order) {
+      this.$order = order
+      this.review = {
+        type: 'reject',
+        reason: ''
+      }
+      this.$refs.rejectDialog.show()
+    },
+    onConfirmReject (done) {
+      const reason = this.review.type === 'reject' ? this.review.reason : this.review.type
+      if (!reason) {
+        this.$message({
+          type: 'warning',
+          message: '请选择或填写驳回原因'
+        })
+        return
+      }
+      rejectOrder(this.$order, reason).then(() => {
+        done()
+        this.$refs.table.decrease(1)
+      })
+    },
+    getSources () {
+      const sources = this.$order.assets
+      return Promise.resolve({
+        data: sources.map(this.transformSource),
+        totalCount: sources.length
+      })
+    },
+    transformSource ({ filename, type, duration, size, keyName }) {
+      return {
+        file: {
+          type,
+          url: keyName,
+          thumbnail: type === AssetType.IMAGE ? keyName : ''
+        },
+        filename,
+        duration: parseDuration(duration),
+        size: parseByte(size)
+      }
+    },
+    onViewSource ({ file }) {
+      this.onViewAsset(file)
+    },
+    onViewAsset (asset) {
+      this.$refs.previewDialog.show(asset)
+    }
+  }
+}
+</script>

+ 1 - 3
src/views/dashboard/Dashboard.vue

@@ -149,7 +149,7 @@ export default {
   },
   methods: {
     onMessage (topic) {
-      if (this.deviceOptions.loaded) {
+      if (!this.deviceOptions.loaded) {
         return
       }
       const result = /^\d+\/(\d+)\/(online|offline)$/.exec(topic)
@@ -164,8 +164,6 @@ export default {
         if (status === onlineStatus) {
           return
         }
-      } else if (status === 'offline') {
-        return
       }
       this.refreshDevices()
     },

+ 6 - 3
src/views/dashboard/components/Device.vue

@@ -41,7 +41,7 @@
         <template v-else-if="current">
           <auto-text
             class="l-flex__none o-device__current"
-            :text="current.target.name"
+            :text="current.name"
           />
           <div class="l-flex__none l-flex--row has-top-padding">
             <span class="o-device__hms">
@@ -86,6 +86,7 @@
 
 <script>
 import { getTimeline } from '@/api/device'
+import { EventPriorityDescription } from '@/constant'
 import {
   listen,
   unlisten
@@ -142,7 +143,7 @@ export default {
       return this.isOnline && this.shot ? { backgroundImage: `url("${this.shot}")` } : null
     },
     nextInfo () {
-      return this.next ? `下一场:${this.next.startDate} ${this.next.startTime} ${this.next.target.name}` : ''
+      return this.next ? `下一场:${this.next.startDate} ${this.next.startTime} ${this.next.name}` : ''
     }
   },
   created () {
@@ -278,8 +279,10 @@ export default {
       }
     },
     getEventInfo (event, startDate, endDate) {
+      const target = event.target
       return {
-        target: event.target,
+        target,
+        name: target.name || EventPriorityDescription[event.priority],
         startDate: parseTime(startDate, '{y}.{m}.{d}'),
         startTime: parseTime(startDate, '{h}:{i}:{s}'),
         endDate: endDate ? parseTime(endDate, '{y}.{m}.{d}') : '',

+ 4 - 1
src/views/dashboard/v0/DeviceCalender.vue

@@ -43,6 +43,7 @@
 
 <script>
 import { getTimelines } from './api'
+import { EventPriorityDescription } from '@/constant'
 import {
   isHit,
   toDate,
@@ -80,7 +81,9 @@ export default {
           id, name,
           updateTime: data ? data.updateTime : '-',
           programName: map
-            ? data?.event?.target.name || '暂无节目'
+            ? data?.event
+              ? data.event.target.name || EventPriorityDescription[data.event.priority]
+              : '暂无节目'
             : '查询中...',
           programDesc: map && data?.event ? getEventDescription(data.event) : '-'
         }

+ 6 - 3
src/views/dashboard/v0/DeviceInfo.vue

@@ -156,10 +156,13 @@ export default {
       if (this.rebooting) {
         return
       }
-      this.$confirm(`立即重启?`, { type: 'warning' }).then(() => {
+      this.$confirm(
+        `立即重启?`,
+        { type: 'warning' }
+      ).then(() => {
         publish(
           `${this.device.productId}/${this.deviceId}/restart/ask`,
-          JSON.stringify({ timestamp: Date.now() })
+          JSON.stringify({ timestamp: `${Date.now()}` })
         ).then(
           () => {
             this.rebooting = true
@@ -181,7 +184,7 @@ export default {
       publish(
         `${this.device.productId}/${this.deviceId}/function/invoke`,
         JSON.stringify({
-          timestamp: Date.now(),
+          timestamp: `${Date.now()}`,
           function: invoke,
           inputs
         }),

+ 4 - 1
src/views/dashboard/v1/DeviceCalender.vue

@@ -45,6 +45,7 @@
 
 <script>
 import { getTimelines } from './api'
+import { EventPriorityDescription } from '@/constant'
 import {
   isHit,
   toDate,
@@ -83,7 +84,9 @@ export default {
           id, name,
           updateTime: data ? data.updateTime : '-',
           programName: map
-            ? data?.event?.target.name || '暂无节目'
+            ? data?.event
+              ? data.event.target.name || EventPriorityDescription[data.event.priority]
+              : '暂无节目'
             : '查询中...',
           programDesc: map && data?.event ? getEventDescription(data.event) : '-',
           status: onlineStatus === 1 && map && data?.event ? '在播' : ''

+ 1 - 1
src/views/device/detail/components/DeviceInfo.vue

@@ -94,7 +94,7 @@
         <span />
         <a
           class="o-link grid"
-          href="https://lbs.amap.com/tools/picker"
+          href="https://lbs.qq.com/getPoint/"
           target="_blank"
         />
       </div>

+ 1 - 1
src/views/device/detail/components/DeviceInvoke/DeviceNetwork/index.vue

@@ -136,7 +136,7 @@ export default {
         `${this.device.productId}/${this.deviceId}/function/invoke`,
         JSON.stringify({
           messageId: this.$messageId,
-          timestamp: Date.now(),
+          timestamp: `${Date.now()}`,
           'function': 'network',
           inputs: []
         }),

+ 1 - 1
src/views/device/detail/components/DeviceInvoke/DeviceReboot.vue

@@ -41,7 +41,7 @@ export default {
         return
       }
       this.$confirm(
-        `重启 ${this.device.name} ?`,
+        `立即重启?`,
         { type: 'warning' }
       ).then(() => {
         send('/restart/ask').then(

+ 4 - 2
src/views/device/detail/components/DeviceInvoke/index.vue

@@ -32,11 +32,13 @@ export default {
           h('DeviceSwitch', { props: this.$attrs }),
           h('ScreenSwitch', { props: this.$attrs }),
           h('ScreenSwitchPLC', { props: this.$attrs }),
-          h('DeviceReboot', { props: this.$attrs }),
           h('ScreenLight', { props: this.$attrs }),
-          h('ScreenVolume', { props: this.$attrs })
+          h('ScreenVolume', { props: this.$attrs }),
+          h('DeviceReboot', { props: this.$attrs })
         ]
         : [
+          h('DeviceNetwork', { props: this.$attrs }),
+          h('ScreenVolume', { props: this.$attrs }),
           h('DeviceReboot', { props: this.$attrs })
         ]
     )

+ 1 - 1
src/views/device/detail/components/DeviceInvoke/mixins/base.js

@@ -22,7 +22,7 @@ export default {
       publish(
         `${this.device.productId}/${this.deviceId}/function/invoke`,
         JSON.stringify({
-          timestamp: Date.now(),
+          timestamp: `${Date.now()}`,
           'function': invoke,
           inputs
         }),

+ 1 - 1
src/views/device/detail/dashboard/DeviceInfo.vue

@@ -128,7 +128,7 @@ export default {
       publish(
         `${this.device.productId}/${this.deviceId}/function/invoke`,
         JSON.stringify({
-          timestamp: Date.now(),
+          timestamp: `${Date.now()}`,
           function: invoke,
           inputs
         }),

+ 25 - 17
src/views/device/detail/dashboard/Timeline.vue

@@ -86,7 +86,7 @@
                       />
                       <auto-text
                         class="c-event-program__name"
-                        :text="program.event.target.name"
+                        :text="program.event.name"
                         :tag="program.style.width"
                       />
                     </div>
@@ -118,6 +118,7 @@
 
 <script>
 import { getTimeline } from '@/api/device'
+import { EventPriorityDescription } from '@/constant'
 import {
   toDate,
   toDateStr,
@@ -188,12 +189,13 @@ export default {
     transformEvent (event) {
       return {
         ...event,
+        name: event.target.name || EventPriorityDescription[event.priority],
         time: getEventDescription(event),
         startDateTime: toDate(event.start),
         endDateTime: toDate(event.until),
         style: null,
         img () {
-          EventCache.getImage(this.target.type, this.target.id).then(img => {
+          this.target.id && EventCache.getImage(this.target.type, this.target.id).then(img => {
             if (img) {
               this.style = { backgroundImage: `url("${img}")` }
             }
@@ -334,7 +336,7 @@ export default {
   }
 
   &__programs {
-    height: 40px;
+    height: 36px;
   }
 
   &__arrow {
@@ -406,7 +408,7 @@ export default {
     }
 
     .c-timeline__programs {
-      height: 96px;
+      height: 90px;
     }
 
     .c-event-program__img {
@@ -442,7 +444,7 @@ export default {
   }
 
   &__name {
-    margin-top: 10px;
+    margin-top: 6px;
   }
 }
 
@@ -451,6 +453,23 @@ export default {
   line-height: 1;
 }
 
+.priority1 {
+  color: #8e929c;
+  background-color: #edf0f6;
+}
+
+.priority2 {
+  color: #7642fd;
+  background-color: #eae2fe;
+}
+
+.priority3,
+.priority4,
+.priority99 {
+  color: #ff2222;
+  background-color: #ffecec;
+}
+
 .priority {
   font-size: 12px;
 
@@ -462,18 +481,7 @@ export default {
     width: 12px;
     height: 12px;
     margin-right: 4px;
-  }
-
-  &1 {
-    background-color: #79829d;
-  }
-
-  &2 {
-    background-color: #7e4de2;
-  }
-
-  &3 {
-    background-color: #ff2222;
+    background-color: currentColor;
   }
 }
 

+ 1 - 1
src/views/device/detail/index.vue

@@ -103,7 +103,7 @@ export default {
         { key: 'DeviceRuntime', label: '运行状态' },
         { key: 'DeviceAlarm', label: '设备告警' },
         this.accessSet.has(Access.MANAGE_DEVICE) ? { key: 'DeviceInvoke', label: '设备操控' } : null,
-        __SENSOR__ ? { key: 'Sensors', label: '传感器' } : null,
+        { key: 'Sensors', label: '传感器' },
         { key: 'DeviceExternal', label: '全链路监测' },
         __TAKEOVER__ && this.accessSet.has(Access.MANAGE_GROUP) ? { key: 'DeviceTakeOver', label: '接管' } : null
       ].filter(Boolean)

+ 20 - 11
src/views/device/timeline/index.vue

@@ -137,7 +137,7 @@
                       />
                       <auto-text
                         class="c-event-program__name"
-                        :text="program.event.target.name"
+                        :text="program.event.name"
                         :tag="program.style.width"
                       />
                     </div>
@@ -185,7 +185,10 @@ import {
   getDevices,
   getTimeline
 } from '@/api/device'
-import { EventTarget } from '@/constant'
+import {
+  EventTarget,
+  EventPriorityDescription
+} from '@/constant'
 import {
   toDate,
   toDateStr,
@@ -227,7 +230,7 @@ export default {
       return this.device?.name
     },
     programName () {
-      return this.programProxy?.event.target.name
+      return this.programProxy?.event.name
     },
     programTime () {
       return this.programProxy?.event.time
@@ -314,20 +317,23 @@ export default {
     transformEvent (event) {
       return {
         ...event,
+        name: event.target.name || EventPriorityDescription[event.priority],
         time: getEventDescription(event),
         startDateTime: toDate(event.start),
         endDateTime: toDate(event.until),
         style: null,
         selected: false,
         img () {
-          EventCache.getImage(this.target.type, this.target.id).then(img => {
-            if (img) {
-              this.style = {
-                backgroundSize: 'contain',
-                backgroundImage: `url("${img}")`
+          if (this.target.id) {
+            EventCache.getImage(this.target.type, this.target.id).then(img => {
+              if (img) {
+                this.style = {
+                  backgroundSize: 'contain',
+                  backgroundImage: `url("${img}")`
+                }
               }
-            }
-          })
+            })
+          }
         }
       }
     },
@@ -433,6 +439,7 @@ export default {
             }
           })
           event.img?.()
+          console.log(arr)
           if (endDate >= this.$endDateTime) {
             break
           }
@@ -682,7 +689,9 @@ export default {
   background-color: #eae2fe;
 }
 
-.priority3 {
+.priority3,
+.priority4,
+.priority99 {
   color: #ff2222;
   background-color: #ffecec;
 }

+ 19 - 0
src/views/realm/device/settings/api.js

@@ -15,3 +15,22 @@ export function updateAttributes (deviceId, attributes) {
     data: attributes
   })
 }
+
+export function getAdAttributes (deviceId) {
+  return request({
+    url: `/ad/device/info`,
+    method: 'GET',
+    params: { id: deviceId }
+  })
+}
+
+export function updateAdAttributes (deviceId, attributes) {
+  return update({
+    url: '/ad/device/info',
+    method: 'POST',
+    data: {
+      deviceId,
+      ...attributes
+    }
+  })
+}

+ 127 - 0
src/views/realm/device/settings/components/AdConfigDialog.vue

@@ -0,0 +1,127 @@
+<template>
+  <confirm-dialog
+    ref="configDialog"
+    :title="title"
+    @confirm="onSave"
+  >
+    <div class="c-grid-form medium u-align-self--center">
+      <span class="c-grid-form__label required">开机时间</span>
+      <el-time-picker
+        v-model="attributes.openTime"
+        class="c-grid-form__option"
+        value-format="HH:mm"
+        format="HH:mm"
+        :clearable="false"
+      />
+      <span class="c-grid-form__label required">关机时间</span>
+      <el-time-picker
+        v-model="attributes.closeTime"
+        class="c-grid-form__option"
+        value-format="HH:mm"
+        format="HH:mm"
+        :clearable="false"
+      />
+      <span class="c-grid-form__label required">起投时长</span>
+      <div
+        class="c-grid-form__info c-grid-form__option"
+        data-info="单位秒,5秒为间隔,5~60"
+      >
+        <el-input-number
+          v-model="attributes.minDuration"
+          :min="5"
+          :max="60"
+          :step="5"
+          step-strictly
+        />
+      </div>
+      <span class="c-grid-form__label required">起投次数</span>
+      <el-input-number
+        v-model="attributes.minCount"
+        class="c-grid-form__option"
+        :min="1"
+        :max="1000"
+        step-strictly
+      />
+      <span class="c-grid-form__label required">价格(分)</span>
+      <el-input-number
+        v-model="attributes.price"
+        class="c-grid-form__info c-grid-form__option"
+        :data-info="price"
+        :min="1"
+        :max="1000000000"
+        step-strictly
+      />
+    </div>
+  </confirm-dialog>
+</template>
+
+<script>
+import {
+  getAdAttributes,
+  updateAdAttributes
+} from '../api'
+
+export default {
+  name: 'AdConfigDialog',
+  data () {
+    return {
+      isAdd: false,
+      attributes: {}
+    }
+  },
+  computed: {
+    title () {
+      return `${this.isAdd ? '新增' : '更新'}广告属性`
+    },
+    price () {
+      const price = this.attributes.price || 0
+      return `${(price / 100).toFixed(2)}元`
+    }
+  },
+  methods: {
+    show ({ id }) {
+      const loading = this.$showLoading()
+      this.$deviceId = id
+      getAdAttributes(id).then(({ data }) => {
+        this.isAdd = !data
+        this.attributes = {
+          openTime: '07:00', // 开机时间
+          closeTime: '22:00', // 关机时间
+          minDuration: 5, // 最小投放广告时长(单位秒)
+          minCount: 10, // 最小投放次数
+          price: 1000, // 单位分
+          ...data
+        }
+        this.$refs.configDialog.show()
+      }).finally(() => {
+        this.$closeLoading(loading)
+      })
+    },
+    onSave (done) {
+      const { openTime, closeTime } = this.attributes
+      if (!openTime) {
+        this.$message({
+          type: 'warning',
+          message: '请选择开机时间'
+        })
+        return
+      }
+      if (!closeTime) {
+        this.$message({
+          type: 'warning',
+          message: '请选择关机时间'
+        })
+        return
+      }
+      if (openTime >= closeTime) {
+        this.$message({
+          type: 'warning',
+          message: '开机时间不能大于等于关机时间'
+        })
+        return
+      }
+      updateAdAttributes(this.$deviceId, this.attributes).then(done)
+    }
+  }
+}
+</script>

+ 131 - 14
src/views/realm/device/settings/components/AttributeConfigDialog.vue

@@ -12,11 +12,49 @@
         placeholder="rtsp://192.168.0.11:8554"
         clearable
       />
+      <span class="c-grid-form__label">自助二维码</span>
+      <div class="l-flex--row c-grid-form__option">
+        <el-switch
+          v-model="attributes.orderQr"
+          active-color="#13ce66"
+          inactive-color="#ff4949"
+          active-value="1"
+          inactive-value="0"
+        />
+      </div>
+      <span class="c-grid-form__label">二维码宽高</span>
+      <div
+        class="l-flex--row c-grid-form__option c-grid-form__info"
+        :data-info="maxSizeDesc"
+      >
+        <el-input-number
+          v-model="attributes.orderQrSize"
+          :min="64"
+          :max="maxSize"
+          step-strictly
+        />
+      </div>
+      <span class="c-grid-form__label">二维码内容</span>
+      <el-input
+        v-model.trim="attributes.orderQrContent"
+        class="c-grid-form__option"
+        clearable
+      />
+      <span class="c-grid-form__label">二维码位置</span>
+      <el-select v-model="position">
+        <el-option
+          v-for="option in positionOptions"
+          :key="option.value"
+          :label="option.label"
+          :value="option.value"
+        />
+      </el-select>
     </div>
   </confirm-dialog>
 </template>
 
 <script>
+import { publish } from '@/utils/mqtt'
 import { validIPv4Address } from '@/utils/validate'
 import {
   getAttributes,
@@ -29,27 +67,68 @@ const attributeSet = [
       return '回采卡拉流地址格式错误'
     }
     return null
+  }, type: 'string' },
+  { key: 'orderQr', type: 'boolean' },
+  { key: 'orderQrSize', type: 'string', defaults ({ wide, high }) {
+    return Math.max(64, Math.min(wide, high) / 20 | 0)
+  } },
+  { key: 'orderQrContent', type: 'string', defaults ({ id }) {
+    return `${process.env.VUE_APP_AD_ORDER_QR}?device=${id}`
+  }, valid (val, map) {
+    if (!val && map.orderQr === '1') {
+      return '请填写二维码内容'
+    }
+    return null
+  } },
+  { key: 'orderQrTop', type: 'string', defaults () {
+    return '-3'
+  } },
+  { key: 'orderQrLeft', type: 'string', defaults () {
+    return '-3'
   } }
 ]
 
+const defaultValue = {
+  string: '',
+  boolean: '0'
+}
+
 export default {
   name: 'AttibuteConfigDialog',
   data () {
     return {
+      maxSize: 64,
+      position: 'rb',
+      positionOptions: [
+        { value: 'lt', label: '左上' },
+        { value: 'lb', label: '左下' },
+        { value: 'rt', label: '右上' },
+        { value: 'rb', label: '右下' }
+      ],
       attributes: {}
     }
   },
+  computed: {
+    maxSizeDesc () {
+      return `范围:64~${this.maxSize}`
+    }
+  },
   methods: {
-    show ({ id }) {
+    show (device) {
       const loading = this.$showLoading()
-      this.$deviceId = id
+      const { id, productId, wide, high, tenant } = device
       getAttributes(id).then(({ data }) => {
+        this.$productId = productId
+        this.$deviceId = id
+        this.$extra = { tenant, width: wide, height: high }
+        this.maxSize = Math.min(wide, high) / 2 | 0
         const attributes = {}
-        attributeSet.forEach(({ key }) => {
-          attributes[key] = data[key] || ''
-          attributes[`_${key}`] = attributes[key]
+        attributeSet.forEach(({ key, type, defaults }) => {
+          attributes[key] = data[key] || (defaults ? defaults(device) : defaultValue[type])
+          attributes[`_${key}`] = data[key]
         })
         this.attributes = attributes
+        this.position = `${attributes.orderQrLeft === '-1' ? 'l' : 'r'}${attributes.orderQrTop === '-1' ? 't' : 'b'}`
         this.$refs.configDialog.show()
       }).finally(() => {
         this.$closeLoading(loading)
@@ -58,20 +137,46 @@ export default {
     onSave (done) {
       const attributes = this.attributes
       const attributeMap = {}
+      const deviceAttributes = {
+        ...this.$extra
+      }
+      switch (this.position) {
+        case 'lt':
+          attributes.orderQrTop = '-1'
+          attributes.orderQrLeft = '-1'
+          break
+        case 'lb':
+          attributes.orderQrTop = '-3'
+          attributes.orderQrLeft = '-1'
+          break
+        case 'rt':
+          attributes.orderQrTop = '-1'
+          attributes.orderQrLeft = '-3'
+          break
+        case 'rb':
+          attributes.orderQrTop = '-3'
+          attributes.orderQrLeft = '-3'
+          break
+        default:
+          break
+      }
       let needUpdate = false
       for (let i = 0; i < attributeSet.length; i++) {
         const { key, valid } = attributeSet[i]
-        const value = attributes[key]
+        const value = `${attributes[key]}`
+        deviceAttributes[key] = value
         if (value === attributes[`_${key}`]) {
           continue
         }
-        const message = valid(value)
-        if (message) {
-          this.$message({
-            type: 'warning',
-            message
-          })
-          return
+        if (valid) {
+          const message = valid(value, attributes)
+          if (message) {
+            this.$message({
+              type: 'warning',
+              message
+            })
+            return
+          }
         }
         attributeMap[key] = value
         needUpdate = true
@@ -80,7 +185,19 @@ export default {
         done()
         return
       }
-      updateAttributes(this.$deviceId, attributeMap).then(done)
+      updateAttributes(this.$deviceId, attributeMap).then(() => {
+        const timestamp = `${Date.now()}`
+        publish(
+          `${this.$productId}/${this.$deviceId}/setting/update`,
+          JSON.stringify({
+            messageId: `frontend_${this.$deviceId}_${timestamp}`,
+            timestamp,
+            deviceAttributes
+          }),
+          true
+        )
+        done()
+      })
     }
   }
 }

+ 13 - 1
src/views/realm/device/settings/components/DeviceNormalConfig.vue

@@ -12,20 +12,29 @@
     >
       内容保护配置
     </button>
+    <button
+      class="o-button"
+      @click="onAdConfig"
+    >
+      广告属性配置
+    </button>
     <attribute-config-dialog ref="attributeConfigDialog" />
     <content-protection-config-dialog ref="contentProtectionConfigDialog" />
+    <ad-config-dialog ref="adConfigDialog" />
   </div>
 </template>
 
 <script>
 import AttributeConfigDialog from './AttributeConfigDialog'
 import ContentProtectionConfigDialog from './ContentProtectionConfigDialog'
+import AdConfigDialog from './AdConfigDialog'
 
 export default {
   name: 'DeviceNormalConfig',
   components: {
     AttributeConfigDialog,
-    ContentProtectionConfigDialog
+    ContentProtectionConfigDialog,
+    AdConfigDialog
   },
   props: {
     device: {
@@ -39,6 +48,9 @@ export default {
     },
     onContentProtectionConfig () {
       this.$refs.contentProtectionConfigDialog.show(this.device)
+    },
+    onAdConfig () {
+      this.$refs.adConfigDialog.show(this.device)
     }
   }
 }

+ 1 - 1
src/views/realm/device/settings/components/DeviceShadow.vue

@@ -67,7 +67,7 @@ export default {
       publish(
         `${this.device.productId}/${this.device.id}/function/invoke`,
         JSON.stringify({
-          timestamp: Date.now(),
+          timestamp: `${Date.now()}`,
           'function': 'updateConfig',
           inputs: [
             { name: 'reboot', value: 1 }

+ 3 - 2
src/views/review/components/ReviewPublish.vue

@@ -17,6 +17,7 @@ import {
 import {
   State,
   EventPriority,
+  EventPriorityDescription,
   EventTarget,
   PublishType
 } from '@/constant'
@@ -77,10 +78,10 @@ export default {
       let type = ''
       switch (target.type) {
         case PublishType.CALENDAR:
-          type = '排期'
+          type = EventPriorityDescription[EventPriority.NORMAL]
           break
         case PublishType.EVENT:
-          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          type = EventPriorityDescription[target.detail.priority]
           break
         default:
           break

+ 3 - 2
src/views/review/history/index.vue

@@ -23,6 +23,7 @@ import {
 import {
   State,
   EventPriority,
+  EventPriorityDescription,
   EventTarget,
   PublishType
 } from '@/constant'
@@ -90,10 +91,10 @@ export default {
       let type = ''
       switch (target.type) {
         case PublishType.CALENDAR:
-          type = '排期'
+          type = EventPriorityDescription[EventPriority.NORMAL]
           break
         case PublishType.EVENT:
-          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          type = EventPriorityDescription[target.detail.priority]
           break
         default:
           break

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

@@ -14,6 +14,7 @@
 import {
   State,
   EventPriority,
+  EventPriorityDescription,
   EventTarget,
   PublishType
 } from '@/constant'
@@ -81,10 +82,10 @@ export default {
       let type = ''
       switch (target.type) {
         case PublishType.CALENDAR:
-          type = '排期'
+          type = EventPriorityDescription[EventPriority.NORMAL]
           break
         case PublishType.EVENT:
-          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          type = EventPriorityDescription[target.detail.priority]
           break
         default:
           break

+ 3 - 2
src/views/review/workflow/index.vue

@@ -18,6 +18,7 @@ import { getPublishWorkflows } from '@/api/workflow'
 import {
   PublishType,
   EventPriority,
+  EventPriorityDescription,
   State
 } from '@/constant'
 import { getEventDescription } from '@/utils/event'
@@ -83,10 +84,10 @@ export default {
       let type = ''
       switch (target.type) {
         case PublishType.CALENDAR:
-          type = '排期'
+          type = EventPriorityDescription[EventPriority.NORMAL]
           break
         case PublishType.EVENT:
-          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          type = EventPriorityDescription[target.detail.priority]
           break
         default:
           break

+ 3 - 2
src/views/review/workflow/mine/index.vue

@@ -49,6 +49,7 @@ import {
 import {
   PublishType,
   EventPriority,
+  EventPriorityDescription,
   State,
   EventTarget
 } from '@/constant'
@@ -223,10 +224,10 @@ export default {
       let type = ''
       switch (target.type) {
         case PublishType.CALENDAR:
-          type = '排期'
+          type = EventPriorityDescription[EventPriority.NORMAL]
           break
         case PublishType.EVENT:
-          type = ['', '默认播放', '单播', '插播'][target.detail.priority]
+          type = EventPriorityDescription[target.detail.priority]
           break
         default:
           break

+ 28 - 4
src/views/schedule/deploy/index.vue

@@ -59,6 +59,20 @@
               />
             </el-select>
           </div>
+          <div
+            v-if="isEvent"
+            class="c-list__item"
+          >
+            <div class="o-type">级别</div>
+            <el-select v-model="priority">
+              <el-option
+                v-for="option in priorityOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </div>
           <div
             v-if="isCalendar"
             class="c-list__item c-grid-form medium col"
@@ -77,7 +91,6 @@
             v-show="isEvent"
             ref="picker"
             class="c-list__item"
-            :priority="3"
             :ratio="resolutionRatio"
             vertical
             @choosen="onChoosenProgram"
@@ -119,7 +132,9 @@ import {
   State,
   ScheduleType,
   EventTarget,
-  PublishType
+  PublishType,
+  EventPriority,
+  EventPriorityDescription
 } from '@/constant'
 
 export default {
@@ -128,10 +143,15 @@ export default {
     return {
       typeOptions: [
         { value: PublishType.CALENDAR, label: '排期' },
-        { value: PublishType.EVENT, label: '插播' }
+        { value: PublishType.EVENT, label: '节目' }
       ],
       active: 0,
       selectedDevices: [],
+      priority: EventPriority.INSERTED,
+      priorityOptions: [
+        { value: EventPriority.INSERTED, label: EventPriorityDescription[EventPriority.INSERTED] },
+        { value: EventPriority.EMERGENT, label: EventPriorityDescription[EventPriority.EMERGENT] }
+      ],
       eventOptions: null,
       scheduleSchema: {
         condition: { type: ScheduleType.COMPLEX, status: State.AVAILABLE, name: '' },
@@ -204,6 +224,7 @@ export default {
             this.active = 0
             this.$refs.tree.reset()
             this.eventOptions = null
+            this.priority = EventPriority.INSERTED
           })
           break
         default:
@@ -265,7 +286,10 @@ export default {
       if (event) {
         return Promise.resolve({
           type: PublishType.EVENT,
-          detail: { ...event }
+          detail: {
+            ...event,
+            priority: this.priority
+          }
         })
       }
       return Promise.reject()