index.vue 17 KB

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