时间轴,可以拖拽

<template>
    <div class="time-line">
        <div class="line-box">
            <el-image
                :src="require('./left.png')"
                class="img"
                @click="preTime"
                v-if="isMore"
                :style="{ opacity: isLeftDisabled ? 0.5 : 1, cursor: isLeftDisabled ? 'not-allowed' : 'pointer' }"
                :disabled="isLeftDisabled"
            ></el-image>
            <div class="end-label">{{ startTime }}</div>
            <div
                class="split left"
                v-for="item in 3"
                :key="'start' + item"
                :style="{ transform: `scale(${0.2 * item})` }"
            ></div>
            <div class="line" ref="line">
                <div class="points" :style="points.length === 1 ? { justifyContent: 'center' } : {}">
                    <div
                        class="point-item"
                        v-for="(item, index) in points"
                        :key="'point' + item"
                        @click="handlePointClick(item)"
                        :style="{ left: pointWidth * index + '%', width: pointWidth + '%' }"
                    >
                        <el-tooltip :content="item" placement="top">
                            <div class="point"></div>
                        </el-tooltip>
                    </div>
                    <div
                        class="label-item"
                        v-for="(item, index) in points"
                        :key="'label' + item"
                        :style="{ left: pointWidth * index + '%', minWidth: pointWidth + '%' }"
                    >
                        <div class="label">{{ label(index, item) }}</div>
                    </div>
                </div>
                <div
                    class="select-points"
                    :style="{ width: selectStyle.width + '%', left: selectStyle.left + '%' }"
                    @mousedown="onDragStart"
                >
                    <div
                        class="select-point-item"
                        v-for="item in selectPoints"
                        :key="'select' + item"
                        :style="{ width: 100 / selectPoints.length + '%' }"
                        @click="handleSelectClick(item)"
                    >
                        <div class="box-box">
                            <el-tooltip :content="item" placement="top">
                                <div class="point" :class="{ active: selectTime == item }"></div>
                            </el-tooltip>
                        </div>
                    </div>
                </div>
            </div>
            <div
                class="split right"
                v-for="item in 3"
                :key="'end' + item"
                :style="{ transform: `scale(${0.2 * (4 - item)})` }"
            ></div>
            <div class="end-label">{{ endTime }}</div>
            <el-image
                :src="require('./right.png')"
                class="img"
                @click="nextTime"
                v-if="isMore"
                :style="{ opacity: isRightDisabled ? 0.5 : 1, cursor: isRightDisabled ? 'not-allowed' : 'pointer' }"
                :disabled="isRightDisabled"
            ></el-image>
        </div>
    </div>
</template>

<script>
import dayjs from 'dayjs';

