如何基于Grid布局完成最精簡的日期組件
需求背景
咱們在日常開發(fā)中,日期或日歷組件幾乎是每個(gè)前端開發(fā)者都會(huì)遇到的需求。
大多數(shù)時(shí)候我們的項(xiàng)目中肯定有一個(gè)組件庫,如element-plus, antd 等等,這些組件庫基本都是有日期這類標(biāo)配組件,或者市面上也有很多現(xiàn)成庫,能夠輕松解決這類需求。
但是,如遇到比較有理想的產(chǎn)品經(jīng)理時(shí)??,現(xiàn)成組件或魔改組件庫可能都無法滿足,此時(shí)自定義組件就很有必要。日期組件開發(fā)細(xì)節(jié)非常多,看似麻煩,但我們可以 "抽繁尋簡",先聚焦解決其核心部分,接下來咱就一起來開啟這段奇妙旅程吧,嘿嘿。
基礎(chǔ)教學(xué)
第一步:搭建基礎(chǔ)HTML結(jié)構(gòu)
咱們從最簡單的布局來看,為了簡單好理解,結(jié)構(gòu)直接復(fù)制就行哈。
<!DOCTYPE html>
<html>
<body>
<div class="calendar-wrapper">
<h1>2025年08月</h1>
<ul class="calendar">
<li class="weekday">一</li>
<li class="weekday">二</li>
<li class="weekday">三</li>
<li class="weekday">四</li>
<li class="weekday">五</li>
<li class="weekday">六</li>
<li class="weekday">日</li>
<li class="first-day">1</li>
<li>2</li>
<li class="today">3</li>
<li>4</li>
<li>5</li>
<!-- 更多日期... -->
<li>31</li>
</ul>
</div>
</body>
</html>
這里小編用了 ul 和 li 標(biāo)簽來構(gòu)建日期結(jié)構(gòu),讓它語義化更好,也便于 CSS Grid 布局。
第二步:CSS Grid 布局
樣式咱們這里我們采用 Grid 布局,它能夠快速來完成所需布局,其他方式當(dāng)然也可以,如 Flex 等。
body {
padding: 0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.calendar-wrapper {
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
width: 400px;
margin-top: 100px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
margin-top: 0;
}
/* ?? 核心Grid布局 - 就這3行! */
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
list-style: none;
padding: 0;
margin: 0;
}
/* 8月1日從星期四開始 */
.first-day {
grid-column-start: 5;
}
.calendar li {
aspect-ratio: 1; /* 保持正方形 */
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
}
.weekday {
background-color: #190f01;
color: #fff;
margin-bottom: 2px;
}
.calendar li:not(.weekday):hover {
background-color: #f4b225;
color: #fff;
transform: scale(1.1);
cursor: pointer;
}
.today {
background-color: #f4b225;
color: #fff;
}
效果:

非常簡單的就完成日期組件的布局!?? 核心就五行代碼:
display: grid: 啟用 Grid 布局grid-template-columns: repeat(7, 1fr): 創(chuàng)建7列等寬布局(一周7天)。gap: 1px: 設(shè)置網(wǎng)格間距。aspect-ratio: 1:讓每個(gè)日期格子保持正方形,無論屏幕大小如何變化都能保持完美比例!?grid-column-start: 5:讓月份的第一天從正確的星期位置開始顯示。比如8月1日是星期五,那就從第5列開始!
高級教學(xué)
靜態(tài)日歷的基本布局就此完成啦,接下來,咱們用上 JS 來給它注入點(diǎn)靈魂。
我們要完成的功能有??:
- 動(dòng)態(tài)來生成 HTML 結(jié)構(gòu)。
- 補(bǔ)全當(dāng)前月份的上下月份的天數(shù)。
- 切換月份。
- 選擇日期。
首先,我們需要一個(gè)更完整的HTML結(jié)構(gòu):
<div class="calendar-wrapper">
<div class="header">
<span class="arrow" id="prevMonth">?</span>
<h1 id="monthYear"></h1>
<span class="arrow" id="nextMonth">?</span>
</div>
<ul class="calendar" id="calendar"></ul>
</div>
<div class="selected-date-info" id="selectedDateInfo">點(diǎn)擊日期進(jìn)行選擇</div>
樣式調(diào)整:
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
h1 {
text-align: center;
color: #333;
margin: 0;
flex: 1;
}
.arrow {
cursor: pointer;
font-size: 24px;
color: #333;
padding: 10px;
border-radius: 50%;
transition: all 0.2s ease;
user-select: none;
}
.other-month {
color: #ccc;
}
.other-month:hover {
background-color: #e0e0e0;
color: #666;
}
然后,JS 的核心代碼,這里小編采用了 ES6 的 class 語法:
/**
* @name 日歷類,用于生成和管理日期或日歷相關(guān)的功能
*/
class Calendar {
constructor() {
this.currentDate = new Date();
this.today = new Date();
this.monthNames = [
'01月', '02月', '03月', '04月', '05月', '06月',
'07月', '08月', '09月', '10月', '11月', '12月'
];
this.weekdays = ['一', '二', '三', '四', '五', '六', '日'];
this.init();
}
/** @name 初始化 **/
init() {
this.bindEvents();
this.render();
}
/** @name 綁定事件 **/
bindEvents() {
document.getElementById('prevMonth').addEventListener('click', () => {
this.previousMonth();
});
document.getElementById('nextMonth').addEventListener('click', () => {
this.nextMonth();
});
}
/**
* @name 獲取某月的第一天是星期幾
* @param {number} year - 年份
* @param {number} month - 月份(0-11)
* @returns {number} 星期幾(調(diào)整為周一開始:0=周一,6=周日)
*/
getFirstDayOfMonth(year, month) {
const firstDay = new Date(year, month, 1).getDay();
// 調(diào)整為周一開始:周日(0)變成6,其他減1
return firstDay === 0 ? 6 : firstDay - 1;
}
/**
* @name 獲取某月的天數(shù)
* @param {number} year - 年份
* @param {number} month - 月份(0-11)
* @returns {number} 天數(shù)
*/
getDaysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
/**
* @name 檢查是否是今天
* @param {number} year - 年份
* @param {number} month - 月份(0-11)
* @param {number} day - 日期
* @returns {boolean} 是否是今天
*/
isToday(year, month, day) {
return year === this.today.getFullYear() &&
month === this.today.getMonth() &&
day === this.today.getDate();
}
/** @name 渲染組件 **/
render() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
// 更新標(biāo)題
document.getElementById('monthYear').textContent =
`${year}年${this.monthNames[month]}`;
// 清空
const calendar = document.getElementById('calendar');
calendar.innerHTML = '';
// 添加星期標(biāo)題
this.weekdays.forEach(day => {
const li = document.createElement('li');
li.className = 'weekday';
li.textContent = day;
calendar.appendChild(li);
});
// 獲取當(dāng)月信息
const firstDay = this.getFirstDayOfMonth(year, month);
const daysInMonth = this.getDaysInMonth(year, month);
const daysInPrevMonth = this.getDaysInMonth(year, month - 1);
// 添加上個(gè)月的尾部日期
for (let i = firstDay - 1; i >= 0; i--) {
const li = document.createElement('li');
li.className = 'other-month';
li.textContent = daysInPrevMonth - i;
calendar.appendChild(li);
}
// 添加當(dāng)月日期
for (let day = 1; day <= daysInMonth; day++) {
const li = document.createElement('li');
li.textContent = day;
if (this.isToday(year, month, day)) {
li.className = 'today';
}
calendar.appendChild(li);
}
// 添加下個(gè)月的開頭日期,補(bǔ)齊6行
const totalCells = calendar.children.length;
const remainingCells = 42 - totalCells; // 6行 × 7列 = 42個(gè)格子
for (let day = 1; day <= remainingCells; day++) {
const li = document.createElement('li');
li.className = 'other-month';
li.textContent = day;
calendar.appendChild(li);
}
}
/** @name 上一個(gè)月 **/
previousMonth() {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.render();
}
/** @name 下一個(gè)月 **/
nextMonth() {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.render();
}
}
new Calendar();
挺簡單哈,就一個(gè)類搞定,設(shè)計(jì)思路?:
- 構(gòu)造函數(shù)初始化基本數(shù)據(jù)。
init()方法負(fù)責(zé)初始化啟動(dòng)。render()方法負(fù)責(zé)渲染。bindEvents()方法負(fù)責(zé)事件綁定。
效果:

