<template>
<div class="timeLineContainer" ref="timeLineContainer">
<canvas
ref="canvas"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
@click="onClick"
></canvas>
<!-- 悬浮提示 -->
<!-- <div v-if="showTooltip" :style="tooltipStyle">{{ tooltipText }}</div>-->
</div>
</template>
<script>
import moment from "moment";
export default {
name: "PlaybackTimeline",
props: {
startTime: {
type: Date,
required: true,
},
endTime: {
type: Date,
required: true,
},
highlightedSegments: {
type: Array,
default: () => [],
},
},
data() {
return {
width: null,
height: 100,
ctx: null,
showTooltip: false,
tooltipText: "",
tooltipPosition: { x: 0, y: 0 },
mouseX: null,
drawPending: false, // 是否已经在等待重绘
lastDrawTime: 0,
minRedrawInterval: 50, // 最小重绘间隔,防抖用
visibleSegmentsCache: [], // 当前可见区域内的 segments 缓存
};
},
mounted() {
this.init();
window.addEventListener("resize", this.debounceRedraw);
},
beforeDestroy() {
window.removeEventListener("resize", this.debounceRedraw);
},
watch: {
highlightedSegments: {
handler(newVal) {
this.debounceRedraw();
},
deep: true,
},
startTime: "debounceRedraw",
endTime: "debounceRedraw",
},
methods: {
init() {
const container = this.$refs.timeLineContainer;
const canvas = this.$refs.canvas;
const { width } = container.getBoundingClientRect();
this.width = width;
canvas.width = width;
canvas.height = this.height;
this.ctx = canvas.getContext("2d");
this.debounceRedraw();
},
debounceRedraw() {
if (this.drawPending) return;
this.drawPending = true;
requestAnimationFrame(() => {
const now = Date.now();
if (now - this.lastDrawTime > this.minRedrawInterval) {
this.draw();
this.lastDrawTime = now;
}
this.drawPending = false;
});
},
onClick(e) {
const rect = this.$refs.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const { startTime, endTime, width, height } = this;
const totalSeconds = (endTime.getTime() - startTime.getTime()) / 1000;
const pxPerMs = width / totalSeconds / 1000;
for (let seg of this.visibleSegmentsCache) {
const startX = (seg.beginTime - startTime.getTime()) * pxPerMs;
const endX = (seg.endTime - startTime.getTime()) * pxPerMs;
const highlightStartX = Math.max(0, startX);
const highlightEndX = Math.min(width, endX);
const highlightTop = height / 2 - 6;
const highlightBottom = height / 2 + 6;
if (
x >= highlightStartX &&
x <= highlightEndX &&
y >= highlightTop &&
y <= highlightBottom
) {
const clickedTime = new Date(startTime.getTime() + x / pxPerMs);
this.$emit("highlight-click", {
time: clickedTime,
segment: seg,
});
return;
}
}
},
draw() {
const { ctx, width, height, startTime, endTime } = this;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(0, 0, width, height);
const totalSeconds = (endTime - startTime) / 1000;
const pxPerMs = width / totalSeconds / 1000;
// 刻度线绘制逻辑不变...
this.drawTicks(pxPerMs);
// 可视区域筛选出需要绘制的 segments
this.visibleSegmentsCache = this.highlightedSegments.filter(seg => {
return seg.beginTime <= endTime && seg.endTime >= startTime;
});
// 绘制高亮段
this.visibleSegmentsCache.forEach((seg) => {
const startX = Math.max(
0,
(seg.beginTime - startTime.getTime()) * pxPerMs
);
const endX = Math.min(
width,
(seg.endTime - startTime.getTime()) * pxPerMs
);
if (endX > startX) {
ctx.fillStyle = "#FFD700"; // 黄色
ctx.fillRect(startX, height / 2 - 6, endX - startX, 20);
}
});
// 绘制鼠标线
if (this.mouseX !== null) {
ctx.strokeStyle = "#fff";
ctx.beginPath();
ctx.moveTo(this.mouseX, 0);
ctx.lineTo(this.mouseX, height);
ctx.stroke();
}
},
drawTicks(pxPerMs) {
const { ctx, width, height, startTime } = this;
const intervalMinutes = this.calculateInterval();
let current = new Date(startTime);
let lastX = null;
while (current <= this.endTime) {
const msFromStart = current - startTime;
const x = msFromStart * pxPerMs;
if (x >= 0 && x <= width) {
const isMajorTick = current.getMinutes() % intervalMinutes === 0;
if (isMajorTick) {
ctx.beginPath();
ctx.strokeStyle = "#fff";
ctx.moveTo(x, 0); ctx.lineTo(x, 15);
ctx.stroke();
ctx.fillStyle = "#ccc";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(moment(current).format(intervalMinutes >= 1440 ? "YYYY-MM-DD" : "HH:mm"), x, 30);
}
if (isMajorTick && lastX !== null && x - lastX > 20) {
const mid1 = lastX + (x - lastX) / 3;
const mid2 = lastX + (x - lastX) * 2 / 3;
ctx.beginPath();
ctx.strokeStyle = "#666";
ctx.moveTo(mid1, 4);
ctx.lineTo(mid1, 10);
ctx.moveTo(mid2, 4);
ctx.lineTo(mid2, 10);
ctx.stroke();
}
if (isMajorTick) lastX = x;
}
current.setMinutes(current.getMinutes() + intervalMinutes);
}
},
calculateInterval() {
const { startTime, endTime } = this;
const totalHours = (endTime - startTime) / (60 * 60 * 1000);
if (totalHours <= 12) return 30;
else if (totalHours <= 24) return 60;
else if (totalHours <= 48) return 120;
else if (totalHours <= 72) return 180;
else if (totalHours <= 168) return 360;
else return 1440;
},
onMouseMove(e) {
const rect = this.$refs.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const time = this.getXTime(x);
this.tooltipText = moment(time).format("YYYY-MM-DD HH:mm:ss");
this.tooltipPosition = { x: e.clientX, y: e.clientY };
this.mouseX = x;
this.showTooltip = true;
this.debounceRedraw();
},
onMouseLeave() {
this.mouseX = null;
this.showTooltip = false;
this.debounceRedraw();
},
getXTime(x) {
const ratio = x / this.width;
return this.startTime.getTime() + ratio * (this.endTime.getTime() - this.startTime.getTime());
},
redraw() {
this.debounceRedraw();
},
},
computed: {
tooltipStyle() {
return {
position: 'absolute',
left: `${this.tooltipPosition.x + 10}px`,
top: `${this.tooltipPosition.y - 30}px`,
background: 'rgba(0,0,0,0.8)',
color: '#fff',
padding: '4px 8px',
borderRadius: '3px',
fontSize: '12px',
pointerEvents: 'none',
zIndex: 1000,
visibility: this.showTooltip ? 'visible' : 'hidden',
opacity: this.showTooltip ? 1 : 0,
transition: 'opacity 0.2s ease'
};
}
}
};
</script>
<style scoped>
.timeLineContainer {
position: relative;
width: 100%;
height: 100px;
}
</style>
上面是子组件 接收的参数只需要开始时间,结束时间,时间轴时间段。
下面是父组件引用,其中这个方法handleHighlightClick是点击子组件黄色部分区域获取到当前时间
<div style="width: 100%; height: 50px;">
<PlaybackTimeline
:startTime="new Date(startTime)"
:endTime="new Date(endTime)"
:highlightedSegments="videoUrls[selectedIndex || 0].segments"
@highlight-click="handleHighlightClick"
/>
</div>
注意:videoUrls[selectedIndex || 0].segments 是数组对象集合
[{startTime:'',endTime:''},{startTime:'',endTime:''}]
是这样的结构,所以传入多少个就是多少段黄色区间