当前位置:   article > 正文

时间轴播放插件_vue 时间轴播放插件

vue 时间轴播放插件

time-slider/index.vue

  1. <template>
  2. <div class="sutpc-play-slider">
  3. <div class="play-wrap">
  4. <icon-svg
  5. name="play"
  6. @click.native="play"
  7. v-if="!isPlaying"
  8. />
  9. <icon-svg
  10. name="pause"
  11. @click.native="pause"
  12. v-else
  13. />
  14. <img
  15. class="hidden"
  16. src="./svgs/play.svg"
  17. />
  18. <img
  19. class="hidden"
  20. src="./svgs/pause.svg"
  21. />
  22. <div class="slider-wrap">
  23. <time-slider
  24. ref="slider"
  25. :interFilter="interFilter"
  26. :interFormat="interFormat"
  27. :tooltipFormat="tooltipFormat"
  28. v-model="currentValue"
  29. @move="sliderMoveHandler"
  30. :sliderData="sliderData"
  31. ></time-slider>
  32. </div>
  33. </div>
  34. </div>
  35. </template>
  36. <script>
  37. import TimeSlider from "./components/slider";
  38. export default {
  39. props: {
  40. //时间间隔为5分钟
  41. timePrecision: {
  42. type: Number,
  43. default: 5
  44. },
  45. playData: {
  46. type: Array,
  47. default: function () {
  48. return [];
  49. }
  50. },
  51. value: {
  52. type: Number,
  53. default: 0
  54. },
  55. sliderData: {
  56. type: Array,
  57. default: function () {
  58. return [];
  59. }
  60. },
  61. playInterval: {
  62. type: Number,
  63. default: 2000
  64. }
  65. },
  66. components: {
  67. TimeSlider
  68. },
  69. data() {
  70. return {
  71. currentValue: this.value,
  72. isPlaying: false,
  73. currentPlayIndex: 0,
  74. initValue: this.value
  75. }
  76. },
  77. watch: {
  78. value: {
  79. handler() {
  80. if (this.currentValue != this.value) {
  81. this.initValue = this.value;
  82. }
  83. this.currentValue = this.value;
  84. let currentPlayIndex = this.enabledSliderIndexArray.indexOf(this.currentValue);
  85. // 添加判断,初始化时enabledSliderIndexArray为空,currentPlayIndex为-1
  86. this.currentPlayIndex = currentPlayIndex == -1 ? 0 : currentPlayIndex;
  87. },
  88. immediate: true
  89. },
  90. currentValue() {
  91. this.$emit("input", this.currentValue);
  92. }
  93. },
  94. computed: {
  95. enabledSliderIndexArray() {
  96. let indexArray = [];
  97. this.sliderData.forEach(item => {
  98. if (item.type == 'disabled') {
  99. return;
  100. }
  101. for (let i = item.from; i <= item.to; i++) {
  102. indexArray.push(i);
  103. }
  104. });
  105. return indexArray;
  106. },
  107. currentTime() {
  108. let showValue = this.currentValue;
  109. if (this.dataFormat) {
  110. showValue = this.dataFormat(showValue,
  111. this.$refs.slider &&
  112. this.$refs.slider.getValueType &&
  113. this.$refs.slider.getValueType(showValue));
  114. }
  115. return `${showValue}`;
  116. },
  117. interFilter: ({ timePrecision }) => Object.seal(value => (value % ((60 / timePrecision) * 2) === 0 || value === 1)),
  118. interFormat: ({ timePrecision }) => Object.seal(value => {
  119. if ((value % ((60 / timePrecision) * 4) === 0)) {
  120. return `${Math.floor(value / (60 / timePrecision)).toString().padStart(2, '0')}:${(value % (60 / timePrecision) * timePrecision).toString().padStart(2, '0')}`;
  121. }
  122. return ''
  123. }),
  124. tooltipFormat: ({ timePrecision }) => Object.seal(value => `${Math.floor(value / (60 / timePrecision)).toString().padStart(2, '0')}:${(value % (60 / timePrecision) * timePrecision).toString().padStart(2, '0')}`),
  125. dataFormat: ({ timePrecision }) => Object.seal(value => `${Math.floor(value / (60 / timePrecision)).toString().padStart(2, '0')}:${(value % (60 / timePrecision) * timePrecision).toString().padStart(2, '0')}`),
  126. },
  127. methods: {
  128. sliderMoveHandler() {
  129. this.currentPlayIndex = this.enabledSliderIndexArray.indexOf(this.currentValue);
  130. if (this.isPlaying) {
  131. clearInterval(this.playInter);
  132. this.play();
  133. }
  134. },
  135. play() {
  136. // 判断是否第一次播放并且是播放进度的最后一个时刻
  137. // 如果是则currentPlayIndex=0,直接从头播放
  138. if (this.isFirstTime == undefined && this.currentPlayIndex == this.enabledSliderIndexArray.length - 1) {
  139. this.currentPlayIndex = 0;
  140. this.isFirstTime = false;
  141. }
  142. this.isPlaying = true;
  143. this.currentValue = this.enabledSliderIndexArray[this.currentPlayIndex];
  144. this.playInter = setInterval(() => {
  145. this.currentPlayIndex++;
  146. if (this.currentPlayIndex >= this.enabledSliderIndexArray.length) {
  147. clearInterval(this.playInter);
  148. this.currentPlayIndex = 0;
  149. this.isPlaying = false;
  150. return;
  151. }
  152. this.currentValue = this.enabledSliderIndexArray[this.currentPlayIndex];
  153. }, this.playInterval);
  154. },
  155. pause() {
  156. this.isPlaying = false;
  157. clearInterval(this.playInter);
  158. }
  159. },
  160. beforeDestroy() {
  161. clearInterval(this.playInter);
  162. }
  163. }
  164. </script>
  165. <style lang="less" scoped>
  166. .hidden {
  167. display: none;
  168. }
  169. .sutpc-play-slider {
  170. padding: @side-gap @side-gap + 20px @side-gap @side-gap;
  171. color: @color-primary;
  172. display: flex;
  173. flex-direction: column;
  174. font-size: 12px;
  175. position: absolute;
  176. right: 0;
  177. left: 0;
  178. margin: auto;
  179. bottom: @side-gap;
  180. z-index: 1500;
  181. width: 680px;
  182. .panel-bg;
  183. .box-shadow;
  184. .play-wrap {
  185. display: flex;
  186. align-items: center;
  187. width: 100%;
  188. flex: 1;
  189. }
  190. .slider-wrap {
  191. flex: 1;
  192. width: 0;
  193. }
  194. .svg-icon {
  195. font-size: 26px;
  196. cursor: pointer;
  197. margin-right: @side-gap;
  198. }
  199. }
  200. </style>