export default {
    components: {},
    data() {
        return {
            startTime: '',
            endTime: '',
            selectTime: '',
            range: [],
            points: [],
            maxCount: 15,
            selectPoints: [],
            selectRange: [],
            allWidth: 0,
            activePoint: '',
            option: {
                type: 'year',
                count: 5,
                format: 'YYYY'
            },
            isMore: true
        };
    },
    created() {},
    mounted() {
        // 获取尺寸不知道为啥不正确
        // this.allWidth = this.$refs.line.offsetWidth;
    },

    computed: {
        isLeftDisabled() {
            return dayjs(this.selectRange[0]).isSame(dayjs(this.startTime));
        },
        isRightDisabled() {
            return dayjs(this.selectRange[1]).isSame(dayjs(this.endTime));
        },
        pointWidth() {
            let width = 0;
            const allwidth = this.allWidth;
            const pointPoint = this.points.length;
            // width = allwidth / pointPoint;
            return 100 / pointPoint;
        },
        selectStyle() {
            const { type, count, format } = this.option;
            const start = dayjs(this.range[0]);
            const end = dayjs(this.selectRange[0]);
            // 计算时间范围内的总单位数
            const totalUnits = end.diff(start, type);
            const width = this.pointWidth * this.selectPoints.length;
            const left = this.pointWidth * totalUnits;
            return {
                width: width,
                left: left
            };
        },
        label() {
            return (idx, label) => {
                // 确保首尾必须有标签
                if (idx === 0 || idx === this.points.length - 1) {
                    return label;
                }

                // 计算中间标记数量(最多5个,避免重叠)
                const totalLabels = Math.min(5, this.points.length - 2); // 减去首尾
                if (totalLabels <= 0) return '';

                // 计算步长和偏移量,确保均匀分布
                const interval = (this.points.length - 1) / (totalLabels + 1);
                const positions = Array.from({ length: totalLabels }, (_, i) => Math.round((i + 1) * interval));

                // 检查当前索引是否在计算出的均匀分布位置上
                return positions.includes(idx) ? label : '';
            };
        }
    },
    watch: {
        selectTime: {
            handler(val) {
                if (val) {
                    this.$emit('timeChange', val);
                }
            }
        },
        selectPoints: {
            handler(val, oldValue) {
                if (JSON.stringify(val) !== JSON.stringify(oldValue)) {
                    this.$emit('rangeChange', this.selectPoints, this.selectRange);
                }
            },
            deep: true
        }
    },
    methods: {
        initLine({ type, minTime }) {
            this.initOption(type);
            const currentYear = dayjs().format(this.option.format);
            this.endTime = currentYear;
            this.startTime = minTime || currentYear;
            this.selectTime = currentYear;
            this.startTime = dayjs(this.startTime).format(this.option.format);

            const start = dayjs(this.startTime);
            const end = dayjs(this.endTime);

            // 计算时间范围内的总单位数
            const totalUnits = end.diff(start, type) + 1;
            this.maxCount = totalUnits > this.maxCount ? this.maxCount : totalUnits;
            this.option.count = totalUnits > this.option.count ? this.option.count : totalUnits;

            // 没有更多的数据
            if (this.option.count == totalUnits) {
                this.isMore = false;
            } else {
                this.isMore = true;
            }
            this.updatePoints();
            this.updateSelectPoints();
        },
        initOption(type) {
            let option = {
                type: 'year',
                count: 5,
                format: 'YYYY'
            };
            if (type == 'day') {
                option = {
                    type: 'day',
                    count: 7,
                    format: 'YYYY-MM-DD'
                };
            } else if (type == 'month') {
                option = {
                    type: 'month',
                    count: 6,
                    format: 'YYYY-MM'
                };
            }
            this.option = option;
        },
        handleSelectClick(date) {
            if (this.hasDragged) return;
            this.selectTime = date;
        },
        handlePointClick(date) {
            const { type, count, format } = this.option;
            const dateObj = dayjs(date);
            const startDate = dayjs(this.selectRange[0]);
            const endDate = dayjs(this.selectRange[1]);

            // 如果点击的日期小于当前选中范围的起点
            if (dateObj.isBefore(startDate)) {
                this.selectRange = [
                    date,
                    dayjs(date)
                        .add(count - 1, type)
                        .format(format)
                ];
            }
            // 如果点击的日期大于当前选中范围的终点
            else if (dateObj.isAfter(endDate)) {
                this.selectRange = [
                    dayjs(date)
                        .subtract(count - 1, type)
                        .format(format),
                    date
                ];
            }
            // 如果点击的日期在选中范围内,则不做改变
            this.updateSelectPoints();
        },
        // 拖拽开始事件处理
        onDragStart(e) {
            this.allWidth = 100;
            // 记录鼠标按下时的初始X坐标
            const startX = e.clientX;
            // 获取当前选中区域的初始left位置(像素值)
            const startLeft = parseInt(this.selectStyle.left);
            // 计算允许拖动的最大left值(防止拖出右边界)
            const maxLeft = this.allWidth - parseInt(this.selectStyle.width);
            this.hasDragged = false; // 新增标志位,判断是否有拖动行为

            // 鼠标移动事件处理函数
            const onMouseMove = (e) => {
                this.hasDragged = true; // 标记为已拖动
                // 计算新的left位置 = 初始位置 + 鼠标移动距离
                let newLeft = startLeft + (e.clientX - startX);
                // 限制拖动范围在0到maxLeft之间
                newLeft = Math.max(0, Math.min(newLeft, maxLeft));

                // 根据像素位置计算对应的时间点索引
                const pointIndex = Math.round(newLeft / this.pointWidth);
                // 检查索引是否有效(不越界)
                if (pointIndex >= 0 && pointIndex + this.option.count <= this.points.length) {
                    // 更新选中时间范围: [起始点, 起始点+count-1]
                    this.selectRange = [this.points[pointIndex], this.points[pointIndex + this.option.count - 1]];
                }
            };
            // 鼠标释放事件处理函数
            const onMouseUp = () => {
                if (this.hasDragged) {
                    const count = this.maxCount - this.option.count;
                    // 检查是否拖拽到左边界
                    if (parseInt(this.selectStyle.left) === 0) {
                        // 向左递减5个日期
                        const newStart = dayjs(this.range[0]).subtract(count, this.option.type);
                        if (newStart.isAfter(dayjs(this.startTime)) || newStart.isSame(dayjs(this.startTime))) {
                            this.range = [
                                newStart.format(this.option.format),
                                dayjs(this.range[1]).subtract(count, this.option.type).format(this.option.format)
                            ];
                        } else {
                            // 如果已经到达最早时间
                            this.range = [
                                this.startTime,
                                dayjs(this.startTime)
                                    .add(this.maxCount - 1, this.option.type)
                                    .format(this.option.format)
                            ];
                        }
                        this.updatePoints();
                    }
                    // 检查是否拖拽到右边界
                    else if (parseInt(this.selectStyle.left) + 1 >= this.allWidth - parseInt(this.selectStyle.width)) {
                        // 向右递增5个日期
                        const newEnd = dayjs(this.range[1]).add(count, this.option.type);
                        if (newEnd.isBefore(dayjs(this.endTime)) || newEnd.isSame(dayjs(this.endTime))) {
                            this.range = [
                                dayjs(this.range[0]).add(count, this.option.type).format(this.option.format),
                                newEnd.format(this.option.format)
                            ];
                        } else {
                            // 如果已经到达最晚时间
                            this.range = [
                                dayjs(this.endTime)
                                    .subtract(this.maxCount - 1, this.option.type)
                                    .format(this.option.format),
                                this.endTime
                            ];
                        }
                        this.updatePoints();
                    }
                    this.updateSelectPoints();
                }
                // 移除事件监听
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };

            // 添加鼠标移动和释放事件监听
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        },
        updatePoints() {
            const points = [];
            const { type, count, format } = this.option;
            const start = dayjs(this.range[0] || this.startTime);
            const end = dayjs(this.range[1] || this.endTime);

            // 计算时间范围内的总单位数
            const totalUnits = end.diff(start, type) + 1;

            if (totalUnits <= this.maxCount) {
                // 如果时间范围小于等于count,显示所有可用时间点
                for (let i = 0; i < totalUnits; i++) {
                    points.push(start.add(i, type).format(format));
                }
            } else {
                // 否则按原逻辑显示count个点
                for (let i = 0; i < this.maxCount; i++) {
                    points.unshift(end.subtract(i, type).format(format));
                }
            }

            // 优化:当只有一个点时,范围设置为相同值
            if (points.length === 1) {
                this.range = [points[0], points[0]];
            } else {
                this.range = [points[0], points[points.length - 1]];
            }

            this.range = [points[0], points[points.length - 1]];
            this.points = points;
        },
        updateSelectPoints() {
            const points = [];
            const { type, count, format } = this.option;
            const end = dayjs(this.selectRange[1] || this.endTime);
            const start = dayjs(this.selectRange[0] || this.startTime);

            // 计算时间范围内的总单位数
            const totalUnits = end.diff(start, type) + 1;
            if (totalUnits <= count) {
                // 如果时间范围小于等于count,显示所有可用时间点
                for (let i = 0; i < totalUnits; i++) {
                    points.push(start.add(i, type).format(format));
                }
            } else {
                // 否则按原逻辑显示count个点
                for (let i = 0; i < count; i++) {
                    points.unshift(end.subtract(i, type).format(format));
                }
            }
            // 优化:当只有一个点时,范围设置为相同值
            if (points.length === 1) {
                this.selectRange = [points[0], points[0]];
            } else {
                this.selectRange = [points[0], points[points.length - 1]];
            }

            this.selectRange = [points[0], points[points.length - 1]];
            this.selectPoints = points;
            // 更新当前选中时间点
            this.selectTime = this.selectPoints[this.selectPoints.length - 1];
        },
        changeEnd() {
            this.updatePoints();
        },
        changeStart(value) {
            const { type, count, format } = this.option;
            const end = dayjs(this.range[0])
                .add(count - 1, type)
                .format(format);
            this.range[1] = end;
            this.updatePoints();
        },
        preTime() {
            const { type, count, format } = this.option;
            // 计算新的selectRange
            const newStart = dayjs(this.selectRange[0]).subtract(count, type);
            // 检查是否超过startTime
            if (newStart.isBefore(dayjs(this.startTime))) {
                // 如果超过,则重置到最早时间
                this.selectRange = [
                    this.startTime,
                    dayjs(this.startTime)
                        .add(count - 1, type)
                        .format(format)
                ];
                this.range = [
                    this.startTime,
                    dayjs(this.startTime)
                        .add(this.maxCount - 1, type)
                        .format(format)
                ];
            } else {
                // 检查是否到达points左边界
                if (newStart.isBefore(dayjs(this.range[0]))) {
                    // 更新points范围
                    const pointsNewStart = dayjs(this.range[0]).subtract(count, type);
                    if (pointsNewStart.isAfter(dayjs(this.startTime)) || pointsNewStart.isSame(dayjs(this.startTime))) {
                        this.range = [
                            pointsNewStart.format(format),
                            dayjs(this.range[1]).subtract(count, type).format(format)
                        ];
                    } else {
                        // 如果已经到达最早时间
                        this.range = [
                            this.startTime,
                            dayjs(this.startTime)
                                .add(this.maxCount - 1, type)
                                .format(format)
                        ];
                    }
                }
                // 更新selectRange
                const newEnd = dayjs(newStart).add(count, type);
                this.selectRange = [newStart.format(format), newEnd.format(format)];
            }
            this.updatePoints();
            this.updateSelectPoints();
        },
        nextTime() {
            const { type, count, format } = this.option;
            // 计算新的selectRange
            const newEnd = dayjs(this.selectRange[1]).add(count, type);
            // 检查是否超过endTime
            if (newEnd.isAfter(dayjs(this.endTime))) {
                // 如果超过,则重置到最晚时间
                this.selectRange = [
                    dayjs(this.endTime)
                        .subtract(count - 1, type)
                        .format(format),
                    this.endTime
                ];
                // 如果已经到达最晚时间
                this.range = [
                    dayjs(this.endTime)
                        .subtract(this.maxCount - 1, type)
                        .format(format),
                    this.endTime
                ];
            } else {
                // 检查是否到达points右边界
                if (newEnd.isAfter(dayjs(this.range[1]))) {
                    // 更新points范围
                    const pointsNewEnd = dayjs(this.range[1]).add(count, type);
                    if (pointsNewEnd.isBefore(dayjs(this.endTime)) || pointsNewEnd.isSame(dayjs(this.endTime))) {
                        this.range = [
                            dayjs(this.range[0]).add(count, type).format(format),
                            pointsNewEnd.format(format)
                        ];
                    } else {
                        // 如果已经到达最晚时间
                        this.range = [
                            dayjs(this.endTime)
                                .subtract(this.maxCount - 1, type)
                                .format(format),
                            this.endTime
                        ];
                    }
                }
                const newStart = dayjs(newEnd).subtract(count, type);
                // 更新selectRange
                this.selectRange = [newStart.format(format), newEnd.format(format)];
            }
            this.updatePoints();
            this.updateSelectPoints();
        }
    }
};
</script>
<style lang="scss" scoped>
.time-line {
    width: 100%;
    color: #fff;
    font-size: 12px;
    user-select: none;
    .img {
        width: 15px;
        cursor: pointer;
        margin: 0 5px;
    }

    .end-label {
        color: #46a2f8;
        font-weight: bold;
    }

    .select-time {
        padding: 5px;
        margin: 0 2px;
        color: #188bf7;
        cursor: pointer;
    }
}
.block {
    display: flex;
    align-items: center;
    justify-content: center;
}
.time {
    width: 100px;
}
.line-box {
    width: 100%;
    display: flex;
    align-items: center;
}
.split {
    width: 8px;
    height: 8px;
    background: #115092;

    &.right {
        margin-left: 1px;
    }
    &.left {
        margin-right: 1px;
    }
}
.line {
    height: 8px;
    background: #115092;
    position: relative;
    flex: 1;
}
.points {
    top: 0;
    flex: 1;
    position: absolute;
    width: 100%;
}
.label-item {
    position: absolute;
    // transform: translateX(-20%);
    top: 12px;
    width: 65px;
    text-align: center;
}
.point-item {
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    position: absolute;
    cursor: pointer;
}
.select-point-item {
    display: flex;
    align-items: center;
    justify-content: center;
}

.box-box {
    height: 18px;
    width: 18px;
    display: flex;
    justify-content: center;
    align-items: center;
}

.point {
    height: 8px;
    width: 1px;
    background: #ffffff60;
}
.select-points {
    height: 8px;
    background: #1890ff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    position: absolute;
    border-radius: 4px;
    z-index: 99;
    cursor: grab; // 添加抓取光标样式

    &:active {
        cursor: grabbing; // 拖动时改变光标样式
    }

    .active {
        height: 14px;
        width: 14px;
        border: 2px solid #1890ff;
        background: #fff;
        border-radius: 100%;
    }
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值