实现一个视频播放刻度条
具体效果
思路
- 刻度点的实现
- 刻度条时间刻度
- 刻度条视频存在的时间刻度
- 刻度条滑块
刻度点的实现
由于刻度点比分块多1, 所以遍历的时候加上1。
<div class="point-wrapper">
<div
class="point-grip-item"
v-for="(_, index) in grid + 1"
:key="index"
:style="{
...pointerStyle,
height: index % 2 == 0 ? '10px' : '5px'
}"
></div>
</div>
刻度条时间刻度
通过计算获取需要展示的时间刻度数组。想要具体某一天,使用下述getDateArray方法,通过传入开始时间00:00:00与结束时间23:59:59来计算获取刻度数组, 时间间隔默认给120分钟,因为传入24:00:00的时间通过new Date转成时间戳会变成00:00:00的时间所以通过23:59:59代替,后面再转换过来。遍历绘制刻度,由于单位通过vw转换,无法确切知道时间刻度所占宽度,通过获取宽度去计算,让其居中显示。
<div class="time-number-wrapper" :style="timeNumStyle">
<div class="time-number-ct">
<div
class="time-number"
v-for="(_, index) in grid"
:key="index"
:style="{ width: `${single}px` }"
:class="{
'first-time-number': index == 0
}"
>
<span ref="timeRef" :style="timeStyle">{{
index == 0
? dateArr[0]
: (index + 1) % spaceNum == 0
? dateArr[Math.ceil((index + 1) / spaceNum)]
: ''
}}</span>
</div>
</div>
</div>
刻度条存在的时间刻度
计算存在视频的起始时间位置与所占长度
<!-- 存在视频的时间段绘制 -->
<div class="scale-active">
<div
class="scale-active-time"
v-for="(timeArr, index) in newActiveTime"
:key="index"
:style="{
left: `${timeArr[0]}px`
}"
>
<div class="scale-active-item" :style="{ width: timeArr[1] + 'px' }"> </div>
</div>
</div>
let curNewActiveTime: Array<any> = []
props.activeTime.forEach((item: any) => {
let t1: number = dateToGrid(item[0], grid.value)
let t2: number = dateToGrid(item[1], grid.value)
// 初始位置距左边距离
let left = t1 * single.value
// 时间在刻度上长度
let width: number = +(t2 - t1).toFixed(2) * single.value
curNewActiveTime.push([left, width])
})
newActiveTime.value = curNewActiveTime
/**
* 返回24小时内时间数组,默认30分钟分隔
* @param startDate
* @param endDate
* @param space 分钟 默认30
* @returns
*/
export function getDateArray(startDate: Date, endDate: Date, space: number) {
endDate = endDate || new Date()
startDate = startDate || new Date(new Date().getTime() - 60 * 60 * 1000)
space = (space || 30) * 60 * 1000
let endTime = endDate.getTime()
let startTime = startDate.getTime()
let mod = endTime - startTime
if (mod <= space) {
return
}
let dateArray = ['00:00']
while (mod >= space) {
let d = new Date()
startTime = startTime + space
d.setTime(startTime)
let newd = dateFormat(d, 'HH:mm')
dateArray.push(newd)
mod = mod - space
}
let newend = dateFormat(endDate, 'HH:mm')
dateArray.push(newend)
let dateArrs = dateArray.sort((a, b) => {
return Date.parse(a) - Date.parse(b)
})
if (dateArrs[dateArrs.length - 1] == '23:59') {
dateArrs[dateArrs.length - 1] = '24:00'
}
return dateArrs
}
刻度条滑块
通过left来控制位置。
<div
class="slider-btn"
:style="{ left: left + 'px' }"
ref="slideBar"
@pointerdown="startDrag"
@pointerup="stopDrag"
@pointerleave="stopDrag"
></div>
// 开始拖拽
let startX: number = 0
const startDrag = (event: any) => {
startX = event.clientX || event.touches[0].clientX
slideBar.value.addEventListener('pointermove', onDrag)
}
// 滚动
const onDrag = (event: any) => {
const currentX = event.clientX || event.touches[0].clientX
left.value += currentX - startX
startX = currentX
// 避免越界
if (left.value <= 0) {
left.value = 0
}
let parentWidth = single.value * grid.value
if (left.value >= parentWidth) {
left.value = parentWidth
}
// 选中的值
let value: string = (left.value / single.value).toFixed(2)
if (+value > grid.value) value = grid.value.toString()
let ct = +value * (86400 / grid.value)
value = secondToDate(ct)
// 滚动结束才返回时间点
timer && clearTimeout(timer)
timer = setTimeout(() => {
// 返回刻度所在可播放时间点
if (isInActiveTime(props.curDate, props.activeTime, value)) {
emits('active-value', value)
} else {
// 返回普通时间点
emits('value', value)
}
}, 500)
}
// 停止拖动
const stopDrag = () => {
slideBar.value.removeEventListener('pointermove', onDrag)
startX = 0
}
使用方式
可以通过传入playTime, 让滑块跳转到当前位置。
<videoSlider
:isAuto="true"
:activeTime="activeTime"
@value="getValue"
@active-value="getActiveValue"
></videoSlider>
<script setup lang="ts">
let activeTime = ref([
['03:00', '04:00'],
['06:00', '08:00'],
['09:00', '13:00']
])
// 滚动到无视频时间
function getValue(value: any) {
console.log('暂停:', value)
}
// 播放滚动到的视频
function getActiveValue(value: any) {
console.log('播放:', value)
}
</script>
所有代码如下
<template>
<div class="video-slider" :style="{ background: stylesObj.bgoutside }">
<div ref="scaleWrapper" class="horizontal-box">
<!-- 滑动按钮 -->
<div
class="slider-btn"
:style="{ left: left + 'px' }"
ref="slideBar"
@pointerdown="startDrag"
@pointerup="stopDrag"
@pointerleave="stopDrag"
></div>
<div class="scroll-wrapper">
<div class="scale-container" :style="ctStyle">
<!-- 存在视频的时间段绘制 -->
<div class="scale-active">
<div
class="scale-active-time"
v-for="(timeArr, index) in newActiveTime"
:key="index"
:style="{
left: `${timeArr[0]}px`
}"
>
<div class="scale-active-item" :style="{ width: timeArr[1] + 'px' }"></div>
</div>
</div>
<!-- 刻度点 -->
<div class="point-wrapper">
<div
class="point-grip-item"
v-for="(_, index) in grid + 1"
:key="index"
:style="{
...pointerStyle,
height: index % 2 == 0 ? '10px' : '5px'
}"
></div>
</div>
<!-- 标尺数时间显示,长度:每格长度*个数 -->
<div class="time-number-wrapper" :style="timeNumStyle">
<div class="time-number-ct">
<div
class="time-number"
v-for="(_, index) in grid"
:key="index"
:style="{ width: `${single}px` }"
:class="{
'first-time-number': index == 0
}"
>
<span ref="timeRef" :style="timeStyle">{{
index == 0
? dateArr[0]
: (index + 1) % spaceNum == 0
? dateArr[Math.ceil((index + 1) / spaceNum)]
: ''
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
import { getDateArray, dateToGrid, secondToDate, isInActiveTime, dateFormat } from '@/utils/video'
const props = withDefaults(
defineProps<{
curDate?: string
activeTime?: Array<Array<string>> // 存在视频的时间数组
styles?: any // 自定义样式
isAuto?: boolean // 自适应宽度
playTime?: Array<string> // 当前播放时间
spaceTime?: number //显示刻度的间隔时间
}>(),
{
curDate: dateFormat(new Date(), 'YYYY-MM-DD'),
activeTime: () => [],
styles: () => ({}),
isAuto: false,
playTime: () => ['00:00:00', '00:00:00'],
spaceTime: 120
}
)
const emits = defineEmits<{
(e: 'value', val: string): void
(e: 'active-value', val: string): void
}>()
const scaleWrapper = ref()
const slideBar = ref()
const timeRef = ref()
let defaultStyles = ref({
line: '#dbdbdb', // 刻度颜色
bginner: '#fbfbfb', // 前景色颜色
bgoutside: '#999', // 背景色颜色
lineSelect: '#ea3639', // 选中线颜色
fontColor: '#fff', // 刻度数字颜色
fontSize: 12 // 字体大小
})
let single = ref(14) // 每个格子的实际行度(单位px ,相对默认值)
let grid = ref<number>(48) // 多少格
let stylesObj = ref<any>({}) // 样式
let dateArr = ref<Array<any>>([]) // 刻度上时间数组,30分钟分隔
let newActiveTime = ref<Array<any>>([]) // 可播放时间
let left = ref<number>(0) // 滚动位置
let timer: NodeJS.Timeout
let timeWidth = ref(0)
nextTick(() => {
watch(
() => props.playTime,
(newVal) => {
setPointLocation(newVal)
},
{
immediate: true
}
)
timeWidth.value = timeRef.value[0].offsetWidth
})
// 标尺宽度
const ctStyle = computed(() => {
return {
width: single.value * grid.value + 'px'
}
})
// 刻度点样式
const pointerStyle = computed(() => {
return {
width: single.value + 'px',
borderColor: stylesObj.value.line
}
})
// 刻度时间样式
const timeNumStyle = computed(() => {
return {
color: stylesObj.value.fontColor,
width: single.value * grid.value + 'px',
fontSize: stylesObj.value.fontSize + 'px'
}
})
// 空几格显示刻度时间
const spaceNum = computed(() => {
return ~~(props.spaceTime / 30)
})
// 时间刻度点居中
const timeStyle = computed(() => {
let offset = (single.value * 2 - timeWidth.value) / 2
return {
marginLeft: `${offset}px`
}
})
onMounted(() => {
init()
})
onUnmounted(() => {
stopDrag()
})
function init() {
// 初始化
let startTime = new Date(props.curDate + ' 00:00:00')
let endTime = new Date(props.curDate + ' 23:59:59')
dateArr.value = getDateArray(startTime, endTime, props.spaceTime) || []
// 宽度自适应
if (props.isAuto) {
let parentWidth = scaleWrapper.value.parentNode.offsetWidth
single.value = (parentWidth - 38) / grid.value
}
let curNewActiveTime: Array<any> = []
props.activeTime.forEach((item: any) => {
let t1: number = dateToGrid(item[0], grid.value)
let t2: number = dateToGrid(item[1], grid.value)
// 初始位置距左边距离
let left = t1 * single.value
// 时间在刻度上长度
let width: number = +(t2 - t1).toFixed(2) * single.value
curNewActiveTime.push([left, width])
})
newActiveTime.value = curNewActiveTime
stylesObj = Object.assign(defaultStyles, props.styles)
}
// 开始拖拽
let startX: number = 0
const startDrag = (event: any) => {
startX = event.clientX || event.touches[0].clientX
slideBar.value.addEventListener('pointermove', onDrag)
}
// 滚动
const onDrag = (event: any) => {
const currentX = event.clientX || event.touches[0].clientX
left.value += currentX - startX
// 避免越界
if (left.value <= 0) {
left.value = 0
}
let parentWidth = single.value * grid.value
if (left.value >= parentWidth) {
left.value = parentWidth
}
startX = currentX
// 选中的值
let value: string = (left.value / single.value).toFixed(2)
if (+value > grid.value) value = grid.value.toString()
let ct = +value * (86400 / grid.value)
value = secondToDate(ct)
// 滚动结束才返回时间点
timer && clearTimeout(timer)
timer = setTimeout(() => {
// 返回刻度所在可播放时间点
if (isInActiveTime(props.curDate, props.activeTime, value)) {
emits('active-value', value)
} else {
// 返回普通时间点
emits('value', value)
}
}, 500)
}
// 停止拖动
const stopDrag = () => {
slideBar.value.removeEventListener('pointermove', onDrag)
startX = 0
}
// 定位滑动位置
function setPointLocation(timeList: Array<string>) {
let t: number = dateToGrid(timeList[0], grid.value)
let t2: number = dateToGrid(timeList[1], grid.value)
left.value = (t + (t2 - t) / 2) * single.value
}
</script>
<style lang="scss" scoped>
div,
text {
box-sizing: border-box;
}
.video-slider {
padding-left: 18px;
.horizontal-box {
position: relative;
padding-top: 2px;
height: 40px;
.slider-btn {
position: absolute;
width: 24px;
height: 24px;
border-radius: 50%;
border: 9px solid #00b4e2;
z-index: 10;
background-color: #fff;
top: 2px;
transform: translateX(-50%);
cursor: grab;
}
.point-wrapper {
display: flex;
border-top: 1px solid #dddddd;
z-index: 6;
}
.point-grip {
position: relative;
height: 50px;
display: flex;
&::before {
content: '';
position: absolute;
top: 0;
border-width: 1px;
border-color: inherit;
border-style: solid;
height: 100%;
transform: translateX(-50%);
left: 0px;
}
&:last-child {
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
border-width: 1px;
border-color: inherit;
border-style: solid;
height: 100%;
}
}
}
.point-grip-item {
height: 9px;
padding-top: 5px;
border-right: 3px solid #fff;
&:first-of-type {
width: 0px !important;
}
}
.time-number-wrapper {
position: relative;
display: flex;
text-align: center;
z-index: 6;
.time-number-ct {
display: flex;
}
}
.time-number {
padding: 5px 0;
color: #fff;
font-size: 12px;
}
.first-time-number {
transform: translateX(-70%);
}
.scale-active {
position: relative;
height: 8px;
z-index: 5;
transform: translateY(100%);
background-color: #fff;
.scale-active-time {
position: absolute;
height: 100%;
.scale-active-item {
height: 100%;
background-color: #00b4e2;
}
}
}
}
}
</style>
/**
* 时间补0
* @param val
* @param len
* @returns
*/
function padStart(val: string, len: number) {
return val.padStart(len, '0')
}
/**
* 时间戳转时间格式
* @param date
* @param fmt
* @returns
*/
export function dateFormat(date: Date, fmt: string) {
let ret
const opt: Record<string, string> = {
'Y+': date.getFullYear().toString(),
'M+': (date.getMonth() + 1).toString(),
'D+': date.getDate().toString(),
'H+': date.getHours().toString(),
'm+': date.getMinutes().toString(),
's+': date.getSeconds().toString()
}
for (const k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt)
if (ret) {
fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : padStart(opt[k], ret[1].length))
}
}
return fmt
}
/**
* 秒转时间
* @param seconds
* @returns
*/
export function secondToDate(seconds: any) {
let h = padStart((~~(seconds / 3600)).toString(), 2)
let m = padStart((~~((seconds / 60) % 60)).toString(), 2)
let s = padStart((~~(seconds % 60)).toString(), 2)
return `${h}:${m}:${s}`
}
/**
* 时间转刻度位置
* @param date
* @param max 总共多少格刻度
* @returns 当前时间占多少格
*/
export function dateToGrid(date: string, max: number) {
let t: number = 0
let arr = date.split(':')
if (arr.length == 3) {
t = +arr[0] * 3600 + +arr[1] * 60 + +arr[2]
} else if (arr.length == 2) {
t = +arr[0] * 3600 + +arr[1] * 60
}
return (t / 86400) * max
}
/**
* 判断当前时间是否在时间段上
* @param curTime
* @param startTime
* @param endTime
* @returns
*/
function isInTimePeriod(curTime: string, startTime: string, endTime: string) {
let curDate = new Date(curTime),
beginDate = new Date(startTime),
endDate = new Date(endTime)
return curDate >= beginDate && curDate <= endDate
}
/**
* 判断当前时间是否在存在视频的时间上
* @param curDate
* @param activeTime
* @param value
* @returns
*/
export function isInActiveTime(curDate: string, activeTime: Array<Array<string>>, time: string) {
return activeTime.some((item: Array<string>) => {
let startTime = `${curDate} ${item[0]}`
let endTime = `${curDate} ${item[1]}`
let curTime = `${curDate} ${time}`
return isInTimePeriod(curTime, startTime, endTime)
})
}
/**
* 返回24小时内时间数组,默认30分钟分隔
* @param startDate
* @param endDate
* @param space 分钟 默认30
* @returns
*/
export function getDateArray(startDate: Date, endDate: Date, space: number) {
endDate = endDate || new Date()
startDate = startDate || new Date(new Date().getTime() - 60 * 60 * 1000)
space = (space || 30) * 60 * 1000
let endTime = endDate.getTime()
let startTime = startDate.getTime()
let mod = endTime - startTime
if (mod <= space) {
return
}
let dateArray = ['00:00']
while (mod >= space) {
let d = new Date()
startTime = startTime + space
d.setTime(startTime)
let newd = dateFormat(d, 'HH:mm')
dateArray.push(newd)
mod = mod - space
}
let newend = dateFormat(endDate, 'HH:mm')
dateArray.push(newend)
let dateArrs = dateArray.sort((a, b) => {
return Date.parse(a) - Date.parse(b)
})
if (dateArrs[dateArrs.length - 1] == '23:59') {
dateArrs[dateArrs.length - 1] = '24:00'
}
return dateArrs
}