手搓日历组件,大屏样式最佳解决方案!

简介: 【10月更文挑战第6天】手搓日历组件,大屏样式最佳解决方案!

简介

大家好,我是石小石!在大屏开发中,我们可能会遇到日历组件的使用,然而,对于一些定制化的大屏,传统的第三方UI组件很难满足我们的样式要求。

这个时候,我们一般有两个选择:

  • 说服UI和老板,使用第三方组件(客户吐槽,最终可能要改)
  • 强行改组件库UI样式,让自己痛不欲生

实际上,大屏使用的日历组件大多只是展示使用,业务简单,我们手搓一个其实非常简单,最重要的是,样式完全可以自定义!

这篇文章,我将向大家展示如何封装一个vue日历组件,可以直接引入使用。它的样式功能如下

  • 基本日期展示
  • 时间切换
  • 禁用未来时间
  • 高亮当前样式
  • ....

核心思路分析

日历组件的核心其实是每页日期数据的生成,日期数据是一个7*6的一维列表,我们只要计算出这个列表的第一项日期数据,后面的日期依次加1即可。其次,就是一些简单的样式处理,非当前月的日期进行置灰即可。

页面的数据结构

为了便于页面的渲染,我们可以将7*6的日期列表封装到一个数组里,数组的每一行对应日期的每一行数据

为了便于后续的样式拓展,我们可以给每个日期对象增加一些数据,比如

{ 
  // 显示的标签
  label: '30', 
  // 标签对应的实际数据
  date: '2024-06-30', 
  // 是否被选中
  active: false, 
  // 是否当前日期
  isCurrent: false
}

那么,一个比较完整的日期列表数据,结构就应该如下

[
    {
        "time": "2024-06-30",
        "children": [
            {
                "label": "30",
                "date": "2024-06-30",
                "active": false,
                "isCurrent": false
            },
            {
                "label": "1",
                "date": "2024-07-01",
                "active": true,
                "isCurrent": false
            },
            {
                "label": "2",
                "date": "2024-07-02",
                "active": true,
                "isCurrent": false
            },
            {
                "label": "3",
                "date": "2024-07-03",
                "active": true,
                "isCurrent": false
            },
            {
                "label": "4",
                "date": "2024-07-04",
                "active": true,
                "isCurrent": false
            },
            {
                "label": "5",
                "date": "2024-07-05",
                "active": true,
                "isCurrent": false
            },
            {
                "label": "6",
                "date": "2024-07-06",
                "active": true,
                "isCurrent": false
            }
        ]
    },
    {   
        "time": "2024-07-07",//第二行起始日期
        "children": []       // 内容省略,格式同第一条数据
    },
    {
        "time": "2024-07-14",//第三行起始日期
        "children": []
    },
    {
        "time": "2024-07-21",//第四行起始日期
        "children": []
    },
    {
        "time": "2024-07-28",//第五行起始日期
        "children": []
    },
    {
        "time": "2024-08-04",//第六行起始日期
       "children": []
    }
]

如何生成日期列表

要生成日期列表,最核心的就是找到当前选中年月的日历面板的起始日期。

比如,我们选中2024年7月,我们的核心就是计算出当前面板的第一个标签的时间。

要计算当前日历面板的第一个时间,我们可以先找到当前选中年月的1号时间

const year = ref(dayjs().year())
const month = ref(dayjs().month() + 1)
// 获取当前选中月份的1号
let selectDay = `${year.value}-${month.value}-01`

然后,我们可以计算出选中月是周几,便可以计算出起始时间

// 选中时间是周几
const weekDay = dayjs(selectDay).day()
// 日历组件的起始日期
const firstDay = dayjs(selectDay).subtract(weekDay, 'day')

接下来,我们遍历生成整个日期列表即可

