bak.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. <template>
  2. <wrapper
  3. auto
  4. margin
  5. padding
  6. background
  7. >
  8. <div class="l-flex__none l-flex c-device-detail has-bottom-padding">
  9. <div
  10. class="l-flex__none c-device-detail__screen o-program"
  11. :class="{ 'u-pointer': programProxy }"
  12. :style="programStyle"
  13. @click="onView"
  14. />
  15. <div class="l-flex__auto l-flex--col">
  16. <div class="l-flex__none c-device-detail__name u-ellipsis">{{ deivceName }}</div>
  17. <template v-if="programProxy">
  18. <div class="l-flex__none c-device-detail__program u-ellipsis">
  19. <span
  20. class="u-pointer"
  21. @click="onView"
  22. >
  23. {{ programName }}
  24. </span>
  25. </div>
  26. <div class="l-flex__none c-device-detail__time">{{ programTime }}</div>
  27. </template>
  28. </div>
  29. </div>
  30. <div
  31. v-loading="deviceOptions.loading"
  32. class="l-flex__fill l-flex--col c-timeline"
  33. >
  34. <div class="l-flex__none l-flex--row has-bottom-padding">
  35. <div class="l-flex__auto c-sibling-item" />
  36. <el-date-picker
  37. v-model="current"
  38. class="l-flex__none c-sibling-item u-pointer"
  39. type="date"
  40. placeholder="选择日期"
  41. :picker-options="pickerOptions"
  42. :editable="false"
  43. :clearable="false"
  44. @change="onTimeChange"
  45. />
  46. <search-input
  47. v-model.trim="deviceOptions.params.name"
  48. class="l-flex__none c-sibling-item"
  49. placeholder="设备名称"
  50. @search="search"
  51. />
  52. <button
  53. class="l-flex__none c-sibling-item near o-button"
  54. @click="search"
  55. >
  56. 搜索
  57. </button>
  58. </div>
  59. <div class="l-flex__none l-flex c-timeline__row header">
  60. <div class="l-flex__none c-timeline__left">
  61. <span class="o-priority is-priority99">高</span>
  62. <span class="o-priority is-priority3">中</span>
  63. <span class="o-priority is-priority1">低</span>
  64. </div>
  65. <div class="l-flex__auto l-flex--row c-timeline__right">
  66. <i
  67. class="l-flex__none c-sibling-item c-timeline__arrow el-icon-arrow-left u-pointer"
  68. :class="{ display: canPrevious }"
  69. @click="offsetTime(-1)"
  70. />
  71. <div class="l-flex__auto l-flex--row c-sibling-item c-timeline__time">
  72. <div
  73. v-for="time in times"
  74. :key="time"
  75. class="l-flex__none"
  76. >
  77. {{ time }}
  78. </div>
  79. </div>
  80. <i
  81. class="l-flex__none c-sibling-item c-timeline__arrow display el-icon-arrow-right u-pointer"
  82. @click="offsetTime(1)"
  83. />
  84. </div>
  85. </div>
  86. <div class="l-flex__self l-flex--col c-timeline__main u-relative">
  87. <div class="l-flex__auto u-overflow-y--auto">
  88. <div
  89. v-for="item in deviceOptions.list"
  90. :key="item.id"
  91. class="l-flex c-timeline__row"
  92. :class="{ selected: item.id === deviceId }"
  93. @click="chooseProgramProxy(item)"
  94. >
  95. <div class="l-flex__none c-timeline__left u-relative u-pointer">
  96. <div class="u-ellipsis">{{ item.name }}</div>
  97. </div>
  98. <div class="l-flex__auto l-flex--col c-timeline__right">
  99. <div
  100. v-if="item.options.loading"
  101. class="l-flex--row c-timeline__programs"
  102. >
  103. <i class="el-icon-loading has-padding--h" />加载中...
  104. </div>
  105. <div
  106. v-else-if="item.options.error"
  107. class="l-flex--row c-timeline__programs has-padding--h"
  108. >
  109. <el-link
  110. type="warning"
  111. @click.stop="getTimeline(item)"
  112. >
  113. 获取失败,点击重试
  114. </el-link>
  115. </div>
  116. <template v-else-if="item.options.list.length">
  117. <div
  118. v-for="(programs, index) in item.options.list"
  119. :key="index"
  120. class="c-timeline__programs l-flex--row u-relative"
  121. >
  122. <div
  123. v-for="program in programs"
  124. :key="program.key"
  125. class="l-flex__none l-flex--row c-event-program u-pointer"
  126. :class="[{ 'selected': program.event.selected }, `is-priority${program.event.priority}`]"
  127. :style="program.style"
  128. @click.stop="chooseProgramProxy(item, program)"
  129. >
  130. <i
  131. class="l-flex__none c-event-program__img o-program is-ratio--16_9"
  132. :style="program.event.style"
  133. />
  134. <div class="l-flex__auto">
  135. <auto-text
  136. class="c-event-program__time"
  137. :text="program.time"
  138. :tag="program.style.width"
  139. />
  140. <auto-text
  141. class="c-event-program__name"
  142. :text="program.event.name"
  143. :tag="program.style.width"
  144. />
  145. </div>
  146. </div>
  147. </div>
  148. </template>
  149. <div
  150. v-else
  151. class="l-flex--row c-timeline__programs has-padding--h"
  152. >
  153. 当前时段暂无节目
  154. </div>
  155. </div>
  156. </div>
  157. </div>
  158. <status-wrapper
  159. v-if="isAbnormal"
  160. :error="deviceOptions.error"
  161. @click="getDevices"
  162. />
  163. <div
  164. v-show="style"
  165. class="c-timeline__line"
  166. >
  167. <div
  168. class="c-timeline__mask"
  169. :style="style"
  170. />
  171. </div>
  172. </div>
  173. <pagination
  174. :total="deviceOptions.totalCount"
  175. :page.sync="deviceOptions.params.pageNum"
  176. :limit.sync="deviceOptions.params.pageSize"
  177. @pagination="getDevices"
  178. />
  179. </div>
  180. <material-dialog ref="materialDialog" />
  181. </wrapper>
  182. </template>
  183. <script>
  184. import {
  185. getDevices,
  186. getTimeline
  187. } from '@/api/device'
  188. import { EventPriorityInfo } from '@/constant'
  189. import {
  190. toDate,
  191. toDateStr,
  192. toTimeStr,
  193. toZeroPoint,
  194. getNearestHitDate,
  195. getStartDate,
  196. getFinishDate,
  197. pickMin,
  198. pickMax,
  199. getEventDescription
  200. } from '@/utils/event'
  201. import { EventCache } from '@/utils/cache'
  202. import { createListOptions } from '@/utils'
  203. export default {
  204. name: 'ScheduleTimeline',
  205. data () {
  206. return {
  207. deviceOptions: createListOptions({
  208. name: '',
  209. activate: 1,
  210. pageSize: 5
  211. }),
  212. style: null,
  213. canPrevious: false,
  214. startHour: 0,
  215. times: [],
  216. current: null,
  217. device: null,
  218. programProxy: null
  219. }
  220. },
  221. computed: {
  222. deviceId () {
  223. return this.device?.id
  224. },
  225. deivceName () {
  226. return this.device?.name
  227. },
  228. programName () {
  229. return this.programProxy?.event.name
  230. },
  231. programTime () {
  232. return this.programProxy?.event.time
  233. },
  234. programStyle () {
  235. return this.programProxy?.event.style
  236. },
  237. isAbnormal () {
  238. const deviceOptions = this.deviceOptions
  239. return deviceOptions.error || !deviceOptions.loading && deviceOptions.totalCount === 0
  240. },
  241. pickerOptions () {
  242. return {
  243. disabledDate: this.isDisableDate
  244. }
  245. }
  246. },
  247. created () {
  248. this.initTimes(new Date())
  249. this.getDevices()
  250. this.$timer = setInterval(this.calcLine, 1000)
  251. },
  252. beforeDestroy () {
  253. clearInterval(this.$timer)
  254. },
  255. methods: {
  256. getDevices () {
  257. this.device = null
  258. if (this.programProxy) {
  259. this.programProxy.event.selected = false
  260. }
  261. this.programProxy = null
  262. const options = this.deviceOptions
  263. options.error = false
  264. options.loading = true
  265. getDevices(options.params).then(({ data, totalCount }) => {
  266. options.list = data.map(this.transform)
  267. options.totalCount = totalCount
  268. options.list.forEach(this.getTimeline)
  269. }, () => {
  270. options.error = true
  271. options.list = []
  272. }).finally(() => {
  273. options.loading = false
  274. })
  275. },
  276. search () {
  277. const options = this.deviceOptions
  278. options.list = []
  279. options.totalCount = 0
  280. options.params.pageNum = 1
  281. this.getDevices()
  282. },
  283. transform (device) {
  284. const { id, name } = device
  285. return {
  286. id, name,
  287. options: {
  288. loading: true,
  289. error: false,
  290. events: [],
  291. list: []
  292. }
  293. }
  294. },
  295. getTimeline (device) {
  296. const options = device.options
  297. options.error = false
  298. options.loading = true
  299. getTimeline(device.id, { custom: true }).finally(() => {
  300. options.loading = false
  301. }).then(
  302. events => {
  303. const now = Date.now()
  304. options.events = this.transformEvents(events.filter(({ until }) => !until || now <= toDate(until).getTime()))
  305. this.calcEvents(options)
  306. },
  307. () => {
  308. options.error = true
  309. options.list = []
  310. }
  311. )
  312. },
  313. transformEvent (event) {
  314. return {
  315. ...event,
  316. name: event.target.name || EventPriorityInfo[event.priority],
  317. time: getEventDescription(event),
  318. startDateTime: toDate(event.start),
  319. endDateTime: toDate(event.until),
  320. style: null,
  321. selected: false,
  322. img () {
  323. let promise = null
  324. switch (this.target.type) {
  325. case EventTarget.RECUR:
  326. promise = EventCache.getImage(EventTarget.PROGRAM, this.target.programs[0]?.programId)
  327. break
  328. default:
  329. promise = EventCache.getImage(this.target.type, this.target.id)
  330. break
  331. }
  332. promise.then(img => {
  333. if (img) {
  334. this.style = {
  335. backgroundSize: 'contain',
  336. backgroundImage: `url("${img}")`
  337. }
  338. }
  339. })
  340. }
  341. }
  342. },
  343. transformEvents (events) {
  344. const map = {}
  345. const now = Date.now()
  346. for (let i = 0; i < events.length; i++) {
  347. const event = this.transformEvent(events[i])
  348. event.key = `${i}_${now}`
  349. if (!map[event.priority]) {
  350. map[event.priority] = []
  351. }
  352. map[event.priority].push(event)
  353. }
  354. return Object.keys(map)
  355. .sort((a, b) => a > b ? -1 : 1)
  356. .map(key => map[key].sort((a, b) => toDate(a.start) - toDate(b.start)))
  357. },
  358. isDisableDate (date) {
  359. const now = new Date()
  360. const min = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  361. const max = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 30)
  362. return date < min || date > max
  363. },
  364. initTimes (date) {
  365. const maxDate = pickMax(toDate(date), new Date())
  366. this.current = toZeroPoint(maxDate)
  367. this.startHour = Math.min(20, maxDate.getHours())
  368. this.refreshTimes(this.startHour)
  369. },
  370. refreshTimes (start) {
  371. const times = []
  372. for (let i = 0; i < 5; i++) {
  373. times.push(`${(start + i).toString().padStart(2, '0')}:00`)
  374. }
  375. this.times = times
  376. this.$startDateTime = toDate(this.current.getTime() + start * 3600000)
  377. this.$endDateTime = toDate(this.current.getTime() + (start + 4) * 3600000)
  378. this.calcLine()
  379. this.refreshTimeline()
  380. },
  381. calcLine () {
  382. const now = Date.now()
  383. this.canPrevious = now < this.$startDateTime
  384. if (now < this.$startDateTime || now >= this.$endDateTime) {
  385. this.style = null
  386. } else {
  387. this.style = {
  388. left: `${Math.min(100, (now - this.$startDateTime) / 144000)}%`
  389. }
  390. }
  391. },
  392. refreshTimeline () {
  393. this.deviceOptions.list.forEach(device => {
  394. this.calcEvents(device.options)
  395. })
  396. },
  397. offsetTime (offset) {
  398. const next = this.startHour + offset
  399. if (offset < 0) {
  400. const timestamp = this.current.getTime() + next * 3600000
  401. const now = new Date()
  402. now.setMinutes(0)
  403. now.setSeconds(0)
  404. now.setMilliseconds(0)
  405. if (timestamp >= now) {
  406. if (next < 0) {
  407. this.initTimes(timestamp)
  408. } else {
  409. this.refreshTimes(this.startHour = next)
  410. }
  411. }
  412. } else if (next > 20) {
  413. this.initTimes(this.$endDateTime)
  414. } else {
  415. this.refreshTimes(this.startHour = next)
  416. }
  417. },
  418. onTimeChange (val) {
  419. this.initTimes(val)
  420. },
  421. calcSamePriorityEvents (events) {
  422. const total = 144000
  423. const arr = []
  424. for (let i = 0; i < events.length; i++) {
  425. const event = events[i]
  426. const { startDateTime, endDateTime } = event
  427. if (endDateTime && endDateTime <= this.$startDateTime || startDateTime >= this.$endDateTime) {
  428. continue
  429. }
  430. const hit = getNearestHitDate(event, this.$startDateTime, this.$endDateTime)
  431. if (hit) {
  432. const startDate = getStartDate(event, hit)
  433. const endDate = getFinishDate(event, hit)
  434. arr.push({
  435. key: event.key,
  436. event,
  437. time: `${toDateStr(startDate)} ${toTimeStr(startDate)} - ${toDateStr(endDate)} ${toTimeStr(endDate)}`,
  438. style: {
  439. left: `${(hit - this.$startDateTime) / total}%`,
  440. width: `${(pickMin(this.$endDateTime, getFinishDate(event, hit)) - hit) / total}%`,
  441. zIndex: event.priority
  442. }
  443. })
  444. event.img?.()
  445. if (endDate >= this.$endDateTime) {
  446. break
  447. }
  448. }
  449. }
  450. return arr
  451. },
  452. calcEvents (options) {
  453. if (options.loading || options.error) {
  454. return
  455. }
  456. options.list = options.events.map(this.calcSamePriorityEvents).filter(events => events.length)
  457. },
  458. chooseProgramProxy (device, programProxy) {
  459. this.device = device
  460. if (this.programProxy) {
  461. this.programProxy.event.selected = false
  462. }
  463. if (programProxy) {
  464. programProxy.event.selected = true
  465. }
  466. this.programProxy = programProxy
  467. },
  468. onView () {
  469. if (this.programProxy) {
  470. this.$refs.materialDialog.showEventTarget(this.programProxy.event.target)
  471. }
  472. }
  473. }
  474. }
  475. </script>
  476. <style lang="scss" scoped>
  477. .c-device-detail {
  478. &__screen {
  479. width: 352px;
  480. margin-right: 24px;
  481. }
  482. &__name {
  483. color: $black;
  484. font-size: 20px;
  485. font-weight: bold;
  486. line-height: 1;
  487. }
  488. &__program {
  489. margin-top: 20px;
  490. color: $blue;
  491. font-size: 20px;
  492. font-weight: bold;
  493. }
  494. &__time {
  495. margin-top: $spacing;
  496. color: $info--dark;
  497. font-size: 20px;
  498. }
  499. }
  500. .c-timeline {
  501. min-height: 400px;
  502. &__main {
  503. border: 1px solid $border;
  504. }
  505. &__row {
  506. & + & {
  507. border-top: 1px solid $border;
  508. }
  509. }
  510. &__row.header {
  511. .c-timeline__left {
  512. border-right: none;
  513. }
  514. }
  515. &__row.header &__right {
  516. color: $black;
  517. user-select: none;
  518. background-color: transparent;
  519. }
  520. &__row.selected &__left {
  521. color: #fff;
  522. background-color: #9fbfe8;
  523. }
  524. &__row.selected &__left::after {
  525. content: "";
  526. position: absolute;
  527. top: 0;
  528. left: 0;
  529. bottom: 0;
  530. width: 4px;
  531. background-color: $blue;
  532. }
  533. &__left {
  534. display: inline-flex;
  535. align-items: center;
  536. width: 210px;
  537. padding: 0 10px 0 20px;
  538. margin-right: 1px;
  539. color: $black;
  540. font-size: 16px;
  541. background-color: #fafbfc;
  542. border-right: 1px solid $border;
  543. }
  544. &__right {
  545. position: relative;
  546. color: $info--dark;
  547. font-size: 14px;
  548. line-height: 1;
  549. overflow: hidden;
  550. }
  551. &__programs {
  552. box-sizing: content-box;
  553. height: 54px;
  554. padding: 2px 0;
  555. & + & {
  556. border-top: 1px dashed $border;
  557. }
  558. }
  559. &__arrow {
  560. visibility: hidden;
  561. padding: 6px;
  562. color: #fff;
  563. font-size: 12px;
  564. border-radius: $radius--sm;
  565. background-color: $blue;
  566. &.display {
  567. visibility: visible;
  568. }
  569. }
  570. &__time {
  571. justify-content: space-between;
  572. padding: 10px 0;
  573. color: $black;
  574. font-size: 14px;
  575. }
  576. &__line {
  577. position: absolute;
  578. top: -12px;
  579. left: 210px;
  580. right: 0;
  581. bottom: 0;
  582. pointer-events: none;
  583. }
  584. &__mask {
  585. position: absolute;
  586. top: 0;
  587. left: 0;
  588. bottom: 0;
  589. color: #ff0000;
  590. border-right: 1px solid currentColor;
  591. z-index: 999;
  592. &::after {
  593. content: "";
  594. position: absolute;
  595. top: 0;
  596. right: -5px;
  597. width: 9px;
  598. height: 9px;
  599. border-radius: 50%;
  600. background-color: currentColor;
  601. }
  602. }
  603. }
  604. .c-event-program {
  605. position: absolute;
  606. top: 0;
  607. bottom: 0;
  608. padding: 0 4px;
  609. font-size: 12px;
  610. overflow: hidden;
  611. &:hover {
  612. color: #fff;
  613. background-color: rgba($blue, 0.4);
  614. }
  615. &.selected {
  616. color: #fff;
  617. background-color: $blue;
  618. z-index: 99;
  619. }
  620. &__img {
  621. width: 96px;
  622. margin-right: 8px;
  623. }
  624. &__name {
  625. margin-top: 10px;
  626. }
  627. }
  628. .o-priority {
  629. display: inline-block;
  630. padding: 4px;
  631. font-size: $font-size--sm;
  632. border-radius: $radius--sm;
  633. & + & {
  634. margin-left: 4px;
  635. }
  636. }
  637. .o-program {
  638. display: inline-block;
  639. font-size: 0;
  640. border-radius: $radius--sm;
  641. background: rgba(#000, 0.8) url("~@/assets/program_bg.png") center center /
  642. 100% 100% no-repeat;
  643. }
  644. </style>