<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>
时间轴,可以拖拽
最新推荐文章于 2025-08-04 14:37:55 发布