Vue3 可拖拽、点击的时间轴
·
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>精确时间刻度选择器(10分钟刻度)可点击拖拽</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
min-width: 800px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
font-size: 28px;
margin-bottom: 8px;
}
.header p {
color: #7f8c8d;
font-size: 16px;
}
.date-display {
text-align: center;
font-size: 18px;
color: #3498db;
margin-bottom: 20px;
font-weight: 600;
}
.timeline-container {
position: relative;
margin: 40px 0;
padding: 20px 0;
width: 100%;
}
.timeline-axis {
position: relative;
height: 4px;
background: #48db54;
border-radius: 2px;
}
.timeline-ticks {
display: flex;
justify-content: space-between;
position: relative;
top: -10px;
}
.tick {
position: relative;
text-align: center;
flex: 1;
min-width: 0;
}
.tick.major::before {
content: '';
position: absolute;
width: 2px;
height: 15px;
background: #3498db;
top: -19px;
left: 50%;
transform: translateX(-50%);
}
.tick.minor::before {
content: '';
position: absolute;
width: 1px;
height: 8px;
background: #3498db;
top: -12px;
left: 50%;
transform: translateX(-50%);
}
.tick-label {
position: absolute;
top: -35px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #2c3e50;
white-space: nowrap;
}
.marker {
position: absolute;
bottom: 0;
transform: translateX(-50%);
cursor: grab;
z-index: 10;
}
.marker:active {
cursor: grabbing;
}
.marker-triangle {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 16px solid #e74c3c;
border-top: none;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.marker-line {
position: absolute;
bottom: 16px;
left: 50%;
width: 2px;
height: 50px;
background: #e74c3c;
transform: translateX(-50%);
}
.marker-label {
position: absolute;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
background: #e74c3c;
color: white;
padding: 3px 6px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
.range-highlight {
position: absolute;
height: 8px;
background: rgba(52, 152, 219, 0.3);
top: 50%;
transform: translateY(-50%);
border-radius: 4px;
z-index: 5;
}
.range-background {
position: absolute;
height: 66px;
background: rgba(52, 152, 219, 0.1);
bottom: 0;
z-index: 1;
border-radius: 4px;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 30px;
}
button {
padding: 8px 16px;
background: #3498db;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
button:hover {
background: #2980b9;
transform: translateY(-2px);
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="header">
<h1>精确时间刻度选择器(10分钟刻度)</h1>
<p>拖动三角形标记选择时间范围</p>
</div>
<div class="date-display">{{ currentDate }}</div>
<div class="timeline-container">
<div class="timeline-axis">
<div
class="range-background"
:style="{
left: `${rangeStartPercent}%`,
width: `${rangeEndPercent - rangeStartPercent}%`
}"
></div>
<div
class="range-highlight"
:style="{
left: `${rangeStartPercent}%`,
width: `${rangeEndPercent - rangeStartPercent}%`
}"
></div>
</div>
<div class="timeline-ticks">
<div
class="tick"
:class="isMajorTick(tick) ? 'major' : 'minor'"
v-for="tick in ticks"
:key="tick"
@click="handleTickClick(tick)"
>
<span class="tick-label" v-if="isMajorTick(tick)">{{ formatTime(tick) }}</span>
</div>
</div>
<!-- 开始标记 -->
<div
class="marker"
:style="{ left: `${rangeStartPercent}%` }"
@mousedown="startDrag('start', $event)"
@touchstart="startDrag('start', $event)"
>
<div class="marker-triangle"></div>
<div class="marker-line"></div>
<div class="marker-label">{{ formatTime(rangeStart) }}</div>
</div>
<!-- 结束标记 -->
<div
class="marker"
:style="{ left: `${rangeEndPercent}%` }"
@mousedown="startDrag('end', $event)"
@touchstart="startDrag('end', $event)"
>
<div class="marker-triangle"></div>
<div class="marker-line"></div>
<div class="marker-label">{{ formatTime(rangeEnd) }}</div>
</div>
</div>
<div class="controls">
<button @click="setDefaultRange">重置范围</button>
<button @click="setRandomRange">随机范围</button>
</div>
</div>
</div>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
// 初始数据
const currentDate = ref('2025年10月9日');
const minTime = 0;
const maxTime = 24 * 60;
const rangeStart = ref(8 * 60); // 08:00
const rangeEnd = ref(18 * 60); // 18:00
const lastClickTime = ref(0);
const lastClickType = ref(null);
// 刻度生成 - 每10分钟一个刻度
const ticks = computed(() => {
const result = [];
for (let i = 0; i <= 24 * 60; i += 10) {
result.push(i);
}
return result;
});
// 主要刻度 - 每小时一个主要刻度
const isMajorTick = (minutes) => {
return minutes % 60 === 0;
};
// 精确计算百分比位置
const calculatePosition = (minutes) => {
const totalMinutes = maxTime - minTime;
const percentage = (minutes - minTime) / totalMinutes;
return percentage * 100;
};
const rangeStartPercent = computed(() => calculatePosition(rangeStart.value));
const rangeEndPercent = computed(() => calculatePosition(rangeEnd.value));
// 格式化时间显示
const formatTime = (minutes) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
};
// 重置范围
const setDefaultRange = () => {
rangeStart.value = 8 * 60;
rangeEnd.value = 18 * 60;
};
// 随机范围
const setRandomRange = () => {
const start = Math.floor(Math.random() * (22 * 60 - 0 * 60)) + 0 * 60;
const end = Math.min(maxTime, start + Math.floor(Math.random() * 240) + 60);
rangeStart.value = start;
rangeEnd.value = end;
};
// 处理刻度点击
const handleTickClick = (tick) => {
const now = Date.now();
// 判断是否为双击
if (now - lastClickTime.value < 300 && lastClickType.value === 'start') {
// 双击设置结束时间
rangeEnd.value = tick;
lastClickType.value = 'end';
} else {
// 单击设置开始时间
rangeStart.value = tick;
lastClickType.value = 'start';
}
lastClickTime.value = now;
// 确保开始时间不超过结束时间
if (rangeStart.value > rangeEnd.value) {
[rangeStart.value, rangeEnd.value] = [rangeEnd.value, rangeStart.value];
}
};
// 拖拽逻辑
let dragging = null;
let startX = 0;
let startValue = 0;
const startDrag = (type, event) => {
dragging = type;
startX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX;
startValue = type === 'start' ? rangeStart.value : rangeEnd.value;
document.addEventListener('mousemove', onDrag);
document.addEventListener('touchmove', onDrag, { passive: false });
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchend', stopDrag);
if (event.type.includes('touch')) {
event.preventDefault();
}
};
const onDrag = (event) => {
if (!dragging) return;
event.preventDefault();
const clientX = event.type.includes('touch') ? event.touches[0].clientX : event.clientX;
const timeline = document.querySelector('.timeline-axis');
const rect = timeline.getBoundingClientRect();
// 修正后的位置计算
const position = (clientX - rect.left) / rect.width;
const timeValue = Math.round((minTime + position * (maxTime - minTime)) / 10) * 10;
if (dragging === 'start') {
rangeStart.value = Math.max(minTime, Math.min(timeValue, rangeEnd.value - 10));
} else {
rangeEnd.value = Math.min(maxTime, Math.max(timeValue, rangeStart.value + 10));
}
};
const stopDrag = () => {
dragging = null;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('touchmove', onDrag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag);
};
return {
currentDate,
rangeStart,
rangeEnd,
ticks,
isMajorTick,
rangeStartPercent,
rangeEndPercent,
formatTime,
setDefaultRange,
setRandomRange,
startDrag,
handleTickClick
};
}
}).mount('#app');
</script>
</body>
</html>
更多推荐


所有评论(0)