效果展示
功能需求
布局:右侧为按时间排列的图片列表,左侧为时间轴
交互:要求右侧滚动时左侧滚动到对应时间,左侧点击对应时间时右侧滚动到指定位置
实现思路
右侧时间轴:由传入的开始时间和结束时间按月确认间隔,计算出每个月份间隔的距离,然后根据传入的时间计算到开始时间或者结束时间的差值计算出高度,并展示到页面上。当鼠标在时间轴上移动时,由vueuse的useMouseInElement计算鼠标位置来确定显示的时间,并将选中的时间反馈给父组件。
外层容器组件:加载数据时,给每个循序模块设置id为时间,当时间轴反馈回时间后可以根据锚点跳转至指定位置。当外层容器滚动时,可以根据useIntersectionObserver来确定当前显示的元素,根据id反向显示时间轴的时间。
分类轴(A`Z)实现思路同上
代码实现
时间轴组件
import moment from 'moment';
import { nextTick, onMounted, reactive, ref, unref, watch } from 'vue';
import { useMouseInElement, useIntersectionObserver } from '@vueuse/core';
import _ from 'lodash';
const props = withDefaults(
defineProps<{
startTime?: string;
endTime?: string;
scrollTime?: any;
activeKey?: string;
}>(),
{
startTime: moment().format('YYYY-MM-DD'),
endTime: moment().format('YYYY-MM-DD'),
scrollTime: '',
activeKey: '',
},
);
const timeline = ref<HTMLElement | null>(null);
const emits = defineEmits(['changeValue']);
const mouse = reactive(useMouseInElement(timeline));
const hoverTop = ref(0);
const uiOffset = 8;
const currentTop = ref(0);
const currentTime = ref(moment().format('YYYY.MM'));
const hoverTime = ref(moment().format('YYYY.MM'));
const timeList = ref<Array<number>>([]);
let allMonths = ref(12);
let monthsSlide = ref(12);
// 根据开始时间和结束时间确定时间轴的长度
const handlerTimeList = () => {
let l = moment(props.endTime).year() - moment(props.startTime).year();
allMonths.value = l * 12 + 12;
for (let i = 0; i <= l; i++) {
timeList.value.unshift(moment(props.startTime).year() + i);
}
};
// 根据鼠标距离顶部位置确定hover时显示的时间
const handelHoverTime = () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
let m = _.round((hoverTop.value / timeline.value?.clientHeight!) * allMonths.value, 4);
hoverTime.value = moment(moment(unref(props.endTime)).format('YYYY-01-01'))
.add(1, 'years')
.subtract(m, 'months')
.format('YYYY.MM');
};
// 根据传入的时间确定位置
const handlerTopByTime = (curtime) => {
let d = moment(moment(unref(props.endTime)).format('YYYY-01-01'))
.add(1, 'years')
.diff(moment(curtime), 'months');
return _.round(d * monthsSlide.value, 4);
};
// 鼠标移动事件
watch(
() => mouse,
(n) => {
if (n && !n.isOutside) {
hoverTop.value = mouse.elementY;
handelHoverTime();
}
},
{
deep: true,
},
);
// 外部时间改变时,改变位置
watch(
() => props.scrollTime,
(n) => {
if (!n || n == '') {
return;
}
currentTime.value = moment(props.scrollTime).format('YYYY.MM');
currentTop.value = handlerTopByTime(unref(currentTime));
},
{
immediate: true,
},
);
// 点击时间事件
const setTime = () => {
currentTop.value = hoverTop.value;
currentTime.value = hoverTime.value;
emits('changeValue', currentTime.value);
};
onMounted(() => {
handlerTimeList();
nextTick(() => {
useIntersectionObserver(
timeline.value,
([{ isIntersecting }]) => {
if (isIntersecting && timeline.value) {
monthsSlide.value = _.round(timeline.value?.clientHeight / allMonths.value, 4);
currentTime.value = moment(props.scrollTime).format('YYYY.MM');
currentTop.value = handlerTopByTime(unref(currentTime));
}
},
{},
);
});
});
外层图片列表组件
import { ref, watch } from 'vue';
import _ from 'lodash';
import { TimeLine } from '@/components';
import ImgsGroup from './imgsGroup.vue';
import moment from 'moment';
import { useTemplateRefsList } from '@vueuse/core';
import { useIntersectionObserver, useElementVisibility } from '@vueuse/core';
const elRefs = useTemplateRefsList<HTMLDivElement>();
const rootTimeDiv = ref<HTMLDivElement | null>(null);
let currentTime = ref('');
let currentId = ref('');
let elRefStatus: any = ref([]);
let checkedTimeImgsMap = new Map();
let checkedTimeImgsData = ref<any>([]);
const imgs = [
'2022-11-01',
'2022-09-01',
'2022-04-01',
'2022-01-01',
'2021-11-01',
'2021-01-01',
'2020-01-01',
'2019-11-01',
'2019-01-01',
'2018-01-01',
'2017-01-01',
'2016-11-01',
'2016-01-01',
'2015-10-01',
];
// 点击时间时滚动到指定位置,如果没有对应时间则滚动到期上一个月
const changeTimeValue = (data) => {
let jumpTime = moment(data).format('YYYY-MM-DD');
while (!imgs.includes(jumpTime) && !moment(jumpTime).isBefore(imgs[imgs.length - 1])) {
jumpTime = moment(jumpTime).subtract(1, 'months').format('YYYY-MM-DD');
}
document.getElementById(jumpTime)?.scrollIntoView({
behavior: 'smooth',
});
};
// 选择框选择事件
const groupTimeCheckBoxChange = ({ title, checkData }) => {
checkedTimeImgsMap.set(title, checkData);
let res: any = [];
checkedTimeImgsMap.forEach(function (item, key, mapObj) {
res.push(...item);
});
checkedTimeImgsData.value = res;
};
const handleCurrentTime = () => {
currentTime.value = currentId.value;
};
const calcBool = (arr, value) => {
let indexArr: any = [];
for (let i = 0; i < arr.length; i++) {
arr[i] === value && indexArr.push(i);
}
return indexArr;
};
// 滚动时处理当前显示时间事件
watch(
elRefs,
async () => {
[...elRefs.value].map((el, index) => {
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
elRefStatus.value[index] = isIntersecting;
let a = calcBool(elRefStatus.value, true);
if (a.length > 0) {
currentId.value = [...elRefs.value][a[0]].id;
handleCurrentTime();
}
},
{ root: rootTimeDiv, threshold: 0.8 },
);
});
},
{
deep: true,
flush: 'post',
},
);