日历看板(日期维度)

186 阅读5分钟

开局看demo

代码块中用到的函数在最底下

数据结构设计

  • 日期数组dateList
// 获取日期列表
const dateList = getMonthDateList(2024, 12)
  • 事件数组eventList
// 事件列表
const eventList = ref([
    {
        start: new Date('2024-11-30 8:00').getTime(),
        end: new Date('2024-12-05 17:00').getTime(),
        title: '测试事件0',
        id: 0,
        color: 'pink'
    },
    {
        start: new Date('2024-12-05 8:00').getTime(),
        end: new Date('2024-12-07 17:00').getTime(),
        title: '测试事件1',
        id: 1,
        color: '#69C0FF'
    },
    {
        start: new Date('2024-12-05 8:00').getTime(),
        end: new Date('2024-12-06 17:00').getTime(),
        title: '测试事件2',
        id: 2,
        color: '#FF9F7F'
    },
    {
        start: new Date('2024-12-06 8:00').getTime(),
        end: new Date('2024-12-06 17:00').getTime(),
        title: '测试事件4',
        id: 4,
        color: '#B37FEB'
    },
    {
        start: new Date('2024-12-26 8:00').getTime(),
        end: new Date('2024-12-27 17:00').getTime(),
        title: '测试事件3',
        id: 3,
        color: '#FFC069'
    },
    {
        start: new Date('2024-11-01 8:00').getTime(),
        end: new Date('2024-12-03 17:00').getTime(),
        title: '测试事件5',
        id: 5,
        color: '#73D13D'
    },
    {
        start: new Date('2024-12-05 8:00').getTime(),
        end: new Date('2024-12-07 17:00').getTime(),
        title: '测试事件6',
        id: 6,
        color: '#40A9FF'
    },
    {
        start: new Date('2024-12-10 8:00').getTime(),
        end: new Date('2024-12-12 17:00').getTime(),
        title: '测试事件7',
        id: 7,
        color: '#FF85C0'
    },
    {
        start: new Date('2024-12-15 8:00').getTime(),
        end: new Date('2024-12-16 17:00').getTime(),
        title: '测试事件8',
        id: 8,
        color: '#36CFC9'
    },
    {
        start: new Date('2024-12-18 8:00').getTime(),
        end: new Date('2024-12-19 17:00').getTime(),
        title: '测试事件9',
        id: 9,
        color: '#597EF7'
    },
    {
        start: new Date('2024-12-21 8:00').getTime(),
        end: new Date('2024-12-22 17:00').getTime(),
        title: '测试事件10',
        id: 10,
        color: '#FFB8B1'
    },
    {
        start: new Date('2024-12-23 8:00').getTime(),
        end: new Date('2024-12-24 17:00').getTime(),
        title: '测试事件11',
        id: 11,
        color: '#87E8DE'
    },
    {
        start: new Date('2024-12-28 8:00').getTime(),
        end: new Date('2024-12-29 17:00').getTime(),
        title: '测试事件12',
        id: 12,
        color: '#ADC6FF'
    },
    {
        start: new Date('2024-12-30 8:00').getTime(),
        end: new Date('2024-12-31 17:00').getTime(),
        title: '测试事件13',
        id: 13,
        color: '#FFE58F'
    },
    {
        start: new Date('2024-12-13 8:00').getTime(),
        end: new Date('2024-12-14 17:00').getTime(),
        title: '测试事件14',
        id: 14,
        color: '#9254DE'
    }
])
  • 预期结果

字段注释已添加

// eventDistribution
{
    '2024-12-01': [
        {
            id: 1, // 事件唯一标识
            title: '测试事件1', // 事件标题
            start: 1701388800000, // 事件开始时间戳
            end: 1701475200000, // 事件结束时间戳
            color: '#FF4D4F', // 事件颜色
            days: 2, // 当前周内事件持续天数
            index: 0, // 事件在当天的显示层级
            isFirstDay: true // 是否为事件的第一天或当周第一天
        }
    ],
    '2024-12-02': [
        {
            id: 1,
            title: '测试事件1',
            start: 1701388800000,
            end: 1701475200000,
            color: '#FF4D4F',
            days: 1,
            index: 0,
            isFirstDay: false
        }
]
}

数据处理

  1. 将日期列表按周分割
// 将日期列表按周分割
const splitDateList = splitDatesByWeek(dateList)
  1. eventList进行排序

排序的规则:先按开始时间排序,其次开始时间相同时按结束时间排序

// 按时间戳排序
const sortList = sortByTimeStamp(eventList.value)
  1. 其次按日期分配事件

将事件按日期分配到每一天,并记录当前事件的前一天的标记位index

// 按日期分配事件
/* 示例
    {
        '2024-12-01': [event1, event2],
        '2024-12-02': [event3],
    }
 */
