index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <template>
  2. <wrapper
  3. class="c-step"
  4. fill
  5. margin
  6. padding
  7. background
  8. >
  9. <div class="l-flex__none l-flex--row c-step__header">
  10. <button
  11. class="l-flex__none c-sibling-item o-button"
  12. :class="{ hidden: active === 0 }"
  13. @click="onPresent"
  14. >
  15. 上一步
  16. </button>
  17. <el-steps
  18. :active="active"
  19. class="l-flex__fill"
  20. finish-status="success"
  21. align-center
  22. >
  23. <el-step title="选择设备" />
  24. <el-step title="选择内容" />
  25. </el-steps>
  26. <button
  27. class="l-flex__none c-sibling-item o-button"
  28. :class="{ hidden: hideNext }"
  29. @click="onNext"
  30. >
  31. {{ btnMsg }}
  32. </button>
  33. </div>
  34. <div class="l-flex__fill l-flex">
  35. <device-tree
  36. v-show="active === 0"
  37. ref="tree"
  38. class="l-flex__fill"
  39. exact
  40. checkbox
  41. check-on-click-node
  42. @change="onChange"
  43. />
  44. <template v-if="active > 0">
  45. <div class="l-flex--col c-sibling-item far c-step__column">
  46. <div class="l-flex__none c-sibling-item--v u-font-size--sm u-bold">设备</div>
  47. <div class="l-flex__fill c-sibling-item--v near u-overflow-y--auto">
  48. <div
  49. v-for="device in selectedDevices"
  50. :key="device.id"
  51. class="c-sibling-item--v u-font-size--sm"
  52. >
  53. {{ device.name }}
  54. </div>
  55. </div>
  56. </div>
  57. <div
  58. v-if="isCalendar"
  59. class="l-flex__fill l-flex--col c-step__column"
  60. >
  61. <div class="c-sibling-item--v has-bottom-padding--sm has-bottom-border">
  62. <div class="c-sibling-item--v u-font-size--sm u-bold">上播类型</div>
  63. <div class="l-flex--row c-sibling-item--v near">
  64. <el-select
  65. v-model="eventOptions.type"
  66. class="c-sibling-item"
  67. @change="onChangeType"
  68. >
  69. <el-option
  70. v-for="option in typeOptions"
  71. :key="option.value"
  72. :label="option.label"
  73. :value="option.value"
  74. />
  75. </el-select>
  76. <div
  77. class="l-flex--row c-sibling-item far has-active u-ellipsis"
  78. @click="onView"
  79. >
  80. {{ currentTarget }}
  81. </div>
  82. </div>
  83. </div>
  84. <schema-table
  85. class="c-sibling-item--v"
  86. :schema="schema"
  87. row-key="id"
  88. :current-row-key="selectedId"
  89. highlight-current-row
  90. @row-click="onClickRow"
  91. />
  92. </div>
  93. <div class="l-flex__none l-flex--col c-step__column large">
  94. <div class="c-sibling-item--v u-font-size--sm u-bold">上播配置</div>
  95. <div class="c-sibling-item--v u-font-size--sm">上播类型</div>
  96. <el-select
  97. v-model="eventOptions.type"
  98. class="c-sibling-item--v near u-width--sm"
  99. @change="onChangeType"
  100. >
  101. <el-option
  102. v-for="option in typeOptions"
  103. :key="option.value"
  104. :label="option.label"
  105. :value="option.value"
  106. />
  107. </el-select>
  108. <template v-if="!isDefaultPlayback">
  109. <div class="c-sibling-item--v u-font-size--sm">优先级</div>
  110. <el-select
  111. v-model="priority"
  112. class="c-sibling-item--v near u-width--sm"
  113. >
  114. <el-option
  115. v-for="option in priorityOptions"
  116. :key="option.value"
  117. :label="option.label"
  118. :value="option.value"
  119. />
  120. </el-select>
  121. </template>
  122. <div class="c-sibling-item--v">
  123. <div
  124. class="l-flex--row inline u-font-size--sm has-active"
  125. @click="onAddEvent"
  126. >
  127. <span class="c-sibling-item">播放时段</span>
  128. <i class="c-sibling-item near el-icon-circle-plus-outline" />
  129. </div>
  130. </div>
  131. <schema-table
  132. ref="timeTable"
  133. class="c-sibling-item--v near"
  134. :schema="timeSchema"
  135. />
  136. </div>
  137. <event-target-picker
  138. ref="eventTargetPicker"
  139. class="l-flex__fill c-step__column"
  140. :event-target="eventTarget"
  141. :ratio="ratio"
  142. />
  143. </template>
  144. </div>
  145. <event-frequency-config-dialog
  146. ref="eventFrequencyConfigDialog"
  147. @confirm="onConfirmEventFrequency"
  148. />
  149. <confirm-dialog
  150. ref="conflictDialog"
  151. title="冲突提醒"
  152. cancel-text="重新编辑"
  153. confirm-text="覆盖"
  154. append-to-body
  155. @confirm="onCover"
  156. >
  157. <div
  158. v-for="conflict in conflicts"
  159. :key="conflict.key"
  160. >
  161. {{ conflict.info }}
  162. </div>
  163. </confirm-dialog>
  164. <material-dialog ref="materialDialog" />
  165. </wrapper>
  166. </template>
  167. <script>
  168. import {
  169. State,
  170. ScheduleType,
  171. EventTarget,
  172. EventTargetInfo,
  173. PublishType,
  174. PublishTargetType,
  175. EventPriority,
  176. EventPriorityInfo,
  177. EventFrequency
  178. } from '@/constant'
  179. import {
  180. toDateStr,
  181. getConflict,
  182. getEventDescription
  183. } from '@/utils/event'
  184. import { getSchedules } from '@/api/calendar'
  185. import { publish } from '@/api/platform'
  186. import EventFrequencyConfigDialog from '../components/EventFrequencyConfigDialog.vue'
  187. const DEFAULT_PLAYBACK = 'DEFAULT_PLAYBACK'
  188. export default {
  189. name: 'DeployDevice',
  190. components: {
  191. EventFrequencyConfigDialog
  192. },
  193. data () {
  194. return {
  195. active: 0,
  196. ratio: '',
  197. selectedDevices: [],
  198. priority: EventPriority.INSERTED,
  199. priorityOptions: [
  200. { value: EventPriority.SCHEDULING, label: EventPriorityInfo[EventPriority.SCHEDULING] },
  201. { value: EventPriority.INSERTED, label: EventPriorityInfo[EventPriority.INSERTED] },
  202. { value: EventPriority.EMBEDDED, label: EventPriorityInfo[EventPriority.EMBEDDED] },
  203. { value: EventPriority.EMERGENT, label: EventPriorityInfo[EventPriority.EMERGENT] }
  204. ],
  205. eventOptions: null,
  206. typeOptions: [
  207. { value: PublishTargetType.EVENT, label: '事件' },
  208. // { value: PublishTargetType.CALENDAR, label: '排期' },
  209. { value: DEFAULT_PLAYBACK, label: '默认播放' }
  210. ],
  211. schema: {
  212. list: getSchedules,
  213. condition: { type: ScheduleType.COMPLEX, status: State.AVAILABLE },
  214. filters: [
  215. { key: 'name', type: 'search', placeholder: '名称' }
  216. ],
  217. cols: [
  218. { render: ({ id }, h) => h(
  219. 'span',
  220. { staticClass: `el-radio__input ${id === this.selectedId ? 'is-checked' : ''}` },
  221. [h('span', { staticClass: 'el-radio__inner' })]
  222. ), width: 60, align: 'center' },
  223. { prop: 'name', label: '名称' },
  224. { prop: 'resolutionRatio', label: '分辨率' },
  225. { type: 'invoke', render: [
  226. { label: '查看', on: this.onView }
  227. ] }
  228. ]
  229. },
  230. timeSchema: {
  231. nonPagination: true,
  232. props: {
  233. size: 'small'
  234. },
  235. list: this.getEvents,
  236. cols: [
  237. { prop: 'time', label: '生效时间', 'show-overflow-tooltip': false },
  238. { type: 'invoke', render: [
  239. { label: '移除', on: this.removeEventProxy }
  240. ], width: 60 }
  241. ]
  242. },
  243. events: [],
  244. eventTarget: this.createEventTarget(),
  245. conflicts: []
  246. }
  247. },
  248. computed: {
  249. hideNext () {
  250. switch (this.active) {
  251. case 0:
  252. return this.selectedDevices.length === 0
  253. case 1:
  254. return this.isCalendar ? !this.selectedId : false
  255. default:
  256. return false
  257. }
  258. },
  259. isCalendar () {
  260. return this.eventOptions?.type === PublishTargetType.CALENDAR
  261. },
  262. isEvent () {
  263. return this.eventOptions?.type === PublishTargetType.EVENT
  264. },
  265. isDefaultPlayback () {
  266. return this.eventOptions?.type === DEFAULT_PLAYBACK
  267. },
  268. btnMsg () {
  269. return this.active < 1 ? '下一步' : '发布'
  270. },
  271. selectedId () {
  272. return this.eventOptions?.target?.id
  273. },
  274. currentTarget () {
  275. return this.eventOptions?.target?.name
  276. }
  277. },
  278. methods: {
  279. onPresent () {
  280. if (this.active > 0) {
  281. this.active = 0
  282. }
  283. },
  284. onNext () {
  285. switch (this.active) {
  286. case 0:
  287. if (this.checkDevices()) {
  288. this.active += 1
  289. }
  290. break
  291. case 1:
  292. case 2:
  293. this.publish().then(() => {
  294. this.active = 0
  295. this.$refs.tree.reset()
  296. this.eventOptions = null
  297. this.priority = EventPriority.INSERTED
  298. this.events = []
  299. this.eventTarget = this.createEventTarget()
  300. })
  301. break
  302. default:
  303. break
  304. }
  305. },
  306. onChange (devices) {
  307. this.selectedDevices = devices
  308. },
  309. createEventOptions (type) {
  310. return {
  311. type,
  312. target: null
  313. }
  314. },
  315. checkDevices () {
  316. const devices = this.selectedDevices
  317. const length = devices.length
  318. if (!length) {
  319. this.$message({
  320. type: 'warning',
  321. message: '请选择目标设备'
  322. })
  323. return false
  324. }
  325. const ratio = devices[0].resolutionRatio
  326. if (devices.some(device => device.resolutionRatio !== ratio)) {
  327. this.ratio = ''
  328. // this.$confirm(
  329. // '上播内容可能无法完全适配,继续下一步?',
  330. // '选择的设备分辨率不一致',
  331. // { type: 'warning' }
  332. // ).then(() => {
  333. // this.eventOptions = this.createEventOptions(PublishTargetType.EVENT)
  334. // this.active += 1
  335. // })
  336. // return false
  337. } else {
  338. this.ratio = ratio
  339. }
  340. this.eventOptions = this.createEventOptions(PublishTargetType.EVENT)
  341. return true
  342. },
  343. onChangeType (type) {
  344. this.eventOptions = this.createEventOptions(type)
  345. },
  346. onClickRow (row) {
  347. this.eventOptions.target = row
  348. },
  349. onView ({ id }) {
  350. this.$refs.materialDialog.showSchedule(id)
  351. },
  352. createEventTarget () {
  353. return { type: EventTarget.ASSETS }
  354. },
  355. getEvents () {
  356. return Promise.resolve(({ data: this.events }))
  357. },
  358. onAddEvent () {
  359. this.$refs.eventFrequencyConfigDialog.show()
  360. },
  361. onConfirmEventFrequency ({ value, done }) {
  362. const conflicts = new Map()
  363. if (this.events.length) {
  364. this.events.forEach(eventProxy => {
  365. value.forEach(event => {
  366. const date = getConflict(event, eventProxy.origin)
  367. if (date) {
  368. conflicts.set(eventProxy.key, {
  369. key: eventProxy.key,
  370. info: `与 ${eventProxy.time} 有冲突`
  371. })
  372. }
  373. })
  374. })
  375. }
  376. if (conflicts.size) {
  377. this.conflicts = [...conflicts.values()]
  378. this.$conflictOptions = { value, done }
  379. this.$refs.conflictDialog.show()
  380. return
  381. }
  382. this.onAdded(value, done)
  383. },
  384. onCover (closeCb) {
  385. this.conflicts.forEach(this.removeEventProxy)
  386. const { value, done } = this.$conflictOptions
  387. this.$conflictOptions = null
  388. closeCb()
  389. this.onAdded(value, done)
  390. },
  391. onAdded (value, done) {
  392. this.events = [
  393. ...value.map(event => {
  394. return {
  395. key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
  396. origin: event,
  397. time: getEventDescription(event)
  398. }
  399. }),
  400. ...this.events
  401. ]
  402. this.$refs.timeTable?.pageTo(1)
  403. done()
  404. },
  405. removeEventProxy (eventProxy) {
  406. if (eventProxy) {
  407. const { key } = eventProxy
  408. const index = this.events.findIndex(event => event.key === key)
  409. if (~index) {
  410. this.events.splice(index, 1)
  411. return index
  412. }
  413. }
  414. return -1
  415. },
  416. getPublishTarget () {
  417. if (!this.events.length) {
  418. this.$message({
  419. type: 'warning',
  420. message: '请添加上播时段'
  421. })
  422. return Promise.reject('invalid event, no time')
  423. }
  424. if (this.isCalendar) {
  425. const { id, name, resolutionRatio } = this.eventOptions.target
  426. return Promise.resolve({
  427. publishType: PublishType.PROGRAM_TO_DEVICE,
  428. targetList: [{
  429. type: PublishTargetType.CALENDAR,
  430. detail: id
  431. }],
  432. name,
  433. resolutionRatio
  434. })
  435. }
  436. const eventTarget = this.$refs.eventTargetPicker.getValue()
  437. if (!eventTarget) {
  438. return Promise.reject('invalid event, no target')
  439. }
  440. const { detail } = this.$refs.eventTargetPicker.getSnapshot()
  441. const type = PublishTargetType.EVENT
  442. const priority = this.isDefaultPlayback ? EventPriority.SHIM : this.priority
  443. return Promise.resolve({
  444. publishType: eventTarget.type === EventTarget.ASSETS
  445. ? PublishType.ASSET_TO_DEVICE
  446. : PublishType.PROGRAM_TO_DEVICE,
  447. targetList: this.isDefaultPlayback
  448. ? [{
  449. type,
  450. detail: {
  451. priority,
  452. freq: EventFrequency.ONCE,
  453. start: `${toDateStr(new Date())} 00:00:00`,
  454. target: eventTarget
  455. }
  456. }]
  457. : this.events.map(({ origin }) => {
  458. return {
  459. type,
  460. detail: {
  461. priority,
  462. ...origin,
  463. target: eventTarget
  464. }
  465. }
  466. }),
  467. name: detail?.name || EventTargetInfo[eventTarget.type],
  468. resolutionRatio: detail?.resolutionRatio
  469. })
  470. },
  471. publish () {
  472. return this.getPublishTarget().then(
  473. ({ publishType, targetList, name, resolutionRatio }) => this.$prompt(
  474. `<p>发布后需审核生效,操作完成后请通知相关人员进行审核</p>${resolutionRatio && (!this.ratio || resolutionRatio !== this.ratio) ? '<p class="u-color--error">上播内容与设备分辨率不一致,可能无法完全适配</p>' : ''}<br/><p class="u-color--black u-font-size--sm u-bold">编单名称</p>`,
  475. '操作确认',
  476. {
  477. dangerouslyUseHTMLString: true,
  478. closeOnClickModal: false,
  479. inputPlaceholder: '发布内容的简单描述',
  480. inputValue: name,
  481. inputPattern: /^.{1,30}$/,
  482. inputErrorMessage: '请用1~30个字符进行描述',
  483. confirmButtonText: '发布',
  484. cancelButtonText: '取消'
  485. }
  486. ).then(
  487. ({ value }) => publish(
  488. publishType,
  489. this.selectedDevices.map(device => device.id),
  490. targetList,
  491. {
  492. programCalendarName: value,
  493. resolutionRatio: resolutionRatio || '自适应',
  494. remark: EventPriorityInfo[this.priority]
  495. }
  496. )
  497. )
  498. )
  499. }
  500. }
  501. }
  502. </script>
  503. <style lang="scss" scoped>
  504. .o-schedule-button {
  505. position: relative;
  506. width: 217px;
  507. height: 40px;
  508. color: $blue;
  509. font-size: 14px;
  510. line-height: 1;
  511. border-radius: $radius--sm;
  512. border: 1px solid #dcdfe6;
  513. &:hover {
  514. border-color: #c0c4cc;
  515. }
  516. &__label {
  517. display: inline-flex;
  518. justify-content: center;
  519. align-items: center;
  520. position: absolute;
  521. width: 100%;
  522. height: 100%;
  523. }
  524. }
  525. </style>