怎么開發(fā)一個(gè)自定義日歷的vue組件,下面本篇文章就手把手教你如何封裝一個(gè)自定義日歷組件,希望對(duì)大家有所幫助!
眾所周知啊,一般來說,如果項(xiàng)目中有需要用到日歷組件,往往是找第三方UI庫(kù)中的組件來使用,或者是找現(xiàn)成的其他第三方插件。對(duì)于很多小伙伴來說,第一眼看到日歷組件,下意識(shí)的就會(huì)覺得很復(fù)雜,無從下手。但是當(dāng)我閱讀了這個(gè)日歷插件的源碼之后,發(fā)現(xiàn)并沒有我想象中的復(fù)雜。我以前傻傻得認(rèn)為,想要做一個(gè)日歷組件,得需要把距離現(xiàn)在年份前后至少十年的日歷數(shù)據(jù)都獲取到,然后才能進(jìn)行下一步的開發(fā)。
然而,在我嘗試著閱讀了dycalendar.js這個(gè)庫(kù)的源碼之后,一方面感覺自己太笨了,把問題想得太復(fù)雜了。另外也感慨作者思路之清晰。看完之后感覺受益匪淺。
在將作者的思路邏輯梳理完畢后,我依據(jù)這個(gè)思路開發(fā)了一個(gè)vue組件。如下圖所示:
接下來,就隨著我一起看看如何開發(fā)一個(gè)自己的日歷組件吧。
核心代碼實(shí)現(xiàn)
1、梳理思路
獲取到目標(biāo)日期數(shù)據(jù)
獲取到當(dāng)前日期的各項(xiàng)重要屬性,諸如當(dāng)前年
,當(dāng)前月
,當(dāng)前日期
,當(dāng)前星期幾
,當(dāng)前月一共有幾天
,當(dāng)前月的第一天對(duì)應(yīng)的是星期幾
,上個(gè)月總共有多少天
等。
根據(jù)這些屬性,來生成具體的日歷日期數(shù)據(jù)列表
,然后將其循環(huán)渲染到模板中。
當(dāng)切換月份的時(shí)候,獲取到新的目標(biāo)日期對(duì)應(yīng)的各項(xiàng)關(guān)鍵數(shù)據(jù)。vue檢測(cè)到日歷屬性變化之后,通知頁(yè)面進(jìn)行更新。
2、初始化所需要的數(shù)據(jù)
一般來說,成熟的日歷組件,日期都是一個(gè)雙向綁定的變量。為了方便使用,我們也采用雙向綁定的方式。
<script setup> import { reactive, ref, computed, watch } from "vue"; const props = defineProps({ modelValue: Date, }); const emits = defineEmits(["update:modelValue"]); /** * 最小年份 */ const MIN_YEAR = 1900; /** * 最大年份 */ const MAX_YEAR = 9999; /** * 目標(biāo)日期 */ const targetDate = ref(props.modelValue);
接下來,我們還需要初始化一些常量用來表示月份和日期:
/** * 有關(guān)月度的名稱列表 */ const monthNameList = { chineseFullName: [ "一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月", ], fullName: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ], mmm: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ], }; /** * 有關(guān)周幾的名稱列表 */ const dayNameList = [ { chineseFullName: "周日", chineseShortName: "日", fullName: "Sunday", shortName: "Sun", dayNumber: 0, }, { chineseFullName: "周一", chineseShortName: "一", fullName: "Monday", shortName: "Mon", dayNumber: 1, }, { chineseFullName: "周二", chineseShortName: "二", fullName: "Tuesday", shortName: "Tue", dayNumber: 2, }, { chineseFullName: "周三", chineseShortName: "三", fullName: "Wednesday", shortName: "Wed", dayNumber: 3, }, { chineseFullName: "周四", chineseShortName: "四", fullName: "Thursday", shortName: "Thu", dayNumber: 4, }, { chineseFullName: "周五", chineseShortName: "五", fullName: "Friday", shortName: "Fri", dayNumber: 5, }, { chineseFullName: "周六", chineseShortName: "六", fullName: "Saturday", shortName: "Sat", dayNumber: 6, }, ];
接下來,準(zhǔn)備幾個(gè)vue的響應(yīng)式數(shù)據(jù):
/** * 今日 */ const today = new Date(); /** * 日歷的各項(xiàng)屬性 */ const calendarProps = reactive({ target: { year: null, month: null, date: null, day: null, monthShortName: null, monthFullName: null, monthChineseFullName: null, firstDay: null, firstDayIndex: null, totalDays: null, }, previous: { totalDays: null, }, }); /** * 用于展現(xiàn)的日歷數(shù)據(jù) */ const calendarData = ref([]);
3、初始化日歷的各項(xiàng)屬性
接下來,通過setCalendarProps
方法獲取日歷的各個(gè)屬性,逐個(gè)填充calendarProps
中的數(shù)據(jù):
function setCalendarProps() { if (!targetDate.value) { targetDate.value = today; } // 獲取目標(biāo)日期的年月日星期幾數(shù)據(jù) calendarProps.target.year = targetDate.value.getFullYear(); calendarProps.target.month = targetDate.value.getMonth(); calendarProps.target.date = targetDate.value.getDate(); calendarProps.target.day = targetDate.value.getDay(); if ( calendarProps.target.year < MIN_YEAR || calendarProps.target.year > MAX_YEAR ) { console.error("無效的年份,請(qǐng)檢查傳入的數(shù)據(jù)是否是正常"); return; } // 獲取到目標(biāo)日期的月份【中文】名稱 let dateString; dateString = targetDate.value.toString().split(" "); calendarProps.target.monthShortName = dateString[1]; calendarProps.target.monthFullName = monthNameList.fullName[calendarProps.target.month]; calendarProps.target.monthChineseFullName = monthNameList.chineseFullName[calendarProps.target.month]; // 獲取目標(biāo)月份的第一天是星期幾,和在星期幾中的索引值 const targetMonthFirstDay = new Date( calendarProps.target.year, calendarProps.target.month, 1 ); calendarProps.target.firstDay = targetMonthFirstDay.getDay(); calendarProps.target.firstDayIndex = dayNameList.findIndex( (day) => day.dayNumber === calendarProps.target.firstDay ); // 獲取目標(biāo)月份總共多少天 const targetMonthLastDay = new Date( calendarProps.target.year, calendarProps.target.month + 1, 0 ); calendarProps.target.totalDays = targetMonthLastDay.getDate(); // 獲取目標(biāo)月份的上個(gè)月總共多少天 const previousMonth = new Date( calendarProps.target.year, calendarProps.target.month, 0 ); calendarProps.previous.totalDays = previousMonth.getDate(); }
需要注意的一個(gè)知識(shí)點(diǎn)是,在獲取本月多少天和上個(gè)月多少天的時(shí)候,都將date值設(shè)置為了
0
。這是因?yàn)楫?dāng)date值為0
的時(shí)候,返回的Date對(duì)象是上個(gè)月的最后一天。所以說,為了獲取本月多少天,需要將本月的month值加1
。
執(zhí)行這個(gè)方法之后,此時(shí)calendarProps
的值為:
4、根據(jù)日歷屬性生成日歷日期的數(shù)據(jù)
當(dāng)我們已經(jīng)知道本月第一天對(duì)應(yīng)的周幾索引值、本月一共有多少天和上個(gè)月一共有多少天這三個(gè)核心數(shù)據(jù)之后,就可以開始生成對(duì)應(yīng)的日歷數(shù)據(jù)了。
思路如下:
由于大部分情況下,本月的第一天不是從頭開始的,之前的部分是上個(gè)月的日期。所以第一行要單獨(dú)進(jìn)行處理。
設(shè)置一個(gè)公用的date數(shù)值,初始值設(shè)置為1
。然后從本月第一天對(duì)應(yīng)的周幾索引值開始進(jìn)行遞增。本月之前的日期和之后的日期設(shè)置一個(gè)算法進(jìn)行計(jì)算。
為了方便之后進(jìn)行日期切換、樣式區(qū)分,將生成的數(shù)據(jù)加工成一個(gè)對(duì)象,其中包含日期類型——dateType
,表示是本月還是上月還是下月;
/** * 生成日歷的數(shù)據(jù) */ function setCalendarData() { let i; let date = 1; const originData = []; const firstRow = []; // 設(shè)置第一行數(shù)據(jù) for (i = 0; i <= 6; i++) { // 設(shè)置目標(biāo)月份之前月份的日期數(shù)據(jù) if (i < calendarProps.target.firstDayIndex) { const previousDate = calendarProps.previous.totalDays - calendarProps.target.firstDayIndex + (i + 1); firstRow.push({ dateObj: new Date( calendarProps.target.year, calendarProps.target.month - 1, previousDate ), dateNumber: previousDate, dateType: "previous" }); } else { // 設(shè)置目標(biāo)月份當(dāng)月的日期數(shù)據(jù) firstRow.push({ dateObj: new Date( calendarProps.target.year, calendarProps.target.month, date ), dateNumber: date, dateType: "current" }); date++; } } originData.push(firstRow); // 設(shè)置后面五行的數(shù)據(jù) for (let j = 0; j <= 4; j++) { const rowData = []; for (let k = 0; k <= 6; k++) { // 設(shè)置目標(biāo)月份剩下的日期數(shù)據(jù) if (date <= calendarProps.target.totalDays) { rowData.push({ dateObj: new Date( calendarProps.target.year, calendarProps.target.month, date ), dateNumber: date, dateType: "current" }); } else { // 設(shè)置目標(biāo)月份下個(gè)月的日期數(shù)據(jù) const nextDate = date - calendarProps.target.totalDays; rowData.push({ dateObj: new Date( calendarProps.target.year, calendarProps.target.month + 1, nextDate ), dateNumber: nextDate, dateType: "next" }); } date++; } originData.push(rowData); } calendarData.value = originData; }
至此,這個(gè)日歷組件的核心部分的邏輯就已經(jīng)實(shí)現(xiàn)了。你看,是不是很簡(jiǎn)單?
接下來,我們只需要根據(jù)calendarData
中的數(shù)據(jù)渲染出相應(yīng)的html模板和添加上樣式就可以了。
5、添加模板和樣式部分
一般來說,日歷組件都是網(wǎng)格狀的結(jié)構(gòu),所以我選擇table的方式進(jìn)行渲染。不過你要是問我還有沒有別的方式,那還是有的,比如使用flex布局或者grid布局,但是如果采用這種方式的話,calendarData
的數(shù)據(jù)結(jié)構(gòu)就不是現(xiàn)在這個(gè)樣子了。
dom結(jié)構(gòu)如下圖:
至于按鈕邊框的流動(dòng)效果,是我參照蘇蘇的文章做的,詳情請(qǐng)見:
Clip-path實(shí)現(xiàn)按鈕流動(dòng)邊框動(dòng)畫 juejin.cn/post/719877…
然后剩下的樣式部分,即興發(fā)揮或者根據(jù)UI設(shè)計(jì)圖繪制即可。想必各位都領(lǐng)教過UI姐姐們精美的設(shè)計(jì)圖吧(嘻嘻
具體的代碼部分就不貼在文章中了,如有需要可以直接查看下方的完整源碼
gitee.com/wushengyuan…
結(jié)語(yǔ)
有些感覺很麻煩的組件,可能核心邏輯往往不是那么復(fù)雜。有些時(shí)候,可能僅僅是需要一些耐心,將代碼一行一行的拆解出來閱讀,理清楚其中的思路。