// 生成日期列表
const getTimeListByYearAndMonth = () => {
    // 获取当前选中月份的1号
    let selectDay = `${year.value}-${month.value}-01`;  // 构造当前选中年份和月份的第一天日期字符串
    // 选中时间是周几
    const weekDay = dayjs(selectDay).day();  // 获取当前选中日期是星期几(0-6,周日为0)
    // 日历组件的起始日期
    const firstDay = dayjs(selectDay).subtract(weekDay, 'day');  // 计算出日历显示的第一天,减去当前星期几的天数,得到上个月的最后几天或本月的第一天
    // 初始化日期列表数组
    const dayList = [];  
    // 遍历6周
    for (let i = 0; i < 6; i++) {
        // 初始化每周的日期数组
        const childrenList = [];  
        // 遍历每周的7天
        for (let time = 0; time < 7; time++) {
            // 计算当前日期
            let day = dayjs(firstDay).add(i * 7 + time, "day");  
            // 格式化当前日期为字符串
            const date = day.format("YYYY-MM-DD");  
            childrenList.push({
                label: day.format("D"),  // 获取当前日期的日
                date,  // 当前日期的完整格式
                // 判断当前日期是否属于选中的月份
                active: Number(day.format("M")) == month.value,  
                // 判断当前日期是否是今天
                isCurrent: date === dayjs().format("YYYY-MM-DD")  
            });
        }
        dayList.push({
            // 当前周的第一天日期
            time: dayjs(firstDay).add(i * 7, "day").format("YYYY-MM-DD"), 
            // 当前周的所有日期
            children: childrenList  
        });
    }
    // 返回生成的日期列表
    return dayList;  
}

技术方案

基础样式搭建

日历组件的样式其实非常简单,无非就是头部切换功能样式,和主体日历时间部分样式,这部分大家完全可以按照业务情况自己开发。

组件参数设计

作为一个公用组件,一些核心的组件props参数是必不可少的,这个组件可以传递默认选中的年份year、月份month,以及一些灵活的拓展参数:是否禁用未来日期disabledFutureDay、是否高亮当前日期highlightCurrentDay等等。

相应的,它还需要一些核心的emit事件抛出,如年份切换事件yearChange、月份切换事件 monthChange、日期切换事件dateChange。

interface Props {
    year?: number
    month?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
    // 是否禁用未来日期
    disabledFutureDay?: boolean
    // 是否高亮当前日期
    highlightCurrentDay?: boolean
}
interface TimeEmitInfo {
    year: number, month: number, date: number | null, time: string
}
const props = withDefaults(defineProps<Props>(), {
    year: dayjs().year(),
    month: dayjs().month() + 1,
    disabledFutureDay: true,
    highlightCurrentDay: true,
})
const emit = defineEmits<{
    yearChange: [params: TimeEmitInfo],
    monthChange: [params: TimeEmitInfo],
    dateChange: [params: TimeEmitInfo]
}>()

日期生成与页面渲染

日期的生成我们在【思路分析】章节已经详细分析过,结合数据渲染,他的页面结构和核心代码如下

<template>
    <div class="schedule-calendar">
        <!--日历组件年月栏-->
        <div class="time-switch-tool">
            
        </div>
        <!--日历组件星期显示区域-->
        <div class="week-day">
            <span>日</span><span>一</span><span>二</span><span>三</span>
            <span>四</span><span>五</span><span>六</span>
        </div>
        <!--日历组件时间遍历生成-->
        <div class="calendar-wrap">
            <div class="day-list" v-for="time in dayList  ">
                <div class="day-cell" v-for="item in time.children" @click="selectDay(item.date)">
                    <span class="day">
                        {{ item.label }}
                    </span>
                </div>
            </div>
        </div>
    </div>
