上一期我们比较了国内 6 大热门平台的 AI 编码能力,今天我们针对 DeepSeek 的日历组件代码进行详细解析,看看它是如何实现的。

文章将代码分为三个部分来讲解:HTML、CSS、JavaScript 三个部分,看看有没有值得我们学习的地方。
HTML 结构
以下是日历组件的 HTML 源码部分:
html
<div class="calendar-container">
<div class="calendar-header">
<div class="calendar-title" id="calendar-title">2023年10月</div>
<div class="calendar-nav">
<button id="prev-year"><<</button>
<button id="prev-month"><</button>
<button id="current-month">今天</button>
<button id="next-month">></button>
<button id="next-year">>></button>
</div>
</div>
<div class="weekdays">
<div>日</div>
<div>一</div>
<div>二</div>
<div>三</div>
<div>四</div>
<div>五</div>
<div>六</div>
</div>
<div class="days" id="calendar-days"></div>
</div>
在组件容器calendar-container
中,主要有头部区域calendar-header
,星期标题区域weekdays
,日期显示区域days
。这个结构非常清晰,class 命名也很直观,同时,对于需要操作的部分,也都有对应的 id 进行标识。
日期显示区域days
只提供了一个容器,具体日期可以推断将由 JavaScript 代码来动态生成。
有点遗憾的是,年份和月份的切换按钮图标没有使用真正的图标,而是直接使用了符号代替(大于和小于号:<
和>
)。
CSS 样式
组件容器calendar-container
定义了宽度、背景色、圆角、阴影等外观样式,符合人类编程直觉。
css
.calendar-container {
width: 350px;
background: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
头部calendar-header
采用了flex
布局,定义了背景和字体颜色;操作区域calendar-nav
也使用了flex
布局,并且设置了间距gap: 10px;
;对操作按钮还增加了悬浮背景过度样式。
css
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #4285f4;
color: white;
}
.calendar-title {
font-size: 18px;
font-weight: bold;
}
.calendar-nav {
display: flex;
gap: 10px;
}
.calendar-nav button {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.calendar-nav button:hover {
background: rgba(255, 255, 255, 0.3);
}
星期标题与日期展示区域都使用了grid
布局,使用grid-template-columns: repeat(7, 1fr);
将子节点进行 7 列等分布局。
css
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 10px 0;
background: #f1f1f1;
text-align: center;
font-weight: bold;
font-size: 14px;
}
.days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
padding: 10px;
}
对于动态日期部分的样式写的也都非常全面,对悬浮、选中、今日、非本月日期都定义了不同的样式。但也有一个问题,就是日期设置了一个50%
的圆角,但只固定了高度40px
,而宽度依赖容器动态设置,这就导致了最终的效果可能不是正圆,而是一个椭圆,必须确保组件容器的宽度刚好到一个合适的宽度才能让日期显示为正圆。
css
.day {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.day:hover {
background: #e0e0e0;
}
.day.today {
font-weight: bold;
color: #4285f4;
}
.day.selected {
background: #4285f4;
color: white;
}
.day.other-month {
color: #aaa;
opacity: 0.6;
}
总体而言,样式部分基本都是采用当前比较流行的布局,代码简洁,没有冗余的样式。从类名、样式上来看,我相信已经超越很多前端开发者了。在此基础上进行二次开发也非常容易,没有任何心智负担和阅读成本。
JavaScript 代码
DeepSeek 将组件的 JS 部分所有操作,都封装在了一个Calendar
类中,并通过new
关键字进行实例化。
javascript
// 日历核心类
class Calendar {
constructor() {
this.currentDate = new Date();
this.selectedDate = new Date();
// 获取DOM元素
this.calendarTitle = document.getElementById('calendar-title');
this.calendarDays = document.getElementById('calendar-days');
// 初始化事件监听
this.initEventListeners();
// 渲染日历
this.render();
}
initEventListeners() {
// 月份导航
document.getElementById('prev-month').addEventListener('click', () => {
this.changeMonth(-1);
});
document.getElementById('next-month').addEventListener('click', () => {
this.changeMonth(1);
});
// 年份导航
document.getElementById('prev-year').addEventListener('click', () => {
this.changeYear(-1);
});
document.getElementById('next-year').addEventListener('click', () => {
this.changeYear(1);
});
// 返回今天
document.getElementById('current-month').addEventListener('click', () => {
this.currentDate = new Date();
this.selectedDate = new Date();
this.render();
});
}
changeMonth(offset) {
const newDate = new Date(this.currentDate);
newDate.setMonth(newDate.getMonth() + offset);
this.currentDate = newDate;
this.render();
}
changeYear(offset) {
const newDate = new Date(this.currentDate);
newDate.setFullYear(newDate.getFullYear() + offset);
this.currentDate = newDate;
this.render();
}
render() {
this.renderTitle();
this.renderDays();
}
renderTitle() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth() + 1;
this.calendarTitle.textContent = `${year}年${month}月`;
}
renderDays() {
this.calendarDays.innerHTML = '';
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
// 获取当月第一天和最后一天
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// 获取当月天数
const daysInMonth = lastDay.getDate();
// 获取第一天是星期几 (0-6, 0是星期日)
const firstDayOfWeek = firstDay.getDay();
// 获取上个月最后几天
const prevMonthLastDay = new Date(year, month, 0).getDate();
// 获取今天日期
const today = new Date();
const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
// 渲染上个月的最后几天
for (let i = 0; i < firstDayOfWeek; i++) {
const day = prevMonthLastDay - firstDayOfWeek + i + 1;
const dayElement = this.createDayElement(day, true);
this.calendarDays.appendChild(dayElement);
}
// 渲染当月天数
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const isToday = isCurrentMonth && day === today.getDate();
const isSelected =
this.selectedDate &&
date.getFullYear() === this.selectedDate.getFullYear() &&
date.getMonth() === this.selectedDate.getMonth() &&
date.getDate() === this.selectedDate.getDate();
const dayElement = this.createDayElement(day, false, isToday, isSelected);
dayElement.addEventListener('click', () => {
this.selectedDate = date;
this.renderDays();
});
this.calendarDays.appendChild(dayElement);
}
// 计算下个月的前几天
const totalCells = Math.ceil((firstDayOfWeek + daysInMonth) / 7) * 7;
const nextMonthDays = totalCells - (firstDayOfWeek + daysInMonth);
// 渲染下个月的前几天
for (let i = 1; i <= nextMonthDays; i++) {
const dayElement = this.createDayElement(i, true);
this.calendarDays.appendChild(dayElement);
}
}
createDayElement(day, isOtherMonth, isToday = false, isSelected = false) {
const dayElement = document.createElement('div');
dayElement.className = 'day';
dayElement.textContent = day;
if (isOtherMonth) {
dayElement.classList.add('other-month');
}
if (isToday) {
dayElement.classList.add('today');
}
if (isSelected) {
dayElement.classList.add('selected');
}
return dayElement;
}
}
// 初始化日历
new Calendar();
在构造函数中,初始化了当前日期和选中日期,并获取了日历标题和日期显示区域的 DOM 元素。接着,设置了操作事件监听器,处理月份和年份的切换,默认渲染今天日期的月份。
每次操作完成后,都会重新执行render
函数,所有的日期计算都是基于当前日期currentDate
实现的。
核心的计算代码主要在renderDays
函数中,我们来逐步解析:
每次渲染之前,使用this.calendarDays.innerHTML = ''
移除所有日期节点,然后计算完成后,重新生成所有日期节点。这种操作简单粗暴,但对DOM操作的开销比较大,如果使用Vue
或React
这种框架,都会对这种操作进行优化。但如果是纯JS实现,就需要考虑性能和代码复杂度之间的平衡,这里优化没有太大必要。
下一步,根据当前的月份和年份,获取当月第一天和最后一天,以及当月天数。这里代码很巧妙,同时也学到了一个新的知识,获取月份最后一天的方法:
获取下个月0号的日期,就是上个月的最后一天。
javascript
// 获取当月最后一天
const lastDay = new Date(year, month + 1, 0);
// 获取当月天数
const daysInMonth = lastDay.getDate();
拿到当月最后一天后,也意味着当前月份天数也能轻松拿到了。很好的避免了人为去考虑大小月,以及闰年的问题。
然后再获取当月第一天是星期几,这样就可以计算出上个月最后几天需要渲染的日期。使用一个for
循环补齐上个月的最后几天,动态创建日期节点,并添加到日历显示区域的DOM中。
接下来渲染当月的日期,当月的日期需要考虑是否是今天、是否是选中日期,并且还需要在日期节点上添加点击事件,触发选中日期的操作。在选中日期后,直接重新执行了整个renderDays
函数,这里只涉及到节点状态更新完全可以做一个优化,没必要全量重新计算和更新所有节点。
最后,与之前一样,计算下个月的前几天进行补齐。
从代码结构上来看,代码逻辑非常清晰,也有必要的注释。但从业务逻辑层面上考虑,代码也存在可优化空间。总体上,还是一份相当不错的日历组件代码,二次开发同样没有什么压力。
总结
看了DeepSeek写的代码,发现不比人类写代码能力差,至少从代码结构、命名、可读性上,比人写的要好很多。至于业务层面,由于代码本身也没涉及太复杂的业务逻辑处理,所以不好做评判。
如果觉得还不错,请留下一个赞👍再走~