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

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

相关推荐
小赵学鸿蒙25 分钟前
用Uniapp开发鸿蒙项目 五
前端
小lan猫27 分钟前
【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(五)
前端·vue.js
星使bling28 分钟前
基于Baidu JSAPI Three的卫星轨道三维可视化Demo
前端·javascript
Oder_C29 分钟前
自定义指令-优化v-if和v-show上的使用
前端·javascript·vue.js
小赵学鸿蒙30 分钟前
用Uniapp开发鸿蒙项目 八(上)
前端
拾光拾趣录31 分钟前
TypeScript 数组与对象类型定义
前端
小赵学鸿蒙32 分钟前
用Uniapp开发鸿蒙项目 四
前端
程序猿阿伟1 小时前
《深入解析:如何通过CSS集成WebGPU实现高级图形效果》
前端·css
Monster411 小时前
鸿蒙性能引擎:ArkCompiler实战精要
前端
ze_juejin1 小时前
Typescript中的继承示例
前端·typescript