</template>
<script lang="ts" setup>   
import dayjs from "dayjs";
import weekOfYear from "dayjs/plugin/weekOfYear"
dayjs.extend(weekOfYear);
interface Props {
    year?: number
    month?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
    // 是否禁用未来日期
    disabledFutureDay?: boolean
    // 是否高亮当前日期
    highlightCurrentDay?: boolean
}
interface TimeEmitInfo {
    year: number, month: number, date: number | null, time: string
}
const props = withDefaults(defineProps<Props>(), {
    year: dayjs().year(),
    // @ts-ignore
    month: dayjs().month() + 1,
    disabledFutureDay: true,
    highlightCurrentDay: true,
})
const emit = defineEmits<{
    yearChange: [params: TimeEmitInfo],
    monthChange: [params: TimeEmitInfo],
    dateChange: [params: TimeEmitInfo]
}>()
// #选中年
const year = ref(props.year)
// #选中月
const month = ref(props.month)
// #选中的日
const selectDate = ref<null | string>(null)
// 选中日期后的事件处理
const selectDay = ()=>{}
// #生成日期列表
const getTimeListByYearAndMonth = () => {
    // !清空选中日期
    selectDate.value = null
    // 获取当前选中月份的1号
    let selectDay = `${year.value}-${month.value}-01`
    // 选中时间是周几
    const weekDay = dayjs(selectDay).day()
    // 日历组件的起始日期
    const firstDay = dayjs(selectDay).subtract(weekDay, 'day')
    const dayList = []
    for (let i = 0; i < 6; i++) {
        const childrenList = []
        for (let time = 0; time < 7; time++) {
            let day = dayjs(firstDay).add(i * 7 + time, "day")
            const date = day.format("YYYY-MM-DD")
            childrenList.push(
                { label: day.format("D"), date, active: Number(day.format("M")) == month.value, isCurrent: date === dayjs().format("YYYY-MM-DD") }
            )
        }
        dayList.push({
            time: dayjs(firstDay).add(i * 7, "day").format("YYYY-MM-DD"),
            children: childrenList
        })
    }
    console.log('dayList: ', dayList);
    return dayList
}
const dayList = ref(getTimeListByYearAndMonth())

上述代码基本就是日历组件的核心部分。注意,我们引入了dayjs用于处理时间,同时,引入了它的weekOfYear包用于处理日期周相关的逻辑。

逻辑功能实现

日期切换事件

切换日期后,我们应该告诉父组件我们当前选择的时间。

const selectDay = (day: string) => {
    if (day === selectDate.value) {
        selectDate.value = null
    } else {
        selectDate.value = day
    }
    emit("dateChange", day)
}

根据页面的遍历逻辑,上述代码中的day是一个具体日期的时间字符如:2024-06-30

实际上,我们传递给父组件的数据可以更加详细具体些,以便上层进行业务逻辑处理。我们可以将选中的时间进行更加详细的封装:

const emitTimeInfo = computed(() => {
    const time = selectDate.value || `${year.value}-${month.value}-01`;
    return { year: year.value, month: month.value, date: selectDate.value ? Number(dayjs(selectDate.value).format("D")) : null, time }
})
const selectDay = (day: string) => {
    if (day === selectDate.value) {
        selectDate.value = null
    } else {
        selectDate.value = day
    }
    emit("dateChange", emitTimeInfo.value)
}

年月切换

年月的切换,核心就是重新计算日历的起始时间,我们在点击的时候重新调用getTimeListByYearAndMonth方法给要渲染的列表dayList赋值即可。

// #增加年
const addYear = () => {
    if (disabledYear.value) return
    year.value && year.value++
    // !不可点击未来月时,避免时间溢出
    if (props.disabledFutureDay && month.value && month.value > dayjs().month() + 1) {
        // @ts-ignore
        month.value = dayjs().month() + 1
    }
    dayList.value = getTimeListByYearAndMonth()
    emit("yearChange", emitTimeInfo.value)
}
// 减少年
const minusYear = () => {
    year.value && year.value--
    dayList.value = getTimeListByYearAndMonth()
    emit("yearChange", emitTimeInfo.value)
}
// #增加月
const addMonth = () => {
    if (disabledMonth.value) return
    if (month.value == 12) {
        month.value = 1
        year.value && year.value++
    } else {
        month.value && month.value++
    }
    dayList.value = getTimeListByYearAndMonth()
    emit("monthChange", emitTimeInfo.value)
}
// 减少月
const minusMonth = () => {
    if (month.value == 1) {
        year.value && year.value--
        month.value = 12
    } else {
        month.value && month.value--
    }
    dayList.value = getTimeListByYearAndMonth()
    emit("monthChange", emitTimeInfo.value)
}

