353 lines
10 KiB
Vue
353 lines
10 KiB
Vue
<template>
|
|
<div class="timeAxis-component">
|
|
<div class="time">{{ TansTimestamp(startTime, "YYYY.MM.DD HH:mm:ss") }}</div>
|
|
<div class="axis" ref="axisRef" @pointerdown="handlePointerDown">
|
|
<el-tooltip
|
|
v-for="time in timeList"
|
|
:key="time"
|
|
:content="TansTimestamp(time, 'YYYY.MM.DD HH:mm:ss')"
|
|
placement="bottom"
|
|
effect="light"
|
|
>
|
|
<div
|
|
class="time-section"
|
|
:style="{ left: getTimeSectionLeft(time) + 'px' }"
|
|
@pointerdown.stop="handleSectionPointerDown(time)"
|
|
></div>
|
|
</el-tooltip>
|
|
<div class="progress-bar" :style="trackStyle"></div>
|
|
<div class="active-sign">
|
|
<el-tooltip
|
|
v-if="cursor"
|
|
:content="TansTimestamp(cursor, 'YYYY.MM.DD HH:mm:ss')"
|
|
placement="bottom"
|
|
effect="light"
|
|
>
|
|
<div class="active-needle" :style="showHidden"></div>
|
|
</el-tooltip>
|
|
<el-tooltip
|
|
:content="TansTimestamp(currentTime, 'YYYY.MM.DD HH:mm:ss')"
|
|
placement="bottom"
|
|
effect="light"
|
|
>
|
|
<div
|
|
class="timeLine-point"
|
|
@pointerdown.stop="handlePointPointerDown"
|
|
:style="{ left: `${currentPosition - 9}px` }"
|
|
></div>
|
|
</el-tooltip>
|
|
</div>
|
|
</div>
|
|
<div class="time">{{ TansTimestamp(endTime, "YYYY.MM.DD HH:mm:ss") }}</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onUnmounted, onMounted, watch } from "vue"
|
|
import { TansTimestamp } from "@/utils/transform"
|
|
import { nowSize } from "@/utils/echarts-self-adaption"
|
|
const props = defineProps({
|
|
timeList: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
startTime: {
|
|
//起始时间
|
|
type: Date,
|
|
default: new Date("2024-05-16 16:56:04")
|
|
},
|
|
endTime: {
|
|
//结束时间
|
|
type: Date,
|
|
default: new Date("2024-05-23 10:16:56")
|
|
},
|
|
initPosition: {
|
|
//初始位置
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
cursor: {
|
|
//初始时,游标标出指定的时间
|
|
type: Date,
|
|
default: null
|
|
},
|
|
isAutoPlay: {
|
|
//创建组件后,是否自动播放
|
|
type: Boolean,
|
|
default: true
|
|
}
|
|
})
|
|
|
|
const startTime = ref(props.startTime) //开始时间
|
|
const endTime = ref(props.endTime) //结束时间
|
|
const timeList = ref(props.timeList) //时间列表
|
|
const currentTime = ref(new Date("2024-05-16 16:56:04")) // 当前选中的时间
|
|
const currentPosition = ref(props.initPosition) // 初始位置
|
|
const isPlaying = ref(props.isAutoPlay) // 是否自动播放
|
|
const axisRef = ref(null) // 轴的引用
|
|
const isDragging = ref(false) // 是否正在拖动
|
|
const axisWidth = nowSize(415) // 轴的长度(px)
|
|
const startTimeMs = startTime.value.getTime() // 起始时间的毫秒数
|
|
const endTimeMs = endTime.value.getTime() // 结束时间的毫秒数
|
|
const totalDuration = endTimeMs - startTimeMs // 计算总持续时间
|
|
const step = 4 // 每次移动的像素数(px)
|
|
let playTimer = null // 自动播放定时器
|
|
|
|
const emit = defineEmits(["click:pointerDown", "slide:pointerUp"])
|
|
|
|
// 计算进度条的样式
|
|
const trackStyle = computed(() => {
|
|
const progressPercent = Math.min(100, (currentPosition.value / axisWidth) * 100)
|
|
return {
|
|
background: `linear-gradient(90deg, #00F3FF 0%, #00F3FF ${progressPercent}%, #3B7699 ${progressPercent}%, #3B7699 100%)`,
|
|
width: "100%"
|
|
}
|
|
})
|
|
|
|
//点击用户组列表后,显示这两个用户的交互时间切片,获得每一个时间距离时间轴初始位置的距离
|
|
const getTimeSectionLeft = computed(() => {
|
|
return (time) => {
|
|
const total = endTime.value.getTime() - startTime.value.getTime()
|
|
const offset = new Date(time).getTime() - startTime.value.getTime()
|
|
return Math.max(0, Math.min(axisWidth, (offset / total) * axisWidth))
|
|
}
|
|
})
|
|
|
|
// 监听 props.timeList 的变化,更新 timeList
|
|
watch(
|
|
() => props.timeList,
|
|
(newVal) => {
|
|
timeList.value = newVal
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
// 监听 isAutoPlay 属性变化
|
|
watch(
|
|
() => props.isAutoPlay,
|
|
(newVal) => {
|
|
isPlaying.value = newVal
|
|
if (newVal) {
|
|
play()
|
|
} else {
|
|
pause()
|
|
}
|
|
}
|
|
)
|
|
|
|
// 让 active-needle 标定在 timeList 最后一个时间点
|
|
const showHidden = computed(() => {
|
|
if (!timeList.value || timeList.value.length === 0) return {}
|
|
const left = getTimeSectionLeft.value(timeList.value[timeList.value.length - 1]) + 5 // +5px 保持和 time-section 对齐
|
|
return { left: `${left}px` }
|
|
})
|
|
|
|
//自动播放暂停处理函数
|
|
const pause = () => {
|
|
if (!isPlaying.value) return
|
|
isPlaying.value = false
|
|
if (playTimer) {
|
|
clearInterval(playTimer)
|
|
playTimer = null
|
|
}
|
|
}
|
|
|
|
// 自动播放控制
|
|
const play = () => {
|
|
if (!isPlaying.value) return
|
|
playTimer = setInterval(() => {
|
|
if (currentPosition.value >= axisWidth) {
|
|
pause()
|
|
return
|
|
}
|
|
currentPosition.value = Math.min(axisWidth, currentPosition.value + step)
|
|
currentTime.value = getTimeFromPosition(currentPosition.value)
|
|
emit("slide:pointerUp", currentTime.value)
|
|
}, 300) // 每300ms移动一次
|
|
}
|
|
|
|
// 根据位置计算时间
|
|
const getTimeFromPosition = (position) => {
|
|
const ratio = Math.max(0, Math.min(1, position / axisWidth))
|
|
const timeOffset = totalDuration * ratio
|
|
return TansTimestamp(startTimeMs + timeOffset, "YYYY-MM-DD HH:mm:ss")
|
|
}
|
|
|
|
// 在时间轴上点击任意时间
|
|
const handlePointerDown = (e) => {
|
|
if (e.target.classList.contains("timeLine-point")) return
|
|
pause() // 拖动或点击时暂停自动播放
|
|
const rect = axisRef.value.getBoundingClientRect()
|
|
const position = Math.max(0, Math.min(axisWidth, e.clientX - rect.left))
|
|
currentPosition.value = position
|
|
currentTime.value = getTimeFromPosition(position)
|
|
emit("click:pointerDown", currentTime.value) //在父组中件发送请求
|
|
}
|
|
|
|
// 时间点指针按下事件
|
|
const handlePointPointerDown = (e) => {
|
|
e.stopPropagation()
|
|
e.preventDefault()
|
|
isDragging.value = true
|
|
// 缓存轴的边界矩形,避免重复计算
|
|
const rect = axisRef.value.getBoundingClientRect()
|
|
const axisLeft = rect.left
|
|
|
|
const handlePointerMove = (e) => {
|
|
if (!isDragging.value) return
|
|
const position = Math.max(0, Math.min(axisWidth, e.clientX - axisLeft))
|
|
|
|
// 直接更新位置,不检查阈值,确保实时响应
|
|
currentPosition.value = position
|
|
currentTime.value = getTimeFromPosition(position)
|
|
}
|
|
|
|
//拖动时,鼠标松开时
|
|
const handlePointerUp = () => {
|
|
isDragging.value = false
|
|
// 拖动结束时输出当前时间
|
|
pause() // 拖动或点击时暂停自动播放
|
|
const currentTimes = TansTimestamp(currentTime.value, "YYYY-MM-DD HH:mm:ss")
|
|
emit("slide:pointerUp", currentTimes)
|
|
document.removeEventListener("pointermove", handlePointerMove)
|
|
document.removeEventListener("pointerup", handlePointerUp)
|
|
}
|
|
document.addEventListener("pointermove", handlePointerMove, { passive: true })
|
|
document.addEventListener("pointerup", handlePointerUp)
|
|
}
|
|
|
|
const timeSectionWidth = 4 // 与样式保持一致
|
|
const handleSectionPointerDown = (time) => {
|
|
pause()
|
|
// 计算该时间点的中心位置
|
|
const left = getTimeSectionLeft.value(time) + timeSectionWidth / 2
|
|
currentPosition.value = left
|
|
currentTime.value = time // 保持与 timeList 精确一致
|
|
emit("click:pointerDown", time) // 直接发送原始时间
|
|
}
|
|
|
|
//重置时间轴
|
|
const reset = () => {
|
|
currentPosition.value = props.initPosition
|
|
currentTime.value = getTimeFromPosition(props.initPosition)
|
|
// 清理旧定时器
|
|
pause()
|
|
// 重新开始播放
|
|
if (props.isAutoPlay) {
|
|
isPlaying.value = true
|
|
play()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
play()
|
|
})
|
|
|
|
// 组件卸载时清理事件监听器
|
|
onUnmounted(() => {
|
|
document.removeEventListener("pointermove", () => {})
|
|
document.removeEventListener("pointerup", () => {})
|
|
pause()
|
|
})
|
|
|
|
defineExpose({
|
|
reset
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.timeAxis-component {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(270deg, rgba(0, 82, 125, 0.48) 0%, rgba(0, 200, 255, 0.23) 100%);
|
|
backdrop-filter: blur(vw(3));
|
|
border: vw(1) solid #3aa1f8;
|
|
border-radius: vw(4);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
color: #fff;
|
|
padding: 0 vw(10);
|
|
touch-action: none; // 防止触摸设备上的默认行为
|
|
.time {
|
|
font-family: "PingFang SC";
|
|
font-size: vw(16);
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
line-height: normal;
|
|
}
|
|
.axis {
|
|
width: vw(426);
|
|
height: vh(6);
|
|
border-radius: vw(20);
|
|
background-color: #3b7699;
|
|
cursor: pointer;
|
|
transform: translateZ(0); // 启用硬件加速
|
|
position: relative;
|
|
top: vh(2);
|
|
.time-section {
|
|
position: absolute;
|
|
top: vh(-8);
|
|
width: vw(4);
|
|
height: vh(22);
|
|
background: #ffe066;
|
|
border-radius: vw(3);
|
|
z-index: 3;
|
|
}
|
|
.progress-bar {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: vh(6);
|
|
background-color: #00ecf9;
|
|
border-radius: vw(20);
|
|
z-index: 1;
|
|
}
|
|
.active-sign {
|
|
position: relative;
|
|
z-index: 2;
|
|
.active-needle {
|
|
width: vw(30);
|
|
height: vh(34);
|
|
background-image: url("@/assets/images/point.png");
|
|
background-size: cover;
|
|
bottom: vh(-8);
|
|
left: vw(-11);
|
|
position: absolute;
|
|
}
|
|
.timeLine-point {
|
|
width: vw(18);
|
|
height: vh(18);
|
|
background-color: transparent;
|
|
border-radius: 50%;
|
|
border: vh(1.6) solid #ffe5a4;
|
|
position: absolute;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
top: vh(-6);
|
|
left: vw(-5);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
will-change: left;
|
|
transform: translate3d(0, 0, 0); // 强制启用硬件加速
|
|
&:hover {
|
|
transform: translate3d(0, 0, 0) scale(1.1);
|
|
}
|
|
&:active {
|
|
transform: translate3d(0, 0, 0) scale(0.95);
|
|
}
|
|
&::after {
|
|
content: "";
|
|
width: vw(10);
|
|
height: vh(10);
|
|
background-color: #f9bd25;
|
|
border-radius: 50%;
|
|
position: absolute;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|