components/slider.vue

  1. <template>
  2. <div
  3. class="sutpc-slider"
  4. @mouseleave="containerMouseleaveHandler"
  5. @mousedown="containerMousedownHandler"
  6. @mouseup="containerMouseupHandler"
  7. @mousemove="containerMousemoveHandler"
  8. ref="container"
  9. >
  10. <span
  11. v-for="(item, index) in sliderData"
  12. :key="index"
  13. :class="getItemClass(item)"
  14. :style="getItemStyle(item, index)"
  15. @mousedown="mousedownHandler(item, $event)"
  16. ></span>
  17. <div
  18. class="block"
  19. :style="getBlockStyle"
  20. >
  21. <div
  22. class="block-tooltip"
  23. v-text="tooltipContent"
  24. />
  25. </div>
  26. <div class="interval-wrap">
  27. <template v-for="inter in intervalArray">
  28. <span
  29. class="interval"
  30. :key="inter"
  31. v-if="interFilter ? interFilter(inter) : true"
  32. :style="getIntervalStyle(inter)"
  33. >
  34. {{ interFormat ? interFormat(inter, getValueType(inter)) : inter }}
  35. <i
  36. class="interval-tick"
  37. :class="getIntervalClass(inter)"
  38. />
  39. </span>
  40. </template>
  41. </div>
  42. </div>
  43. </template>
  44. <script>
  45. // v-model 当前所在的时间片
  46. // @change 选择的时间片变化时,回传出去
  47. export default {
  48. props: {
  49. step: {
  50. type: Number,
  51. default: 1
  52. },
  53. /**
  54. * [{from, to, type: 'predict|enabled|disabled'}]
  55. */
  56. sliderData: {
  57. type: Array,
  58. default: function () {
  59. return [];
  60. }
  61. },
  62. value: {
  63. type: Number,
  64. default: 0
  65. },
  66. interFilter: {
  67. type: Function,
  68. default: value => value
  69. },
  70. interFormat: {
  71. type: Function,
  72. default: value => value
  73. },
  74. tooltipFormat: {
  75. type: Function,
  76. default: value => value
  77. },
  78. debounce: {
  79. type: Number,
  80. default: 0
  81. },
  82. version: {
  83. type: [Number, String],
  84. default: 1
  85. }
  86. },
  87. data() {
  88. return {
  89. // 原始值,在绘制的时候需要除以step
  90. currentValue: this.value,
  91. // 是否鼠标按住中
  92. isMouseDown: false,
  93. }
  94. },
  95. computed: {
  96. tooltipContent() {
  97. return this.tooltipFormat && this.tooltipFormat(this.currentValue, this.getValueType(this.currentValue));
  98. },
  99. intervalArray() {
  100. let arr = [];
  101. for (let i = this.minValue; i <= this.maxValue; i++) {
  102. arr.push(i);
  103. }
  104. return arr;
  105. },
  106. maxValue() {
  107. let max = Number.NEGATIVE_INFINITY;
  108. this.sliderData.forEach(({ from, to, type }) => {
  109. max = Math.max(from, max, to);
  110. });
  111. return max;
  112. },
  113. minValue() {
  114. let min = Number.POSITIVE_INFINITY;
  115. this.sliderData.forEach(({ from, to, type }) => {
  116. min = Math.min(from, min, to);
  117. });
  118. return min;
  119. },
  120. totalInter() {
  121. return this.maxValue - this.minValue;
  122. },
  123. getBlockStyle() {
  124. if (this.currentValue === '' || this.currentValue < this.minValue) {
  125. return {
  126. display: 'none'
  127. }
  128. }
  129. return {
  130. left: ((this.currentValue - this.minValue) / this.totalInter) * 100 + '%'
  131. }
  132. },
  133. stepWidth() {
  134. return this.$refs.container.offsetWidth / (this.totalInter / this.step);
  135. }
  136. },
  137. methods: {
  138. getIntervalClass(interval) {
  139. const havePredict = this.sliderData.find(item => item.type === 'predict');
  140. const [{ to: enabled }] = this.sliderData;
  141. if (interval <= enabled + (havePredict ? 20 : 0)) {
  142. return 'enabled'
  143. } else {
  144. return 'disabled'
  145. }
  146. },
  147. getIntervalStyle(interval) {
  148. return {
  149. marginLeft: ((interval - this.minValue) / this.totalInter) * 100 + '%'
  150. }
  151. },
  152. getItemClass({ type }) {
  153. return {
  154. 'enabled': 'enabled',
  155. 'disabled': 'disabled',
  156. 'predict': 'predict'
  157. }[type || 'enabled'];
  158. },
  159. getItemStyle({ from, to }, index) {
  160. let itemWidth = null;
  161. // v1的数据区间是断节的,类似[{from: 0, to: 2}, {from: 3, to: 5}],经确认后认为该区间划分不合理,因此升级了v2
  162. if (this.version == 1) {
  163. // 因为区间的不连续,区间的宽度需要+1,例如0-2,则区间宽度是 2 - 0 + 1 * 单位宽度
  164. itemWidth = ((to - from + 1) / this.totalInter) * 100 + '%';
  165. // 最后一个区间则不能+1,否则最后会多出一段多余的
  166. if (index >= this.sliderData.length - 1) {
  167. itemWidth = ((to - from) / this.totalInter) * 100 + '%';
  168. }
  169. }
  170. // v2的数据区间格式如下:[{from: 0, to: 3}, {from:3, to:5}],2个区间之间是连续的
  171. else {
  172. itemWidth = ((to - from) / this.totalInter) * 100 + '%';
  173. }
  174. // 最前和最后一个区间,宽度加长,往左边和右边突出一点
  175. if (index <= 0 || index >= this.sliderData.length - 1) {
  176. return {
  177. width: `calc(${itemWidth} + 3px)`
  178. }
  179. }
  180. return {
  181. width: itemWidth
  182. }
  183. },
  184. mousedownHandler({ type }, e) {
  185. if (type === 'disabled') {
  186. e.stopPropagation();
  187. e.preventDefault();
  188. return false;
  189. }
  190. },
  191. containerMousedownHandler(e) {
  192. if (e.target === e.currentTarget) {
  193. return;
  194. }
  195. let { offsetWidth } = e.currentTarget;
  196. // 鼠标点击位置相对于滑动条的左边距离
  197. let offsetX = e.clientX - e.currentTarget.getBoundingClientRect().left;
  198. this.resetRealBlockPos(offsetX);
  199. this.isMouseDown = true;
  200. },
  201. containerMouseupHandler(e) {
  202. this.isMouseDown = false;
  203. },
  204. containerMousemoveHandler(e) {
  205. if (!this.isMouseDown) {
  206. return;
  207. }
  208. let { offsetWidth } = e.currentTarget;
  209. // 鼠标点击位置相对于滑动条的左边距离
  210. let offsetX = e.clientX - e.currentTarget.getBoundingClientRect().left;
  211. this.resetRealBlockPos(offsetX);
  212. },
  213. containerMouseleaveHandler() {
  214. this.isMouseDown = false;
  215. },
  216. resetRealBlockPos(offsetX) {
  217. let currentValue = offsetX / this.stepWidth * this.step + this.minValue;
  218. if (currentValue > this.maxValue || currentValue < this.minValue) {
  219. return;
  220. }
  221. // v1的数据区间是断节的,类似[{from: 0, to: 2}, {from: 3, to: 5}],经确认后认为该区间划分不合理,因此升级了v2
  222. if (this.version == 1) {
  223. for (let { from, to, type } of this.sliderData) {
  224. if (currentValue >= from &&
  225. currentValue <= to + 1 &&
  226. type === 'disabled') {
  227. return;
  228. }
  229. }
  230. }
  231. // v2的数据区间格式如下:[{from: 0, to: 3}, {from:3, to:5}],2个区间之间是连续的
  232. else {
  233. for (let { from, to, type } of this.sliderData) {
  234. if (currentValue >= from &&
  235. currentValue <= to &&
  236. type === 'disabled') {
  237. return;
  238. }
  239. }
  240. }
  241. this.currentValue = Math.round(currentValue);
  242. let currentType = this.getValueType(this.currentValue);
  243. this.$emit("move", {
  244. value: this.currentValue,
  245. type: currentType
  246. });
  247. },
  248. getValueType(value) {
  249. let valueType = '';
  250. for (let { from, to, type } of this.sliderData) {
  251. if (value >= from && value <= to) {
  252. valueType = type;
  253. }
  254. }
  255. return valueType;
  256. }
  257. },
  258. watch: {
  259. currentValue() {
  260. let currentType = this.getValueType(this.currentValue);
  261. if (this.debounce <= 0) {
  262. this.$emit("input", this.currentValue);
  263. this.$emit("change", {
  264. value: this.currentValue,
  265. type: currentType
  266. });
  267. }
  268. else {
  269. clearTimeout(this.debounceTimeout);
  270. this.debounceTimeout = setTimeout(() => {
  271. this.$emit("input", this.currentValue);
  272. this.$emit("change", {
  273. value: this.currentValue,
  274. type: currentType
  275. });
  276. }, this.debounce);
  277. }
  278. },
  279. value() {
  280. this.currentValue = this.value;
  281. }
  282. }
  283. }
  284. </script>
  285. <style lang="less" scoped>
  286. .sutpc-slider {
  287. position: relative;
  288. display: flex;
  289. justify-content: center;
  290. align-items: center;
  291. background: fadeout(@color-primary, 90%);
  292. height: 12px;
  293. > span {
  294. flex-shrink: 0;
  295. height: 3px;
  296. background: #ccc;
  297. box-sizing: border-box;
  298. &:first-of-type {
  299. border-top-left-radius: 3px;
  300. border-bottom-left-radius: 3px;
  301. }
  302. &:last-of-type {
  303. border-top-right-radius: 3px;
  304. border-bottom-right-radius: 3px;
  305. }
  306. &.enabled {
  307. cursor: pointer;
  308. }
  309. }
  310. .block {
  311. position: absolute;
  312. left: 0;
  313. top: 50%;
  314. transform: translateY(-50%) translateX(-50%);
  315. border-radius: 100%;
  316. user-select: none;
  317. transition: all 0.1s;
  318. cursor: grab;
  319. z-index: 1;
  320. height: 12px;
  321. width: 12px;
  322. box-sizing: border-box;
  323. border: none;
  324. background: @color-primary;
  325. .block-tooltip {
  326. position: absolute;
  327. left: 50%;
  328. top: -2 * @side-gap;
  329. transform: translateX(-50%);
  330. padding: 5px;
  331. .box-shadow;
  332. .panel-bg;
  333. border-radius: @border-radius;
  334. color: @text-color-regular;
  335. &::after {
  336. content: "";
  337. position: absolute;
  338. left: 50%;
  339. transform: translateX(-50%);
  340. bottom: -6px;
  341. border-right: 6px solid transparent;
  342. border-left: 6px solid transparent;
  343. border-top: 6px solid @color-primary;
  344. .box-shadow;
  345. }
  346. }
  347. }
  348. .interval-wrap {
  349. position: absolute;
  350. margin-top: 12px;
  351. left: 0;
  352. right: 0;
  353. top: 0;
  354. font-size: 12px;
  355. .interval {
  356. position: absolute;
  357. left: 0;
  358. top: 0;
  359. transform: translateX(-50%);
  360. white-space: nowrap;
  361. font-size: @font-size-small;
  362. color: @text-color-secondary;
  363. }
  364. .interval-tick {
  365. position: absolute;
  366. left: 50%;
  367. transform: translateX(-50%);
  368. .panel-bg;
  369. width: 12px;
  370. height: 12px;
  371. top: -12px;
  372. border-radius: 50%;
  373. box-sizing: border-box;
  374. border: 3px solid @color-primary;
  375. &.disabled {
  376. border-color: @text-color-placeholder;
  377. }
  378. }
  379. }
  380. }
  381. </style>
  382. <style lang="less">
  383. .slider-tooltip {
  384. border: none;
  385. .box-shadow;
  386. .popper__arrow {
  387. &::after {
  388. border-top-color: @color-primary !important;
  389. }
  390. }
  391. }
  392. .sutpc-slider {
  393. > span {
  394. &.enabled {
  395. background: @color-primary;
  396. }
  397. &.predict {
  398. background: linear-gradient(
  399. to right,
  400. @color-primary 50%,
  401. transparent 0
  402. );
  403. background-size: @side-gap-medium 100%;
  404. }
  405. }
  406. .block {
  407. border: 2px solid @color-primary;
  408. }
  409. .interval-wrap {
  410. color: @text-color-secondary;
  411. }
  412. }
  413. </style>

