
<template>
<div class="time-axis-container">
<div class="time-axis" ref="axisRef">
<!-- 刻度线 - 共25个刻度(0-24) -->
<div
v-for="hour in 25"
:key="hour - 1"
class="tick-mark"
:class="{
'major-tick': isMajorTick(hour - 1),
'active-tick': currentHour === hour - 1
}"
:style="{ left: `${(hour - 1) * (100 / 24)}%` }"
></div>
<!-- 刻度文字 -->
<div
v-for="hour in 25"
:key="`label-${hour - 1}`"
class="tick-label"
:style="{ left: `${(hour - 1) * (100 / 24)}%` }"
v-show="isMajorTick(hour - 1)"
>
{{ hour - 1 }}:00
</div>
<!-- 可拖动滑块 -->
<div
class="slider"
ref="sliderRef"
:style="{ left: `${currentHour * (100 / 24)}%` }"
@mousedown="startDrag"
@touchstart="startDrag"
>
<div class="slider-time">{{ formatTime(currentHour) }}</div>
<div class="slider-icon"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, defineExpose } from 'vue';
const props = defineProps<{
initialHour?: number;
}>();
const emit = defineEmits<{
(e: 'time-change', hour: number): void;
(e: 'stopChange', hour: number): void;
}>();
const axisRef = ref<HTMLElement | null>(null);
const sliderRef = ref<HTMLElement | null>(null);
const currentHour = ref(0); // 先初始化为0,在onMounted中设置实际值
const isDragging = ref(false);
// 获取当前时间的小时数
const getCurrentHour = () => {
const now = new Date();
const currentMinutes = now.getMinutes();
return currentMinutes > 0 ?
Math.min(now.getHours() + 1, 24) : // 确保不超过24
now.getHours();
};
// 初始化当前小时
const initCurrentHour = () => {
currentHour.value = props.initialHour !== undefined ?
Math.max(0, Math.min(24, props.initialHour)) :
getCurrentHour();
};
// 判断是否是主要刻度(0/4/8/12/16/20/24)
const isMajorTick = (hour: number) => hour % 4 === 0;
// 格式化时间显示
const formatTime = (hour: number) => {
return `${hour.toString().padStart(2, '0')}:00`;
};
// 开始拖动
const startDrag = (e: MouseEvent | TouchEvent) => {
e.preventDefault();
isDragging.value = true;
document.addEventListener('mousemove', handleDrag);
document.addEventListener('touchmove', handleDrag);
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchend', stopDrag);
};
// 共用计算方法
const calculateHourFromEvent = (e: MouseEvent | TouchEvent, axisRect: DOMRect) => {
let clientX;
if (e instanceof MouseEvent) {
clientX = e.clientX;
} else {
clientX = e.touches[0].clientX;
}
// 严格限制在轴范围内计算
const position = Math.max(0, Math.min(1, (clientX - axisRect.left) / axisRect.width));
return Math.round(position * 24);
};
// 处理拖动
const handleDrag = (e: MouseEvent | TouchEvent) => {
if (!isDragging.value || !axisRef.value) return;
const axisRect = axisRef.value.getBoundingClientRect();
const newHour = calculateHourFromEvent(e, axisRect);
if (newHour !== currentHour.value) {
currentHour.value = newHour;
emit('time-change', newHour);
}
};
// 停止拖动
const stopDrag = () => {
isDragging.value = false;
emit('stopChange', currentHour.value);
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('touchmove', handleDrag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag);
};
// 点击时间轴直接跳转
const handleAxisClick = (e: MouseEvent) => {
if (!axisRef.value || isDragging.value) return;
const axisRect = axisRef.value.getBoundingClientRect();
// 添加点击位置的安全边距检查
if (e.clientX < axisRect.left || e.clientX > axisRect.right) return;
const newHour = calculateHourFromEvent(e, axisRect);
if (newHour !== currentHour.value) {
currentHour.value = newHour;
emit('time-change', newHour);
}
};
defineExpose({
getCurrentHour
})
onMounted(() => {
initCurrentHour()
if (axisRef.value) {
axisRef.value.addEventListener('click', handleAxisClick);
}
});
onUnmounted(() => {
if (axisRef.value) {
axisRef.value.removeEventListener('click', handleAxisClick);
}
stopDrag();
});
</script>
<style scoped>
.time-axis-container {
width: 100%;
padding: 20px 0;
position: relative;
}
.time-axis {
position: relative;
height: 20px;
width: 100%;
border-radius: 4px;
border-bottom: 2px solid #FFFFFF;
cursor: pointer;
}
.tick-mark {
position: absolute;
bottom: 0;
width: 1px;
height: 6px;
background-color: #FFFFFF;
transform: translateX(-50%);
}
.tick-mark.major-tick {
height: 12px;
background-color: #FFFFFF;
}
/*.tick-mark.active-tick {*/
/* background-color: #ff3d00;*/
/*}*/
.tick-label {
position: absolute;
bottom: -36px;
transform: translateX(-50%);
font-size: 20px;
color: #FFFFFF;
}
.slider {
position: absolute;
top: -36px;
transform: translateX(-50%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: grab;
user-select: none;
z-index: 10;
}
.slider:active {
cursor: grabbing;
}
.slider-icon {
width: 32px;
height: 38px;
background-image: url("/src/assets/realTimeScheduling/ranged-thumb.png");
background-repeat: no-repeat;
background-size: 32px 38px;
background-position: center center;
}
.slider-time {
font-size: 24px;
color: white;
}
</style>