看看DeepSeek是如何实现前端日历组件的?

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

AI 模型大比拼:用一个日历组件,看看谁写出来的效果最好

文章将代码分为三个部分来讲解: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">&lt;&lt;</button>
      <button id="prev-month">&lt;</button>
      <button id="current-month">今天</button>
      <button id="next-month">&gt;</button>
      <button id="next-year">&gt;&gt;</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操作的开销比较大,如果使用VueReact这种框架,都会对这种操作进行优化。但如果是纯JS实现,就需要考虑性能和代码复杂度之间的平衡,这里优化没有太大必要。

下一步,根据当前的月份和年份,获取当月第一天和最后一天,以及当月天数。这里代码很巧妙,同时也学到了一个新的知识,获取月份最后一天的方法:

获取下个月0号的日期,就是上个月的最后一天。

javascript 复制代码
// 获取当月最后一天
const lastDay = new Date(year, month + 1, 0);
// 获取当月天数
const daysInMonth = lastDay.getDate();

拿到当月最后一天后,也意味着当前月份天数也能轻松拿到了。很好的避免了人为去考虑大小月,以及闰年的问题。

然后再获取当月第一天是星期几,这样就可以计算出上个月最后几天需要渲染的日期。使用一个for循环补齐上个月的最后几天,动态创建日期节点,并添加到日历显示区域的DOM中。

接下来渲染当月的日期,当月的日期需要考虑是否是今天、是否是选中日期,并且还需要在日期节点上添加点击事件,触发选中日期的操作。在选中日期后,直接重新执行了整个renderDays函数,这里只涉及到节点状态更新完全可以做一个优化,没必要全量重新计算和更新所有节点。

最后,与之前一样,计算下个月的前几天进行补齐。

从代码结构上来看,代码逻辑非常清晰,也有必要的注释。但从业务逻辑层面上考虑,代码也存在可优化空间。总体上,还是一份相当不错的日历组件代码,二次开发同样没有什么压力。

总结

看了DeepSeek写的代码,发现不比人类写代码能力差,至少从代码结构、命名、可读性上,比人写的要好很多。至于业务层面,由于代码本身也没涉及太复杂的业务逻辑处理,所以不好做评判。

如果觉得还不错,请留下一个赞👍再走~

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax