手撕一个拖拽功能的时间轴

395 阅读5分钟

效果展示

截屏2024-11-24 16.12.32.png

主要功能演示:

  1. 时间轴拖拽
  2. 实时时间显示
  3. 日期切换效果
  4. 过去/未来时段显示

查看完整演示视频

前言

在实际项目开发中,经常会遇到需要展示时间轴的场景。本文将介绍如何实现一个功能完善的交互式时间轴组件,包括时间拖拽、实时时间显示等功能。

功能特点

  • 可视化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>