一些其他的样式逻辑

为了完善日历组件的样式,我们可以增加一些自定义的计算属性,用于控制样式。

// #选中年是否当前年
const isLastYear = computed(() => year.value === new Date().getFullYear())
// #选中月是否当前月
const isLastMonth = computed(() => isLastYear.value && month.value === dayjs().month() + 1)
// #年份禁用
const disabledYear = computed(() => props.disabledFutureDay && isLastYear.value)
// #月份禁用
const disabledMonth = computed(() => props.disabledFutureDay && isLastMonth.value)

完整代码

<template>
    <div class="schedule-calendar">
        <div class="time-switch-tool">
            <div class="time-tool">
                <IconMeriComponentArrowLeft class="icon" color="#8B949E" @click.stop="minusYear">
                </IconMeriComponentArrowLeft>
                <m-button type="text" size="small">
                    <div class="time">{{ year }}年</div>
                </m-button>
                <IconMeriComponentArrowRight class="icon" :class="{ disabled: isLastYear && disabledFutureDay }"
                    :color="disabledYear ? '' : '#8B949E'" @click.stop="addYear">
                </IconMeriComponentArrowRight>
            </div>
            <div class="time-tool">
                <IconMeriComponentArrowLeft class="icon" color="#8B949E" @click.stop="minusMonth">
                </IconMeriComponentArrowLeft>
                <m-button type="text" size="small">
                    <div class="time">{{ month }}月</div>
                </m-button>
                <IconMeriComponentArrowRight class="icon" :class="{ disabled: disabledMonth }"
                    :color="disabledMonth ? '' : '#8B949E'" @click.stop="addMonth">
                </IconMeriComponentArrowRight>
            </div>
        </div>
        <div class="week-day">
            <span>日</span><span>一</span><span>二</span><span>三</span>
            <span>四</span><span>五</span><span>六</span>
        </div>
        <div class="calendar-wrap">
            <div class="day-list" v-for="time in dayList  ">
                <div class="day-cell" v-for="item in time.children   " :class="{ select: item.date === selectDate }"
                    @click="selectDay(item.date)">
                    <span class="day"
                        :class="[{ active: item.active }, { current: item.isCurrent && props.highlightCurrentDay }]">
                        {{ item.label }}
                    </span>
                    <span :class="{ dot: isShowDot(item.date) }"></span>
                </div>
            </div>
        </div>
    </div>