const eventDistribution = {}
// 遍历每周
splitDateList.forEach((week, weekIndex) => {
    const weekStart = week[0];
    const weekEnd = week[week.length - 1];
    week.forEach((currentDate, dayIndex) => {
        const dateIndex = weekIndex * 7 + dayIndex;
        const currentDateEvents = getCurrentDateEvents(currentDate, weekStart, weekEnd, sortList);
        currentDateEvents.forEach(event => {
            setEventDisplayProperties(event, currentDate, dayIndex, dateIndex, dateList, eventDistribution, currentDateEvents);
        });
        eventDistribution[currentDate] = currentDateEvents;
    });
});

以上代码块用到的方法util.js

// 按时间戳排序
export function sortByTimeStamp(eventList) {
    return eventList.sort((a, b) => {
        // 先按开始时间排序
        const startDiff = a.start - b.start;
        if (startDiff !== 0) {
            return startDiff;
        }
        // 开始时间相同时按结束时间排序
        return b.end - a.end;
    })
}

// 将日期列表按周分割
export function splitDatesByWeek(dates) {
    const result = [];
    for (let i = 0; i < dates.length; i += 7) {
        result.push(dates.slice(i, i + 7));
    }
    return result;
}

// 获取日期列表
export const getMonthDateList = (year, month) => {
    // 获取当月第一天是星期几
    const firstDayOfMonth = new Date(`${year}-${String(month).padStart(2, '0')}-01`).getDay()
    // 获取当月最后一天
    const lastDayOfMonth = new Date(year, month, 0).getDate()
    // 获取上月最后一天
    const lastDayOfLastMonth = new Date(year, month - 1, 0).getDate()

    // 计算年月
    const prevYear = month === 1 ? year - 1 : year
    const prevMonth = month === 1 ? 12 : month - 1
    const nextYear = month === 12 ? year + 1 : year
    const nextMonth = month === 12 ? 1 : month + 1

    const dateList = []
    // 添加上月日期
    for (let i = firstDayOfMonth - 1; i >= 0; i--) {
        dateList.push(`${prevYear}-${String(prevMonth).padStart(2, '0')}-${String(lastDayOfLastMonth - i).padStart(2, '0')}`)
    }
    // 添加本月日期
    for (let i = 1; i <= lastDayOfMonth; i++) {
        dateList.push(`${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`)
    }
    // 添加下月日期,补齐到42天(6周)
    for (let i = 1; dateList.length < 42; i++) {
        dateList.push(`${nextYear}-${String(nextMonth).padStart(2, '0')}-${String(i).padStart(2, '0')}`)
    }
    return dateList
}

// 获取日期的ISO格式字符串(YYYY-MM-DD)
const getISODateString = (date) => {
    return new Date(date).toISOString().split('T')[0];
}

// 计算两个日期之间的天数
export const getDaysBetweenDates = (startDate, endDate) => {
    return Math.floor((new Date(endDate) - new Date(startDate)) / (24 * 60 * 60 * 1000)) + 1;
}

// 获取当天的事件列表
export const getCurrentDateEvents = (currentDate, weekStart, weekEnd, sortList) => {
    return sortList.filter(event => {
        const eventStartDate = getISODateString(event.start);
        const eventEndDate = getISODateString(event.end);
        return eventStartDate <= currentDate && eventEndDate >= currentDate;
    }).map(event => {
        const eventStartDate = getISODateString(event.start);
        const eventEndDate = getISODateString(event.end);
        const startDate = eventStartDate < weekStart ? weekStart : eventStartDate;
        const endDate = eventEndDate > weekEnd ? weekEnd : eventEndDate;
        const days = getDaysBetweenDates(startDate, endDate);
        return { ...event, days };
    });
}

// 设置事件的显示属性
export const setEventDisplayProperties = (event, currentDate, dayIndex, dateIndex, dateList, eventDistribution, currentDateEvents) => {
    const previousDate = dateIndex > 0 ? dateList[dateIndex - 1] : null;
    const previousEvent = previousDate ? eventDistribution[previousDate]?.find(item => item.id === event.id) : null;

    event.index = previousEvent?.index ??
        (currentDateEvents[0]?.index ?? 0) + currentDateEvents.findIndex(item => item.id === event.id);

    event.isFirstDay = dayIndex === 0 ||
        getISODateString(event.start) === currentDate;
}

// 处理颜色透明度
export const handleColorOpacity = (color, opacity = 1) => {
    // 处理rgba格式
    if (color.startsWith('rgba')) {
        const values = color.match(/[\d.]+/g);
        return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`;
    }
    // 处理rgb格式
    if (color.startsWith('rgb')) {
        const values = color.match(/\d+/g);
        return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`;
    }
    // 处理十六进制格式
    if (color.startsWith('#')) {
        let hex = color.replace('#', '');
        // 处理简写的十六进制
        if (hex.length === 3) {
            hex = hex.split('').map(char => char + char).join('');
        }
        const r = parseInt(hex.slice(0, 2), 16);
        const g = parseInt(hex.slice(2, 4), 16);
        const b = parseInt(hex.slice(4, 6), 16);
        return `rgba(${r}, ${g}, ${b}, ${opacity})`;
    }
    return color;
}

未完成...