基于這個(gè)設(shè)計(jì)過程,咱們繼續(xù)來實(shí)現(xiàn)日期的點(diǎn)擊??選擇功能,這個(gè)稍微比較麻煩一些。
先把樣式整上:
.selected {
background-color: #007bff !important;
color: #fff;
box-shadow: 0 0 0 2px #007bff;
}
.selected.today {
background-color: #007bff;
box-shadow: 0 0 0 2px #f4b225;
}
.selected-date-info {
margin-top: 20px;
padding: 15px;
background-color: #f4b225;
color: #fff;
border-radius: 10px;
text-align: center;
font-size: 16px;
min-height: 20px;
font-weight: bold;
}
在 Calendar 類中添加選擇功能相關(guān)的屬性和方法:
class Calendar {
constructor() {
this.currentDate = new Date();
this.today = new Date();
this.selectedDate = null; // 新增:選中的日期
// ...
}
bindEvents() {
// ...
// 添加點(diǎn)擊的事件委托
document.getElementById('calendar').addEventListener('click', (e) => {
this.handleDateClick(e);
});
}
/**
* @name 處理日期點(diǎn)擊事件
* @param {Event} e - 點(diǎn)擊事件
*/
handleDateClick(e) {
const target = e.target;
// 只處理日期元素的點(diǎn)擊,排除星期標(biāo)題
if (target.tagName === 'LI' && !target.classList.contains('weekday')) {
const day = parseInt(target.textContent);
const year = this.currentDate.getFullYear();
let month = this.currentDate.getMonth();
// 處理其他月份的日期
if (target.classList.contains('other-month')) {
// 判斷是上個(gè)月還是下個(gè)月
const firstDay = this.getFirstDayOfMonth(year, month);
const daysInMonth = this.getDaysInMonth(year, month);
const totalCurrentMonthCells = firstDay + daysInMonth;
const clickedIndex = Array.from(target.parentNode.children).indexOf(target);
if (clickedIndex < firstDay + 7) { // 上個(gè)月
month = month - 1;
if (month < 0) {
month = 11;
year = year - 1;
}
} else { // 下個(gè)月
month = month + 1;
if (month > 11) {
month = 0;
year = year + 1;
}
}
}
// 設(shè)置選中日期
this.selectedDate = new Date(year, month, day);
// 更新顯示
this.updateSelectedDateDisplay();
this.updateCalendarSelection();
}
}
}
鼠標(biāo)點(diǎn)擊后,我們需要更新頁面的選中狀態(tài),小編用兩個(gè)方法來維護(hù)頁面UI的變化:
/** @name 更新選中日期的顯示 **/
updateSelectedDateDisplay() {
const infoElement = document.getElementById('selectedDateInfo');
if (this.selectedDate) {
const year = this.selectedDate.getFullYear();
const month = this.selectedDate.getMonth() + 1;
const day = this.selectedDate.getDate();
const weekDay = ['日', '一', '二', '三', '四', '五', '六'][this.selectedDate.getDay()];
infoElement.textContent = `選中日期: ${year}年${month.toString().padStart(2, '0')}月${day.toString().padStart(2, '0')}日 星期${weekDay}`;
} else {
infoElement.textContent = '點(diǎn)擊日期進(jìn)行選擇';
}
}
/** @name 更新選中狀態(tài) **/
updateCalendarSelection() {
const calendarItems = document.querySelectorAll('#calendar li:not(.weekday)');
// 清除所有選中狀態(tài)
calendarItems.forEach(item => {
item.classList.remove('selected');
});
// 如果有選中日期,標(biāo)記對應(yīng)的日期元素
if (this.selectedDate) {
const selectedYear = this.selectedDate.getFullYear();
const selectedMonth = this.selectedDate.getMonth();
const selectedDay = this.selectedDate.getDate();
const currentYear = this.currentDate.getFullYear();
const currentMonth = this.currentDate.getMonth();
// 只在當(dāng)前顯示的月份中標(biāo)記選中狀態(tài)
if (selectedYear === currentYear && selectedMonth === currentMonth) {
calendarItems.forEach(item => {
if (parseInt(item.textContent) === selectedDay && !item.classList.contains('other-month')) {
item.classList.add('selected');
}
});
}
}
}
為了方便外部調(diào)用,提供一些實(shí)用的 API 方法:
/**
* @name 獲取選中的日期
* @returns {Date|null} 選中的日期對象
*/
getSelectedDate() {
return this.selectedDate;
}
/**
* @name 設(shè)置選中的日期
* @param {Date} date - 要選中的日期
*/
setSelectedDate(date) {
this.selectedDate = date;
this.updateSelectedDateDisplay();
this.updateCalendarSelection();
}
/** @name 清除選中狀態(tài) **/
clearSelection() {
this.selectedDate = null;
this.updateSelectedDateDisplay();
this.updateCalendarSelection();
}
最后,由于天數(shù)是根據(jù)月份動(dòng)態(tài)渲染的,別忘了在月份切換時(shí)也要更新選中狀態(tài)的顯示:
previousMonth() {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.render();
// 重新渲染后更新選中狀態(tài)
this.updateCalendarSelection();
}
nextMonth() {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.render();
// 重新渲染后更新選中狀態(tài)
this.updateCalendarSelection();
}
效果:

總結(jié)
通過這篇文章,咱們從零開始實(shí)現(xiàn)了一個(gè)完整的日期組件,整個(gè)過程中,最讓小編印象深刻的就是 CSS Grid 的強(qiáng)大!在這種場景下,布局這塊絕對非它莫屬。??
然后呢,學(xué)會(huì)了基礎(chǔ)版本,你還可以繼續(xù)擴(kuò)展:
- 添加事件標(biāo)記: 在特定日期顯示小圓點(diǎn)。
- 日期選擇功能: 支持單選或多選日期。
- 農(nóng)歷顯示: 在公歷下方顯示農(nóng)歷。
- 主題切換: 支持多種顏色主題。
- 動(dòng)畫效果: 月份切換時(shí)的滑動(dòng)動(dòng)畫。
- ...
那就沒啦~??
到此這篇關(guān)于如何基于Grid布局完成最精簡的日期組件的文章就介紹到這了,更多相關(guān)Grid布局日期組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決bootstrap導(dǎo)航欄navbar在IE8上存在缺陷的方法
這篇文章主要為大家詳細(xì)介紹了解決bootstrap導(dǎo)航欄navbar在IE8上存在缺陷的方法,需要的朋友可以參考下2016-07-07
關(guān)于 byval 與 byref 的區(qū)別分析總結(jié)
關(guān)于 byval 與 byref 的區(qū)別分析總結(jié)...2007-10-10
BootStrap 獲得輪播中的索引和當(dāng)前活動(dòng)的焦點(diǎn)對象
這篇文章主要介紹了BootStrap 獲得輪播中的索引和當(dāng)前活動(dòng)的焦點(diǎn)對象,本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2017-05-05
ES6新特性:使用export和import實(shí)現(xiàn)模塊化詳解
本篇文章主要介紹了ES6新特性:使用export和import實(shí)現(xiàn)模塊化詳解,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07
js css 實(shí)現(xiàn)遮罩層覆蓋其他頁面元素附圖
實(shí)現(xiàn)遮罩層的方法有很多,但大多都是使用js css來實(shí)現(xiàn)的,本例也實(shí)現(xiàn)一個(gè),效果還不錯(cuò),喜歡的朋友可以感受下2014-09-09
JS使用位運(yùn)算實(shí)現(xiàn)權(quán)限組合的代碼示例
在業(yè)務(wù)開發(fā)中我們經(jīng)常會(huì)遇到處理不同權(quán)限的情況,例如根據(jù)用戶角色是否有編輯權(quán)限來展示和隱藏一個(gè)按鈕,或者一個(gè)函數(shù)根據(jù)傳入的配置項(xiàng)來執(zhí)行不同的邏輯,也就是所謂的權(quán)限控制,所以本文給大家介紹了JS使用位運(yùn)算實(shí)現(xiàn)權(quán)限組合,需要的朋友可以參考下2024-07-07