</template>
<script lang="ts" setup>
import { IconMeriComponentArrowLeft, IconMeriComponentArrowRight } from "xxx-icon";
import dayjs from "dayjs";
import weekOfYear from "dayjs/plugin/weekOfYear"
dayjs.extend(weekOfYear);
interface Props {
    year?: number
    month?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
    // 是否禁用未来日期
    disabledFutureDay?: boolean
    // 是否高亮当前日期
    highlightCurrentDay?: boolean
    // 选中的日期列表
    dotDateList?: string[]
}
interface TimeEmitInfo {
    year: number, month: number, date: number | null, time: string
}
const props = withDefaults(defineProps<Props>(), {
    year: dayjs().year(),
    // @ts-ignore
    month: dayjs().month() + 1,
    disabledFutureDay: true,
    highlightCurrentDay: true,
    dotDateList: () => []
})
const emit = defineEmits<{
    yearChange: [params: TimeEmitInfo],
    monthChange: [params: TimeEmitInfo],
    dateChange: [params: TimeEmitInfo]
}>()
// #选中年
const year = ref(props.year)
// #选中月
const month = ref(props.month)
// #选中的日
const selectDate = ref<null | string>(null)
const emitTimeInfo = computed(() => {
    const time = selectDate.value || `${year.value}-${month.value}-01`;
    return { year: year.value, month: month.value, date: selectDate.value ? Number(dayjs(selectDate.value).format("D")) : null, time }
})
const selectDay = (day: string) => {
    if (day === selectDate.value) {
        selectDate.value = null
    } else {
        selectDate.value = day
    }
    emit("dateChange", emitTimeInfo.value)
}
// #生成日期列表
const getTimeListByYearAndMonth = () => {
    // !清空选中日期
    selectDate.value = null
    // 获取当前选中月份的1号
    let selectDay = `${year.value}-${month.value}-01`
    // 选中时间是周几
    const weekDay = dayjs(selectDay).day()
    // 日历组件的起始日期
    const firstDay = dayjs(selectDay).subtract(weekDay, 'day')
    const dayList = []
    for (let i = 0; i < 6; i++) {
        const childrenList = []
        for (let time = 0; time < 7; time++) {
            let day = dayjs(firstDay).add(i * 7 + time, "day")
            const date = day.format("YYYY-MM-DD")
            childrenList.push(
                { label: day.format("D"), date, active: Number(day.format("M")) == month.value, isCurrent: date === dayjs().format("YYYY-MM-DD") }
            )
        }
        dayList.push({
            time: dayjs(firstDay).add(i * 7, "day").format("YYYY-MM-DD"),
            children: childrenList
        })
    }
    console.log('dayList: ', dayList);
    return dayList
}
const dayList = ref(getTimeListByYearAndMonth())
// #选中年是否当前年
const isLastYear = computed(() => year.value === new Date().getFullYear())
// #选中月是否当前月
const isLastMonth = computed(() => isLastYear.value && month.value === dayjs().month() + 1)
// #年份禁用
const disabledYear = computed(() => props.disabledFutureDay && isLastYear.value)
// #月份禁用
const disabledMonth = computed(() => props.disabledFutureDay && isLastMonth.value)
// #增加年
const addYear = () => {
    if (disabledYear.value) return
    year.value && year.value++
    // !不可点击未来月时,避免时间溢出
    if (props.disabledFutureDay && month.value && month.value > dayjs().month() + 1) {
        // @ts-ignore
        month.value = dayjs().month() + 1
    }
    dayList.value = getTimeListByYearAndMonth()
    emit("yearChange", emitTimeInfo.value)
}
// 减少年
const minusYear = () => {
    year.value && year.value--
    dayList.value = getTimeListByYearAndMonth()
    emit("yearChange", emitTimeInfo.value)
}
// #增加月
const addMonth = () => {
    if (disabledMonth.value) return
    if (month.value == 12) {
        month.value = 1
        year.value && year.value++
    } else {
        month.value && month.value++
    }
    dayList.value = getTimeListByYearAndMonth()
    emit("monthChange", emitTimeInfo.value)
}
// 减少月
const minusMonth = () => {
    if (month.value == 1) {
        year.value && year.value--
        month.value = 12
    } else {
        month.value && month.value--
    }
    dayList.value = getTimeListByYearAndMonth()
    emit("monthChange", emitTimeInfo.value)
}
// 是否展示dot
const isShowDot = (date: string) => {
    if (props.dotDateList?.length === 0) return false
    return props.dotDateList?.find((res) => dayjs(res).format("YYYY-MM-DD") === dayjs(date).format("YYYY-MM-DD"))
}
</script>
<style lang="less" scoped>
.schedule-calendar {
    user-select: none;
    .time-switch-tool {
        height: 22px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        .time-tool {
            .icon {
                width: 16px;
                height: 16px;
                color: #8B949E;
                cursor: pointer;
                &.disabled {
                    cursor: not-allowed;
                }
            }
            .time {
                color: #1B2129;
                text-align: center;
                font-size: 14px;
                font-weight: 400;
                line-height: 22px;
                margin: 0 4px;
                user-select: none;
            }
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
    }
    .week-day {
        margin-top: 7px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        height: 28px;
        span {
            display: block;
            width: 28px;
            color: #8B949E;
            text-align: center;
            font-size: 14px;
            font-weight: 400;
            line-height: 22px;
        }
    }
    .calendar-wrap {
        .day-list {
            display: flex;
            margin-top: 4px;
            height: 28px;
            justify-content: space-between;
            align-items: center;
            .day-cell {
                width: 28px;
                height: 28px;
                position: relative;
                >span {
                    display: block;
                    text-align: center;
                    font-size: 14px;
                    font-weight: 400;
                    line-height: 28px;
                    cursor: pointer;
                    color: #C4C9CF;
                    &.active {
                        color: #2E3742;
                    }
                    &.current {
                        color: #246FE5;
                    }
                }
                &:hover {
                    background: #E8ECF0;
                    border-radius: 5px;
                }
                .dot {
                    position: absolute;
                    width: 4px;
                    height: 4px;
                    border-radius: 50%;
                    background: #246FE5;
                    bottom: 1px;
                    left: 12px;
                }
            }
            .select {
                border-radius: 4px;
                background: #246FE5;
                .day {
                    color: #FFF !important;
                }
                &:hover {
                    background: #246FE5;
                    border-radius: 4px;
                }
                .dot {
                    background: #FFF;
                }
            }
        }
    }
}
</style>

注意,上述代码中的年月切换箭头采用了其他UI库,实际使用时,需要进行替换。

效果演示

总结

本文分享了一个日历组件的实现方案,样式、功能都比较完备,是一个可以直接在项目中使用的组件。 大家可以根据自己的业务需求、样式进行适当调整。

相关文章
|
4月前
|
JavaScript
手搓日历组件,大屏样式最佳解决方案!
【8月更文挑战第23天】手搓日历组件,大屏样式最佳解决方案!
44 1
手搓日历组件,大屏样式最佳解决方案!
|
5月前
|
数据可视化 前端开发 开发者
花样玩转“所见即所得”的可视化开发UI
【7月更文挑战第12天】WYSIWYG)的可视化开发UI带来的便利与创新: 降低开发门槛: 即使无编程基础也能通过直观操作快速构建界面。 提高开发效率: 实时预览减少代码与预览间的频繁切换。 促进团队协作: 设计师与开发者可在同一界面交流修改。 增加创意实现: 自由尝试布局、颜色与交互方式以验证想法。 此类工具(如Adobe XD、Figma、Sketch等)正变革软件开发方式,带来更高效、具创意及易操作的体验。
|
4月前
|
前端开发 UED
设计新潮流:CSS动画毛玻璃按钮,展开效果引人入胜!
设计新潮流:CSS动画毛玻璃按钮,展开效果引人入胜!
|
5月前
|
前端开发
网页设计04 WEB端按钮设计技巧,交互规范参考资料,点击事件,什么是3:1 ,3:1 的介绍,网页设计牵扯的知识整理成口诀
网页设计04 WEB端按钮设计技巧,交互规范参考资料,点击事件,什么是3:1 ,3:1 的介绍,网页设计牵扯的知识整理成口诀
|
前端开发 数据可视化
漏刻有时数据大屏CSS样式表成长教程(4):数据指标层叠排版
漏刻有时数据大屏CSS样式表成长教程(4):数据指标层叠排版
87 0
|
数据可视化 前端开发
漏刻有时数据大屏CSS样式表成长教程(2):九宫格图表背景自适应的解决方案
漏刻有时数据大屏CSS样式表成长教程(2):九宫格图表背景自适应的解决方案
141 1
Echarts实战案例代码(29):来找茬,柱状图在F11全屏下显示三角形的异形情况案例
Echarts实战案例代码(29):来找茬,柱状图在F11全屏下显示三角形的异形情况案例
122 0
五款炫酷精美动态登录页面,彩虹气泡动态云层深海灯光水母炫酷星空蛛网HTMLCSS源码
五款炫酷精美动态登录页面,彩虹气泡动态云层深海灯光水母炫酷星空蛛网HTMLCSS源码
156 0
|
编解码 前端开发 JavaScript
手摸手带你实现一个时间轴组件
本文给大家带来一个时间轴的组件开发教程
765 0
|
iOS开发
iOS开发 - 柱状图动态展现动画
iOS开发 - 柱状图动态展现动画
171 0
iOS开发 - 柱状图动态展现动画
下一篇
DataWorks