使用组件

  1. <template>
  2. <PlaySlider
  3. :sliderData="sliderData"
  4. v-model="currentPeriod"
  5. :timePrecision="5"
  6. :playInter="4000"
  7. :debounce="300"
  8. />
  9. </template>
  10. <script>
  11. import PlaySlider from 'components/time-slider';
  12. import { state, action } from '../store';
  13. // 播放器的最大时间
  14. const SLIDER_MAX_VALUE = 288;
  15. export default {
  16. components: {
  17. PlaySlider,
  18. },
  19. data: () => ({
  20. sliderData: [],
  21. }),
  22. computed: {
  23. //当前时间片
  24. currentPeriod: {
  25. get: () => state.currentPeriod,
  26. set: (value) => action.updateCurrentPeriod(value)
  27. },
  28. },
  29. watch: {
  30. currentPeriod(value) {
  31. //如果不是currentPeriod第一次设置则返回
  32. if (this.sliderData.length) {
  33. return;
  34. }
  35. this.sliderData = [{
  36. from: 1,
  37. to: value,
  38. type: 'enabled'
  39. }];
  40. if (value >= SLIDER_MAX_VALUE) {
  41. return;
  42. }
  43. let disabledStart = value + 1;
  44. if (disabledStart > SLIDER_MAX_VALUE) {
  45. return;
  46. }
  47. this.sliderData.push({
  48. from: disabledStart,
  49. to: SLIDER_MAX_VALUE,
  50. type: 'disabled'
  51. });
  52. //非响应式
  53. this.sliderData = Object.freeze(this.sliderData);
  54. }
  55. },
  56. }
  57. </script>

store.js

  1. export default {
  2. state: {
  3. //当前时间片
  4. currentPeriod: null,
  5. },
  6. mutations:{
  7. setCurrentPeriod(state, value) {
  8. state.currentPeriod = value;
  9. },
  10. },
  11. action:{
  12. updateCurrentPeriod({ commit }, newPeriod) {
  13. commit("setCurrentPeriod", newPeriod);
  14. }
  15. }
  16. };

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/130207
推荐阅读
相关标签
  

闽ICP备14008679号