SocialNetworks_duan/src/components/timeAxis.vue
qumeng039@126.com 5cf9e0c4ec bug修复
2025-08-04 13:54:33 +08:00

327 lines
9.2 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) + 5 + 'px' }"></div>
</el-tooltip>
<div class="progress-bar" :style="trackStyle"></div>
<div class="active-sign">
<el-tooltip
: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}px` }"
></div>
</el-tooltip>
</div>
</div>
<div class="time">{{ TansTimestamp(endTime, "YYYY.MM.DD HH:mm:ss") }}</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, computed, onUnmounted, onMounted, watch } from "vue"
import { TansTimestamp } from "@/utils/transform"
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: new Date("2024-05-23 10:16:56")
},
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 = 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 }
)
// 让 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 new Date(startTimeMs + timeOffset)
}
// 在时间轴上点击任意时间
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 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="less">
.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(3px);
border: 1px solid #3aa1f8;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
padding: 0 10px;
touch-action: none; // 防止触摸设备上的默认行为
.time {
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.axis {
width: 426px;
height: 6px;
border-radius: 20px;
background-color: #3b7699;
cursor: pointer;
transform: translateZ(0); // 启用硬件加速
position: relative;
top: 2px;
.time-section {
position: absolute;
top: -8px;
width: 4px;
height: 22px;
background: #ffe066;
border-radius: 3px;
z-index: 3;
}
.progress-bar {
position: absolute;
top: 0;
left: 0;
height: 6px;
background-color: #00ecf9;
border-radius: 20px;
z-index: 1;
}
.active-sign {
position: relative;
z-index: 2;
.active-needle {
width: 30px;
height: 34px;
background-image: url("@/assets/images/point.png");
background-size: cover;
bottom: -8px;
left: -11px;
position: absolute;
}
.timeLine-point {
width: 18px;
height: 18px;
background-color: transparent;
border-radius: 50%;
border: 1.6px solid #ffe5a4;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: -6px;
left: -5px;
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: 10px;
height: 10px;
background-color: #f9bd25;
border-radius: 50%;
position: absolute;
}
}
}
}
}
</style>