index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  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 priority3">高</span>
  60. <span class="o-priority priority2">中</span>
  61. <span class="o-priority 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--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 }, `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.target.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. <div
  157. v-show="style"
  158. class="c-timeline__line"
  159. >
  160. <div
  161. class="c-timeline__mask"
  162. :style="style"
  163. />
  164. </div>
  165. </div>
  166. <status-wrapper
  167. v-if="isAbnormal"
  168. :error="deviceOptions.error"
  169. @click="getDevices"
  170. />
  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. <program-dialog ref="programDialog" />
  179. <schedule-dialog ref="scheduleDialog" />
  180. </wrapper>
  181. </template>
  182. <script>
  183. import {
  184. getDevices,
  185. getTimeline
  186. } from '@/api/device'
  187. import { EventTarget } from '@/constant'
  188. import {
  189. toDate,
  190. toDateStr,
  191. toTimeStr,
  192. toZeroPoint,
  193. getNearestHitDate,
  194. getStartDate,
  195. getFinishDate,
  196. pickMin,
  197. pickMax,
  198. getEventDescription
  199. } from '@/utils/event'
  200. import { EventCache } from '@/utils/cache'
  201. import { createListOptions } from '@/utils'
  202. export default {
  203. name: 'ScheduleTimeline',
  204. data () {
  205. return {
  206. deviceOptions: createListOptions({
  207. name: '',
  208. activate: 2,
  209. pageSize: 5
  210. }),
  211. style: null,
  212. canPrevious: false,
  213. startHour: 0,
  214. times: [],
  215. current: null,
  216. device: null,
  217. programProxy: null
  218. }
  219. },
  220. computed: {
  221. deviceId () {
  222. return this.device?.id
  223. },
  224. deivceName () {
  225. return this.device?.name
  226. },
  227. programName () {
  228. return this.programProxy?.event.target.name
  229. },
  230. programTime () {
  231. return this.programProxy?.event.time
  232. },
  233. programStyle () {
  234. return this.programProxy?.event.style
  235. },
  236. isAbnormal () {
  237. const deviceOptions = this.deviceOptions
  238. return deviceOptions.error || !deviceOptions.loading && deviceOptions.totalCount === 0
  239. },
  240. pickerOptions () {
  241. return {
  242. disabledDate: this.isDisableDate
  243. }
  244. }
  245. },
  246. created () {
  247. this.initTimes(new Date())
  248. this.getDevices()
  249. this.$timer = setInterval(this.calcLine, 1000)
  250. },
  251. beforeDestroy () {
  252. clearInterval(this.$timer)
  253. },
  254. methods: {
  255. getDevices () {
  256. this.device = null
  257. if (this.programProxy) {
  258. this.programProxy.event.selected = false
  259. }
  260. this.programProxy = null
  261. const options = this.deviceOptions
  262. options.error = false
  263. options.loading = true
  264. getDevices(options.params).then(({ data, totalCount }) => {
  265. options.list = data.map(this.transform)
  266. options.totalCount = totalCount
  267. options.list.forEach(this.getTimeline)
  268. }, () => {
  269. options.error = true
  270. options.list = []
  271. }).finally(() => {
  272. options.loading = false
  273. })
  274. },
  275. search () {
  276. const options = this.deviceOptions
  277. options.list = []
  278. options.totalCount = 0
  279. options.params.pageNum = 1
  280. this.getDevices()
  281. },
  282. transform (device) {
  283. const { id, name } = device
  284. return {
  285. id, name,
  286. options: {
  287. loading: true,
  288. error: false,
  289. events: [],
  290. list: []
  291. }
  292. }
  293. },
  294. getTimeline (device) {
  295. const options = device.options
  296. options.error = false
  297. options.loading = true
  298. getTimeline(device.id, { custom: true }).finally(() => {
  299. options.loading = false
  300. }).then(
  301. events => {
  302. const now = Date.now()
  303. options.events = this.transformEvents(events.map(this.transformEvent).filter(({ until }) => !until || now < toDate(until)))
  304. this.calcEvents(options)
  305. },
  306. () => {
  307. options.error = true
  308. options.list = []
  309. }
  310. )
  311. },
  312. transformEvent (event) {
  313. return {
  314. ...event,
  315. time: getEventDescription(event),
  316. startDateTime: toDate(event.start),
  317. endDateTime: toDate(event.until),
  318. style: null,
  319. selected: false,
  320. img () {
  321. EventCache.getImage(this.target.type, this.target.id).then(img => {
  322. if (img) {
  323. this.style = {
  324. backgroundSize: 'contain',
  325. backgroundImage: `url("${img}")`
  326. }
  327. }
  328. })
  329. }
  330. }
  331. },
  332. transformEvents (events) {
  333. const map = {}
  334. for (let i = 0; i < events.length; i++) {
  335. const event = events[i]
  336. if (!map[event.priority]) {
  337. map[event.priority] = []
  338. }
  339. map[event.priority].push(event)
  340. }
  341. return Object.keys(map)
  342. .sort((a, b) => a > b ? -1 : 1)
  343. .map(key => map[key].sort((a, b) => toDate(a.start) - toDate(b.start)))
  344. },
  345. isDisableDate (date) {
  346. const now = new Date()
  347. const min = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  348. const max = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 30)
  349. return date < min || date > max
  350. },
  351. initTimes (date) {
  352. const maxDate = pickMax(toDate(date), new Date())
  353. this.current = toZeroPoint(maxDate)
  354. this.startHour = Math.min(20, maxDate.getHours())
  355. this.refreshTimes(this.startHour)
  356. },
  357. refreshTimes (start) {
  358. const times = []
  359. for (let i = 0; i < 5; i++) {
  360. times.push(`${(start + i).toString().padStart(2, '0')}:00`)
  361. }
  362. this.times = times
  363. this.$startDateTime = toDate(this.current.getTime() + start * 3600000)
  364. this.$endDateTime = toDate(this.current.getTime() + (start + 4) * 3600000)
  365. this.calcLine()
  366. this.refreshTimeline()
  367. },
  368. calcLine () {
  369. const now = Date.now()
  370. this.canPrevious = now < this.$startDateTime
  371. if (now < this.$startDateTime || now >= this.$endDateTime) {
  372. this.style = null
  373. } else {
  374. this.style = {
  375. left: `${Math.min(100, (now - this.$startDateTime) / 144000)}%`
  376. }
  377. }
  378. },
  379. refreshTimeline () {
  380. this.deviceOptions.list.forEach(device => {
  381. this.calcEvents(device.options)
  382. })
  383. },
  384. offsetTime (offset) {
  385. const next = this.startHour + offset
  386. if (offset < 0) {
  387. const timestamp = this.current.getTime() + next * 3600000
  388. const now = new Date()
  389. now.setMinutes(0)
  390. now.setSeconds(0)
  391. now.setMilliseconds(0)
  392. if (timestamp >= now) {
  393. if (next < 0) {
  394. this.initTimes(timestamp)
  395. } else {
  396. this.refreshTimes(this.startHour = next)
  397. }
  398. }
  399. } else if (next > 20) {
  400. this.initTimes(this.$endDateTime)
  401. } else {
  402. this.refreshTimes(this.startHour = next)
  403. }
  404. },
  405. onTimeChange (val) {
  406. this.initTimes(val)
  407. },
  408. calcSamePriorityEvents (events) {
  409. const total = 144000
  410. const arr = []
  411. for (let i = 0; i < events.length; i++) {
  412. const event = events[i]
  413. const { startDateTime, endDateTime } = event
  414. if (endDateTime && endDateTime <= this.$startDateTime || startDateTime >= this.$endDateTime) {
  415. continue
  416. }
  417. const hit = getNearestHitDate(event, this.$startDateTime, this.$endDateTime)
  418. if (hit) {
  419. const startDate = getStartDate(event, hit)
  420. const endDate = getFinishDate(event, hit)
  421. arr.push({
  422. key: event.target.id,
  423. event,
  424. time: `${toDateStr(startDate)} ${toTimeStr(startDate)} - ${toDateStr(endDate)} ${toTimeStr(endDate)}`,
  425. style: {
  426. left: `${(hit - this.$startDateTime) / total}%`,
  427. width: `${(pickMin(this.$endDateTime, getFinishDate(event, hit)) - hit) / total}%`,
  428. zIndex: event.priority
  429. }
  430. })
  431. event.img?.()
  432. if (endDate >= this.$endDateTime) {
  433. break
  434. }
  435. }
  436. }
  437. return arr
  438. },
  439. calcEvents (options) {
  440. if (options.loading || options.error) {
  441. return
  442. }
  443. options.list = options.events.map(this.calcSamePriorityEvents).filter(events => events.length)
  444. },
  445. chooseProgramProxy (device, programProxy) {
  446. this.device = device
  447. if (this.programProxy) {
  448. this.programProxy.event.selected = false
  449. }
  450. if (programProxy) {
  451. programProxy.event.selected = true
  452. }
  453. this.programProxy = programProxy
  454. },
  455. onView () {
  456. if (this.programProxy) {
  457. switch (this.programProxy.event.target.type) {
  458. case EventTarget.PROGRAM:
  459. this.$refs.programDialog.show(this.programProxy.event.target.id)
  460. break
  461. case EventTarget.RECUR:
  462. this.$refs.scheduleDialog.show(this.programProxy.event.target.id)
  463. break
  464. default:
  465. break
  466. }
  467. }
  468. }
  469. }
  470. }
  471. </script>
  472. <style lang="scss" scoped>
  473. .c-device-detail {
  474. &__screen {
  475. width: 352px;
  476. margin-right: 24px;
  477. }
  478. &__name {
  479. color: $black;
  480. font-size: 20px;
  481. font-weight: bold;
  482. line-height: 1;
  483. }
  484. &__program {
  485. margin-top: 20px;
  486. color: $blue;
  487. font-size: 20px;
  488. font-weight: bold;
  489. }
  490. &__time {
  491. margin-top: $spacing;
  492. color: $info--dark;
  493. font-size: 20px;
  494. }
  495. }
  496. .c-timeline {
  497. min-height: 400px;
  498. &__main {
  499. flex: 0 1 auto;
  500. min-height: 0;
  501. border: 1px solid $border;
  502. }
  503. &__row {
  504. & + & {
  505. border-top: 1px solid $border;
  506. }
  507. }
  508. &__row.header {
  509. .c-timeline__left {
  510. border-right: none;
  511. }
  512. }
  513. &__row.header &__right {
  514. color: $black;
  515. user-select: none;
  516. background-color: transparent;
  517. }
  518. &__row.selected &__left {
  519. color: #fff;
  520. background-color: #9fbfe8;
  521. }
  522. &__row.selected &__left::after {
  523. content: "";
  524. position: absolute;
  525. top: 0;
  526. left: 0;
  527. bottom: 0;
  528. width: 4px;
  529. background-color: $blue;
  530. }
  531. &__left {
  532. display: inline-flex;
  533. align-items: center;
  534. width: 210px;
  535. padding: 0 10px 0 20px;
  536. margin-right: 1px;
  537. color: $black;
  538. font-size: 16px;
  539. background-color: #fafbfc;
  540. border-right: 1px solid $border;
  541. }
  542. &__right {
  543. position: relative;
  544. color: $info--dark;
  545. font-size: 14px;
  546. line-height: 1;
  547. overflow: hidden;
  548. }
  549. &__programs {
  550. box-sizing: content-box;
  551. height: 54px;
  552. & + & {
  553. border-top: 1px dashed $border;
  554. }
  555. }
  556. &__arrow {
  557. visibility: hidden;
  558. position: absolute;
  559. top: 50%;
  560. padding: 6px;
  561. color: #fff;
  562. font-size: 12px;
  563. border-radius: $radius--mini;
  564. background-color: $blue;
  565. transform: translateY(-50%);
  566. &.left {
  567. left: 0;
  568. }
  569. &.right {
  570. right: 0;
  571. }
  572. &.display {
  573. visibility: visible;
  574. }
  575. }
  576. &__time {
  577. justify-content: space-around;
  578. padding: 10px 0;
  579. color: $black;
  580. font-size: 14px;
  581. }
  582. &__line {
  583. position: absolute;
  584. top: -12px;
  585. left: 210px;
  586. right: 0;
  587. bottom: 0;
  588. pointer-events: none;
  589. }
  590. &__mask {
  591. position: absolute;
  592. top: 0;
  593. left: 0;
  594. bottom: 0;
  595. color: #ff0000;
  596. border-right: 1px solid currentColor;
  597. z-index: 999;
  598. &::after {
  599. content: "";
  600. position: absolute;
  601. top: 0;
  602. right: -5px;
  603. width: 9px;
  604. height: 9px;
  605. border-radius: 50%;
  606. background-color: currentColor;
  607. }
  608. }
  609. }
  610. .c-event-program {
  611. position: absolute;
  612. top: 0;
  613. bottom: 0;
  614. font-size: 12px;
  615. overflow: hidden;
  616. &:hover {
  617. color: #fff;
  618. background-color: rgba($blue, 0.4);
  619. }
  620. &.selected {
  621. color: #fff;
  622. background-color: $blue;
  623. z-index: 99;
  624. }
  625. &__img {
  626. width: 96px;
  627. margin-right: 8px;
  628. }
  629. &__name {
  630. margin-top: 10px;
  631. }
  632. }
  633. .o-priority {
  634. display: inline-block;
  635. padding: 4px;
  636. font-size: 12px;
  637. border-radius: $radius--mini;
  638. & + & {
  639. margin-left: 4px;
  640. }
  641. }
  642. .priority1 {
  643. color: #8e929c;
  644. background-color: #edf0f6;
  645. }
  646. .priority2 {
  647. color: #7642fd;
  648. background-color: #eae2fe;
  649. }
  650. .priority3 {
  651. color: #ff2222;
  652. background-color: #ffecec;
  653. }
  654. .o-program {
  655. display: inline-block;
  656. font-size: 0;
  657. border-radius: $radius--mini;
  658. background: rgba(#000, 0.8) url("~@/assets/program_bg.png") center center /
  659. 100% 100% no-repeat;
  660. &::before {
  661. content: "";
  662. display: inline-block;
  663. padding-top: 56.25%;
  664. }
  665. }
  666. </style>