index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  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. <schedule-dialog ref="scheduleDialog" />
  179. </wrapper>
  180. </template>
  181. <script>
  182. import {
  183. getDevices,
  184. getTimeline
  185. } from '@/api/device'
  186. import { EventTarget } 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: 2,
  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.target.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.map(this.transformEvent).filter(({ until }) => !until || now < toDate(until)))
  303. this.calcEvents(options)
  304. },
  305. () => {
  306. options.error = true
  307. options.list = []
  308. }
  309. )
  310. },
  311. transformEvent (event) {
  312. return {
  313. ...event,
  314. time: getEventDescription(event),
  315. startDateTime: toDate(event.start),
  316. endDateTime: toDate(event.until),
  317. style: null,
  318. selected: false,
  319. img () {
  320. EventCache.getImage(this.target.type, this.target.id).then(img => {
  321. if (img) {
  322. this.style = {
  323. backgroundSize: 'contain',
  324. backgroundImage: `url("${img}")`
  325. }
  326. }
  327. })
  328. }
  329. }
  330. },
  331. transformEvents (events) {
  332. const map = {}
  333. for (let i = 0; i < events.length; i++) {
  334. const event = events[i]
  335. if (!map[event.priority]) {
  336. map[event.priority] = []
  337. }
  338. map[event.priority].push(event)
  339. }
  340. return Object.keys(map)
  341. .sort((a, b) => a > b ? -1 : 1)
  342. .map(key => map[key].sort((a, b) => toDate(a.start) - toDate(b.start)))
  343. },
  344. isDisableDate (date) {
  345. const now = new Date()
  346. const min = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  347. const max = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 30)
  348. return date < min || date > max
  349. },
  350. initTimes (date) {
  351. const maxDate = pickMax(toDate(date), new Date())
  352. this.current = toZeroPoint(maxDate)
  353. this.startHour = Math.min(20, maxDate.getHours())
  354. this.refreshTimes(this.startHour)
  355. },
  356. refreshTimes (start) {
  357. const times = []
  358. for (let i = 0; i < 5; i++) {
  359. times.push(`${(start + i).toString().padStart(2, '0')}:00`)
  360. }
  361. this.times = times
  362. this.$startDateTime = toDate(this.current.getTime() + start * 3600000)
  363. this.$endDateTime = toDate(this.current.getTime() + (start + 4) * 3600000)
  364. this.calcLine()
  365. this.refreshTimeline()
  366. },
  367. calcLine () {
  368. const now = Date.now()
  369. this.canPrevious = now < this.$startDateTime
  370. if (now < this.$startDateTime || now >= this.$endDateTime) {
  371. this.style = null
  372. } else {
  373. this.style = {
  374. left: `${Math.min(100, (now - this.$startDateTime) / 144000)}%`
  375. }
  376. }
  377. },
  378. refreshTimeline () {
  379. this.deviceOptions.list.forEach(device => {
  380. this.calcEvents(device.options)
  381. })
  382. },
  383. offsetTime (offset) {
  384. const next = this.startHour + offset
  385. if (offset < 0) {
  386. const timestamp = this.current.getTime() + next * 3600000
  387. const now = new Date()
  388. now.setMinutes(0)
  389. now.setSeconds(0)
  390. now.setMilliseconds(0)
  391. if (timestamp >= now) {
  392. if (next < 0) {
  393. this.initTimes(timestamp)
  394. } else {
  395. this.refreshTimes(this.startHour = next)
  396. }
  397. }
  398. } else if (next > 20) {
  399. this.initTimes(this.$endDateTime)
  400. } else {
  401. this.refreshTimes(this.startHour = next)
  402. }
  403. },
  404. onTimeChange (val) {
  405. this.initTimes(val)
  406. },
  407. calcSamePriorityEvents (events) {
  408. const total = 144000
  409. const arr = []
  410. for (let i = 0; i < events.length; i++) {
  411. const event = events[i]
  412. const { startDateTime, endDateTime } = event
  413. if (endDateTime && endDateTime <= this.$startDateTime || startDateTime >= this.$endDateTime) {
  414. continue
  415. }
  416. const hit = getNearestHitDate(event, this.$startDateTime, this.$endDateTime)
  417. if (hit) {
  418. const startDate = getStartDate(event, hit)
  419. const endDate = getFinishDate(event, hit)
  420. arr.push({
  421. key: event.target.id,
  422. event,
  423. time: `${toDateStr(startDate)} ${toTimeStr(startDate)} - ${toDateStr(endDate)} ${toTimeStr(endDate)}`,
  424. style: {
  425. left: `${(hit - this.$startDateTime) / total}%`,
  426. width: `${(pickMin(this.$endDateTime, getFinishDate(event, hit)) - hit) / total}%`,
  427. zIndex: event.priority
  428. }
  429. })
  430. event.img?.()
  431. if (endDate >= this.$endDateTime) {
  432. break
  433. }
  434. }
  435. }
  436. return arr
  437. },
  438. calcEvents (options) {
  439. if (options.loading || options.error) {
  440. return
  441. }
  442. options.list = options.events.map(this.calcSamePriorityEvents).filter(events => events.length)
  443. },
  444. chooseProgramProxy (device, programProxy) {
  445. this.device = device
  446. if (this.programProxy) {
  447. this.programProxy.event.selected = false
  448. }
  449. if (programProxy) {
  450. programProxy.event.selected = true
  451. }
  452. this.programProxy = programProxy
  453. },
  454. onView () {
  455. if (this.programProxy) {
  456. switch (this.programProxy.event.target.type) {
  457. case EventTarget.PROGRAM:
  458. this.$viewProgram(this.programProxy.event.target.id)
  459. break
  460. case EventTarget.RECUR:
  461. this.$refs.scheduleDialog.show(this.programProxy.event.target.id)
  462. break
  463. default:
  464. break
  465. }
  466. }
  467. }
  468. }
  469. }
  470. </script>
  471. <style lang="scss" scoped>
  472. .c-device-detail {
  473. &__screen {
  474. width: 352px;
  475. margin-right: 24px;
  476. }
  477. &__name {
  478. color: $black;
  479. font-size: 20px;
  480. font-weight: bold;
  481. line-height: 1;
  482. }
  483. &__program {
  484. margin-top: 20px;
  485. color: $blue;
  486. font-size: 20px;
  487. font-weight: bold;
  488. }
  489. &__time {
  490. margin-top: $spacing;
  491. color: $info--dark;
  492. font-size: 20px;
  493. }
  494. }
  495. .c-timeline {
  496. min-height: 400px;
  497. &__main {
  498. flex: 0 1 auto;
  499. min-height: 0;
  500. border: 1px solid $border;
  501. }
  502. &__row {
  503. & + & {
  504. border-top: 1px solid $border;
  505. }
  506. }
  507. &__row.header {
  508. .c-timeline__left {
  509. border-right: none;
  510. }
  511. }
  512. &__row.header &__right {
  513. color: $black;
  514. user-select: none;
  515. background-color: transparent;
  516. }
  517. &__row.selected &__left {
  518. color: #fff;
  519. background-color: #9fbfe8;
  520. }
  521. &__row.selected &__left::after {
  522. content: "";
  523. position: absolute;
  524. top: 0;
  525. left: 0;
  526. bottom: 0;
  527. width: 4px;
  528. background-color: $blue;
  529. }
  530. &__left {
  531. display: inline-flex;
  532. align-items: center;
  533. width: 210px;
  534. padding: 0 10px 0 20px;
  535. margin-right: 1px;
  536. color: $black;
  537. font-size: 16px;
  538. background-color: #fafbfc;
  539. border-right: 1px solid $border;
  540. }
  541. &__right {
  542. position: relative;
  543. color: $info--dark;
  544. font-size: 14px;
  545. line-height: 1;
  546. overflow: hidden;
  547. }
  548. &__programs {
  549. box-sizing: content-box;
  550. height: 54px;
  551. & + & {
  552. border-top: 1px dashed $border;
  553. }
  554. }
  555. &__arrow {
  556. visibility: hidden;
  557. position: absolute;
  558. top: 50%;
  559. padding: 6px;
  560. color: #fff;
  561. font-size: 12px;
  562. border-radius: $radius--mini;
  563. background-color: $blue;
  564. transform: translateY(-50%);
  565. &.left {
  566. left: 0;
  567. }
  568. &.right {
  569. right: 0;
  570. }
  571. &.display {
  572. visibility: visible;
  573. }
  574. }
  575. &__time {
  576. justify-content: space-around;
  577. padding: 10px 0;
  578. color: $black;
  579. font-size: 14px;
  580. }
  581. &__line {
  582. position: absolute;
  583. top: -12px;
  584. left: 210px;
  585. right: 0;
  586. bottom: 0;
  587. pointer-events: none;
  588. }
  589. &__mask {
  590. position: absolute;
  591. top: 0;
  592. left: 0;
  593. bottom: 0;
  594. color: #ff0000;
  595. border-right: 1px solid currentColor;
  596. z-index: 999;
  597. &::after {
  598. content: "";
  599. position: absolute;
  600. top: 0;
  601. right: -5px;
  602. width: 9px;
  603. height: 9px;
  604. border-radius: 50%;
  605. background-color: currentColor;
  606. }
  607. }
  608. }
  609. .c-event-program {
  610. position: absolute;
  611. top: 0;
  612. bottom: 0;
  613. font-size: 12px;
  614. overflow: hidden;
  615. &:hover {
  616. color: #fff;
  617. background-color: rgba($blue, 0.4);
  618. }
  619. &.selected {
  620. color: #fff;
  621. background-color: $blue;
  622. z-index: 99;
  623. }
  624. &__img {
  625. width: 96px;
  626. margin-right: 8px;
  627. }
  628. &__name {
  629. margin-top: 10px;
  630. }
  631. }
  632. .o-priority {
  633. display: inline-block;
  634. padding: 4px;
  635. font-size: 12px;
  636. border-radius: $radius--mini;
  637. & + & {
  638. margin-left: 4px;
  639. }
  640. }
  641. .priority1 {
  642. color: #8e929c;
  643. background-color: #edf0f6;
  644. }
  645. .priority2 {
  646. color: #7642fd;
  647. background-color: #eae2fe;
  648. }
  649. .priority3 {
  650. color: #ff2222;
  651. background-color: #ffecec;
  652. }
  653. .o-program {
  654. display: inline-block;
  655. font-size: 0;
  656. border-radius: $radius--mini;
  657. background: rgba(#000, 0.8) url("~@/assets/program_bg.png") center center /
  658. 100% 100% no-repeat;
  659. &::before {
  660. content: "";
  661. display: inline-block;
  662. padding-top: 56.25%;
  663. }
  664. }
  665. </style>