效果展示
主要功能演示:
- 时间轴拖拽
- 实时时间显示
- 日期切换效果
- 过去/未来时段显示
前言
在实际项目开发中,经常会遇到需要展示时间轴的场景。本文将介绍如何实现一个功能完善的交互式时间轴组件,包括时间拖拽、实时时间显示等功能。
功能特点
- 可视化24小时时间轴
- 支持时间拖拽选择
- 实时显示当前时间
- 日期选择功能
- 过去/未来时段显示
- 支持切换使用拖拽时间或当前时间
源码展示
<template>
<!-- 时间轴容器 -->
<div class="timeline-container">
<!-- 日期选择器区域 -->
<div class="date-picker-container">
<el-date-picker
v-model="selectedDate"
type="date"
placeholder="选择日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
@change="handleDateChange">
</el-date-picker>
</div>
<!-- 时间显示区域:显示蓝色拖拽线时间、红色当前时间线和默认时间 -->
<div class="time-display">
<div class="drag-time">
<span class="time-label">蓝色竖线位置:</span>
<span class="time-value blue">{{ fullDragTimeFormat }}</span>
</div>
<div class="current-time">
<span class="time-label">红色竖线位置:</span>
<span class="time-value red">{{ fullCurrentTimeFormat }}</span>
</div>
<div class="default-time">
<span class="time-label">默认时间:</span>
<span class="time-value">{{ defaultTimeDisplay }}</span>
</div>
</div>
<!-- 主时间轴区域 -->
<div class="timeline">
<!-- 左侧"时间轴"按钮 -->
<div class="timeline-button">
<span>时间轴</span>
</div>
<!-- 时间轴主体部分 -->
<div class="timeline-main">
<!-- 粉色背景轴线 -->
<div class="timeline-bar"></div>
<!-- 时间刻度标记(0-24小时) -->
<div class="time-marks">
<div v-for="hour in 13" :key="hour" class="mark">
<div class="mark-line"></div>
<div class="mark-number">{{ (hour - 1) * 2 }}</div>
</div>
</div>
<!-- 可拖动的蓝色时间指示器 -->
<div class="draggable-line"
:style="dragLineStyle"
@mousedown.prevent="startDrag">
<div class="time-tooltip">{{ dragTimeFormat }}</div>
</div>
<!-- 实时的红色时间指示器 -->
<div class="current-time-indicator"
:style="currentTimeStyle"
v-show="showRedLine">
<div class="triangle top"></div>
<div class="indicator-line"></div>
<div class="triangle bottom"></div>
<div class="now-text">现在</div>
</div>
<!-- 过去和未来时段标识 -->
<div class="time-periods" v-show="showRedLine">
<div class="time-line">
<div class="past-line" :style="{ width: currentTimePosition + '%' }"></div>
<div class="future-line" :style="{ width: (100 - currentTimePosition) + '%' }"></div>
</div>
<div class="period-labels">
<div class="past-label" :style="{ left: (currentTimePosition / 2) + '%' }">过去时段</div>
<div class="future-label" :style="{ left: (currentTimePosition + (100 - currentTimePosition) / 2) + '%' }">未来时段</div>
</div>
</div>
</div>
<!-- 使用拖拽时间的复选框 -->
<div class="checkbox-container">
<el-checkbox v-model="useDragTime">
使用拖拽时间
</el-checkbox>
</div>
</div>
</div>
</template>
<style scoped>
.timeline-container {
padding: 20px;
width: 800px;
margin: 0 auto;
}
.timeline {
position: relative;
height: 100px;
margin-top: 40px;
display: flex;
align-items: center;
gap: 20px; /* 按钮和时间轴之间的间距 */
}
/* 蓝色按钮样式 */
.timeline-button {
background-color: #1E90FF;
color: white;
padding: 8px 15px;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
white-space: nowrap;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.timeline-button:hover {
background-color: #187BE5;
}
/* 时间轴主体容器 */
.timeline-main {
flex: 1;
position: relative;
height: 100px;
}
/* 粉色主轴 */
.timeline-bar {
position: absolute;
top: 35px;
left: 0;
right: 0;
height: 10px;
background-color: #FFB6C1;
border-radius: 5px;
z-index: 2; /* 确保轴线在刻度线上方 */
}
/* 时间轴刻度容器 */
.time-marks {
position: relative;
height: 10px;
margin: 40px 0;
display: flex;
justify-content: space-between;
z-index: 1;
}
/* 刻度线和数字 */
.mark {
position: relative;
width: 2px;
}
.mark-line {
height: 16px;
width: 2px;
background: #000;
position: absolute;
top: -3px;
}
.mark-number {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
}
/* 可拖拽的蓝色竖线 */
.draggable-line {
position: absolute;
top: 20px;
width: 4px;
height: 40px;
background-color: #1E90FF;
cursor: grab;
z-index: 4;
transform: translateX(-50%);
}
.draggable-line:active {
cursor: grabbing;
}
/* 红色时间指示器 */
.current-time-indicator {
position: absolute;
top: 15px;
height: 50px;
z-index: 3;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
transition: opacity 0.3s ease;
}
.indicator-line {
width: 2px;
height: 100%;
background-color: red;
}
/* 三角形样式 */
.triangle {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid red;
}
.triangle.top {
transform: translateY(-50%);
}
.triangle.bottom {
transform: translateY(50%);
}
.time-tooltip {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
background-color: #1E90FF;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
white-space: nowrap;
}
.past-time::before,
.future-time::before {
content: '';
position: absolute;
top: -15px;
width: 40%;
border-top: 2px dashed #000;
}
.past-time::before {
left: 0;
}
.future-time::before {
right: 0;
}
.past-time,
.future-time {
position: relative;
}
.current-time {
text-align: center;
font-weight: bold;
margin-bottom: 20px;
border: 1px solid #000;
display: inline-block;
padding: 2px 10px;
}
.drag-time {
text-align: center;
font-weight: bold;
margin-bottom: 20px;
border: 1px solid #1E90FF;
color: #1E90FF;
display: inline-block;
padding: 2px 10px;
}
.time-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.drag-time,
.current-time,
.default-time {
width: 100%;
max-width: 300px;
transition: all 0.3s ease;
}
.drag-time:hover,
.current-time:hover,
.default-time:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.checkbox-container {
padding: 10px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 4px;
}
/* Element UI 复选框样式调整 */
/deep/ .el-checkbox__label {
font-size: 14px;
color: #606266;
}
/deep/ .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: #1E90FF;
border-color: #1E90FF;
}
.time-label {
color: #666;
min-width: 100px;
display: inline-block;
}
.time-value {
font-family: monospace;
font-weight: bold;
}
.time-value.blue {
color: #1E90FF;
}
.time-value.red {
color: #FF4444;
}
/* 美化边框效果 */
.drag-time {
border-color: #1E90FF;
background-color: rgba(30, 144, 255, 0.05);
}
.current-time {
border-color: #FF4444;
background-color: rgba(255, 68, 68, 0.05);
}
/* 悬停效果 */
.drag-time:hover,
.current-time:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.date-picker-container {
margin-bottom: 20px;
text-align: center;
}
/* 调整日期选择器宽度 */
/deep/ .el-date-picker {
width: 220px;
}
/* 现在文字样式 */
.now-text {
position: absolute;
top: 65px; /* 红线下方15px */
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #FF4444;
white-space: nowrap;
}
/* 时间段样式 */
.time-periods {
position: absolute;
top: 80px; /* 调整位置到时间轴下方 */
left: 0;
right: 0;
height: 30px;
}
.time-line {
display: flex;
height: 1px;
width: 100%;
}
.past-line {
height: 1px;
background: #000; /* 实线 */
}
.future-line {
height: 1px;
background: linear-gradient(to right, #000 50%, transparent 50%); /* 虚线效果 */
background-size: 6px 1px; /* 虚线间距 */
}
.period-labels {
position: relative;
width: 100%;
height: 20px;
}
.past-label, .future-label {
position: absolute;
transform: translateX(-50%);
font-size: 12px;
color: #666;
top: 5px;
}
</style>
<script>
export default {
// 组件数据
data() {
return {
selectedDate: null, // 选中的日期
dragPosition: 0, // 拖拽位置(百分比)
isDragging: false, // 是否正在拖拽
currentTimePosition: 0, // 当前时间位置(百分比)
timer: null, // 定时器引用
useDragTime: false // 是否使用拖拽时间
}
},
// 计算属性
computed: {
// 计算拖拽线的样式
dragLineStyle() {
return {
left: `${this.dragPosition}%`
}
},
// 计算当前时间线的样式
currentTimeStyle() {
return {
left: `${this.currentTimePosition}%`
}
},
// 格式化拖拽时间显示
dragTimeFormat() {
return this.formatTime(this.dragPosition)
},
// 格式化当前时间显示
currentTimeFormat() {
return this.formatTime(this.currentTimePosition)
},
// 获取当前日期字符串
currentDateStr() {
const now = new Date()
return now.toISOString().split('T')[0]
},
// 完整的拖拽时间格式(包含日期)
fullDragTimeFormat() {
const dateStr = this.selectedDate ?
this.formatSelectedDate(this.selectedDate) :
this.currentDateStr
return `${dateStr} ${this.dragTimeFormat}`
},
// 完整的当前时间格式(包含日期)
fullCurrentTimeFormat() {
return `${this.currentDateStr} ${this.currentTimeFormat}`
},
// 是否显示红色时间线
showRedLine() {
return !this.selectedDate || this.selectedDate === this.currentDateStr
},
// 默认时间显示
defaultTimeDisplay() {
return this.useDragTime ? this.fullDragTimeFormat : this.fullCurrentTimeFormat
}
},
// 方法
methods: {
// 格式化时间
formatTime(position) {
const totalSeconds = (position / 100) * 86400
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = Math.floor(totalSeconds % 60)
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
},
// 更新当前时间位置
updateCurrentTime() {
if (this.showRedLine) {
const now = new Date()
const totalSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds()
this.currentTimePosition = (totalSeconds / 86400) * 100
}
},
// 开始拖拽
startDrag(event) {
this.isDragging = true
document.addEventListener('mousemove', this.onDrag)
document.addEventListener('mouseup', this.stopDrag)
},
// 拖拽过程处理
onDrag(event) {
if (!this.isDragging) return
const timeline = this.$el.querySelector('.timeline-main')
const rect = timeline.getBoundingClientRect()
let position = ((event.clientX - rect.left) / rect.width) * 100
position = Math.max(0, Math.min(100, position))
this.dragPosition = position
},
// 停止拖拽
stopDrag() {
this.isDragging = false
document.removeEventListener('mousemove', this.onDrag)
document.removeEventListener('mouseup', this.stopDrag)
},
// 格式化选中的日期
formatSelectedDate(dateStr) {
if (!dateStr) return this.currentDateStr
const date = new Date(dateStr)
const year = date.getFullYear().toString().substr(-2)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day}`
},
// 处理日期变化
handleDateChange(value) {
this.selectedDate = value
this.$nextTick(() => {
if (this.showRedLine) {
this.updateCurrentTime()
}
})
}
},
// 监听器
watch: {
// 监听红线显示状态
showRedLine: {
immediate: true,
handler(newValue) {
if (newValue) {
this.updateCurrentTime()
}
}
}
},
// 生命周期钩子
mounted() {
this.updateCurrentTime()
this.timer = setInterval(() => {
if (this.showRedLine) {
this.updateCurrentTime()
}
}, 1000)
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
}
}
}
</script>