我又挖了个大坑…日历尚未成型,同志仍需努力💪 基本功能已实现!

之前我使用的是 jsCalendar 这个插件,但是它功能比较复杂,加载时间长;并且我还想在日历上添加其它的功能,1500行的代码看不下去,还不如自定义了!

基本日历结构

呜呜呜,我高看了自己,写 js 还是很吃力的,因为之前也没有基础。单纯的功能实现还好,但怎么让代码能运行起来呢?为了省事儿,我直接一股脑儿地把所有代码放到 window.onload = function () {} 里面,然后在 html 页面引入 js 的路径就可以让代码执行了!(随便看了看 github 里面用 js 实现的代码,貌似有一个固定的模版,好像还有什么构造器之类的,不过我现在的目标是能用就好)

代码不难,但是要搞清楚 Date() 对象各种变量的有效范围,比如说月份是 0~11 之间,星期是 0~6 之间,其中星期六=6,星期日=0。要事先想清楚渲染的效果,在写 html 结构的时候要加入对应的 class。

拆解一下日历的结构,可以总结出来重点关注两个日子:上个月的最后一天 以及 这个月的最后一天。如果上个月的最后一天是星期六,那么无需考虑上个月;同理,如果这个月的最后一天是星期六,无需考虑下个月。根据上个月是星期几,我们能推算出当前日历的第一天是几号,由此渲染完第一行。之后的几行比较简单,日子加加即可,直到遇到这个月的最后一天。此时,日子从 1 开始计算,直到该行满 7 个元素为止。

方法外部的变量

var today = new Date();
var thisYear = today.getFullYear();
var thisMonth = today.getMonth() + 1; // Note that `getMonth()` returns 0-11
var thisDate = today.getDate();
var calendar = document.getElementsByClassName('my-calendar')[0];
var tableHtml = '';

日历初始化

// the first to run, the year and month equal to this year and this month
// function: initialize the `tableHtml`
function initCalendar() {
  tableHtml += '<table><thead class="calendar-head"><tr class="calendar-title"><th class="calendar-title-left">\<</th> <th colspan="5" class="calendar-title-name"><span class="cur-year">' + thisYear + '</span>年<span class="cur-month">' + thisMonth + '</span>月</th><th class="calendar-title-right">\></th></tr><tr class="calendar-week-days"><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr></thead>';
  tableHtml += generateCalendarBody(thisYear, thisMonth);
  tableHtml += '</table>';
  calendar.innerHTML = tableHtml;
}

日历更新

function updateCalendar(year, month) {
  // update the head
  document.getElementsByClassName('cur-year')[0].innerHTML = year;
  document.getElementsByClassName('cur-month')[0].innerHTML = month;

  // update the body
  document.getElementsByClassName('calendar-content')[0].innerHTML = generateCalendarBody(year, month);
}

日历body更新

function generateCalendarBody(year, month) {

  // Note that `Date()` uses month 0-11
  var lastDayOfPreviousMonth = new Date(year, month - 1, 0);
  var lastDayOfCurrentMonth = new Date(year, month, 0);
  var lastDayOfPreviousMonthDay = lastDayOfPreviousMonth.getDay();
  var lastDayOfPreviousMonthDate = lastDayOfPreviousMonth.getDate();
  var lastDayOfCurrentMonthDate = lastDayOfCurrentMonth.getDate();
  var lastDayOfCurrentMonthDay = lastDayOfCurrentMonth.getDay();

  // day=6 Saturday means 
  // the first day of this month is Sunday -> the first date in calendar is 1
  var firstDayOfCalendarDate = (lastDayOfPreviousMonthDay != 6) ? lastDayOfPreviousMonthDate - lastDayOfPreviousMonthDay : 1;

  var tbodyHtml = '<tbody class="calendar-content">';
  var displayDate = 0; // date to display in the calendar
  var isThisMonth = (year == thisYear && month == thisMonth); // check if this month

  // render the first line in the calendar body
  for (var i = 0; i < 7; i++) {
    var tmpDate = firstDayOfCalendarDate + i;
    if (firstDayOfCalendarDate == 1) {
      displayDate = tmpDate;
      // today class will have a different style
      if (isThisMonth && displayDate == thisDate) {
        tbodyHtml += '<td class="cur-month today" onclick="getCalendarDate(this);">' + displayDate + '</td>';
      } else {
        tbodyHtml += '<td class="cur-month" onclick="getCalendarDate(this);">' + displayDate + '</td>';
      }
    } else if (tmpDate > lastDayOfPreviousMonthDate) {
      displayDate = tmpDate - lastDayOfPreviousMonthDate;
      // today class will have a different style
      if (isThisMonth && displayDate == thisDate) {
        tbodyHtml += '<td class="cur-month today" onclick="getCalendarDate(this);">' + displayDate + '</td>';
      } else {
        tbodyHtml += '<td class="cur-month" onclick="getCalendarDate(this);">' + displayDate + '</td>';
      }
    } else {
      displayDate = tmpDate;
      tbodyHtml += '<td class="last-month">' + displayDate + '</td>';
    }
  }
  tbodyHtml += '</tr><tr>'; // the end of a line & the begin of another line
  displayDate++;

  var daysInALine = 0;
  while (displayDate <= lastDayOfCurrentMonthDate) {
    // today class will have a different style
    if (isThisMonth && displayDate == thisDate) {
      tbodyHtml += '<td class="cur-month today" onclick="getCalendarDate(this);">' + displayDate + '</td>';
    } else {
      tbodyHtml += '<td class="cur-month" onclick="getCalendarDate(this);">' + displayDate + '</td>';
    }
    daysInALine++;
    displayDate++;
    if (daysInALine == 7) {
      tbodyHtml += '</tr>'; // the end of a line
      if (displayDate != lastDayOfCurrentMonthDate)
        tbodyHtml += '<tr>'; // the begin of another line, only for current display date doesn't equal to the last day of current month
      daysInALine = 0
    }
  }

  displayDate = 1; // the first day of next month
  while (daysInALine < 7 && lastDayOfCurrentMonthDay != 6) {
    tbodyHtml += '<td class="next-month">' + displayDate + '</td>';
    displayDate++;
    daysInALine++;
  }
  tbodyHtml += '</tr></tbody>';
  return tbodyHtml
}

前后月跳转

这里我遇到了一个困难:点击事件只能响应一次。查阅资料发现是修改了 html 内容导致,因为一开始重新加载日历时,我把整个 html 都更新了,导致动态绑定的事件失效了。于是我退而求其次,保留日历的 head 部分,只更新 body 部分。

自定义变量

var prev = document.getElementsByClassName('calendar-title-left')[0];
var next = document.getElementsByClassName('calendar-title-right')[0];
var year = document.getElementsByClassName('cur-year')[0].innerHTML; // display year
var month = document.getElementsByClassName('cur-month')[0].innerHTML; // display month

响应点击事件

prev.onclick = function () {
  console.log("prev");
  if (month == 1) {
    year--;
    month = 12;
  } else {
    month--;
  }
  updateCalendar(year, month);
};

next.onclick = function () {
  console.log("next");
  if (month == 12) {
    year++;
    month = 1;
  } else {
    month++;
  }
  updateCalendar(year, month);
};

响应日期点击事件

又碰到了一个问题:onclick 的函数没被定义,我明明写了呀?一脸疑惑。

根据这位作者的解释,我把原函数的这种形式

function dosave(){
  alert("会报错!!");
}

改成了这种

dosave = function (){
  alert("成功啦!");
}

然后就好了!作者说

dosave = function(){} 的写法会把 dosave 函数作为全局作用域函数

而 onclick 函数要求是全剧函数,恰好符合要求。

题外话:作为一个木有前端基础的同学,我的主要目的是为了实现想要的功能,解决自己的问题,所以都并没有查阅相关专业资料,若有问题,请多多和我讨论哈~

下面是相关代码:

getCalendarDate = function (object) {
  var year = document.getElementsByClassName('cur-year')[0].innerHTML; //string
  var month = Number(document.getElementsByClassName('cur-month')[0].innerHTML);//int
  var date = Number(object.innerHTML);
  if (month < 10) {
    month = '0' + month;//string
  }
  if (date < 10) {
    date = '0' + date;//string
  }
  console.log(year + month + date);
}

目前能做到提取当前点击的日期,但还没有实现跳转当日所有文章的功能。

绑定博文日期

Ummmm,现在还没想好一个好的解决方案。有以下问题需要思考 🤔

1. Hexo 貌似是事先生成一个 json 文件,绑定到日期上??还需要仔细研究研究 2. Hugo 内置根据日期排列的模版,但只能用于归档的感觉?一次性的全排列,不知道可不可以分开 3. URL 绑定日期,输入相关 url 即可访问当日所写全部博文 4. 日历区分有博文的日子和没有博文的日子: style + 如何判断当日博客数为0 5. 当日所写全部博文页面渲染

一直没想到怎么做,最近也有点忙,这个边角料的任务就放在一旁。今天在网上看到这篇博文,顿时就有了启发,下面讲一讲我怎么做的吧!

博客 config 配置

[permalinks]
    post = "/:title/"
    archd = "/:year/:month/:day/"
    archm = "/:year/:month/" 

post 这个本来是设置成 /:year/:month/:day/:title/ 的,后来才发现这会影响文章间 markdown 跳转,于是我改回了只包含文章标题的模式。

archdarchm 分别用于按日期和月份归档。

创建对应文件夹和文件

要想能通过月份和日期访问,单单设定一个 permalinks 是不够的,我们需要把所有写文章的日子都记下来,因为存在文件,所以才能访问。文件夹结构大概如此:

├── archd
│   ├── 2019-10-28.md
│   ├── 2019-10-29.md
│   ├── 2019-10-30.md
├── archm
│   ├── 2019-10.md
│   ├── 2019-11.md
├── post
│   ├── 博文1.md
│   ├── 博文2.md

每个文件的内容格式如下:

{"date": "2019-11-01 00:00:00"}

其中,日期根据文件名来命名。

日月归档分类渲染

这块比较难,因为对 Hugo 不太熟,很多地方一试再试。贴一下我最后的代码:

layouts/archd/single.html

{{ define "content"}}

{{ $archYear := .Date.Format "2006" }}
{{ $archMonth := .Date.Format "January" }}
{{ $archDay := .Date.Format "21" }}

<h3 class="archive-title">
  {{ slicestr .Date 0 10 }} 也是一个勤奋的小蜜蜂!
</h3>

{{ range where .Site.Pages "Section" "post" }}
{{ if and (eq (.Date.Format "2006") $archYear) ( and (eq (.Date.Format "January") $archMonth) (eq (.Date.Format "21") $archDay)) }}
<article class="post">
  <header>
    <h1 class="post-title">
      <a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title }}</a>
    </h1>
  </header>
  <date class="post-meta meta-date">
    {{ .Date.Year }}年{{ printf "%d" .Date.Month }}月{{ .Date.Day }}日
  </date>
  {{ with .Params.Categories }}
  <div class="post-meta">
    <span>|</span>
    {{ range . }}
    <span class="meta-category"><a href='{{ "/categories/" | absLangURL }}{{ . | urlize }}'>{{ . }}</a></span>
    {{ end }}
  </div>
  {{ end }}
  <div class="post-meta">
    <span>|</span>
    <span>{{ .WordCount }} 字</span>
  </div>
  <div class="post-meta">
    <span>|</span>
    <span>需要 {{ .ReadingTime }} 分钟</span>
  </div>
  <div class="post-content">
    {{ .Summary }}
  </div>
  <p class="readmore"><a href="{{ .Permalink }}">阅读全文</a></p>
</article>
{{ end }}
{{ end }}
{{ end }}

知道了日归档怎么写,月归档就很简单啦!这里就不贴代码了。

generate-archives.py

完成以上步骤,我们就可以通过 https://hanmei.netlify.app/2020/05/21/ 这样的网址访问 2020年5月21日 撰写的博文啦~但是,如果 2020-05-21.md 如果没有在文件夹中创建,那这个网址就无法访问了。为了避免每次很麻烦的添加代码,我写了一个 Python 脚本,可以自动生成归档需要的目录以及文件。

日历绑定归档地址

这一步我需要知道那些网址是可访问,也就是说当天有撰写博文。如果只往这方面考虑就走近了死胡同,想通过判断这个网址是否404来渲染日历,但这个我不会实现。后来我换了一种思路,和前面的步骤一样,先把有文章的日子记下来,日历的 js 文件通过这个日期列表来渲染,这个问题就完美的解决啦!

具体实现上,通过 generate-archives.py 向某个 js 中间文件中写入能访问的日期列表。只要把这个 js 文件和 calendar 的 js 文件都引用在 sidebar 的 html 中,就可以在 calendar.js 中直接调用 js 文件里面的日期列表变量(这个地方我研究了好久,不知道怎么在 js 中引用另一个 js 文件)。

PS: 发现之前有个小问题,就是只能在主页显示日历,这是因为路径没处理好,在引用 js 文件时得用上 src='{{ "js/myCalendar.js" | relURL }}'

calendar.js 得到这个日期列表后,事情就变得简单了。修改修改条件语句判断和 click 事件,最后修修 css 样式一切就大功告成了。

顺便学了个如何利用 js 进行页面跳转:

window.location.href = url;

来看看最终效果

点击 14 号,可以看到能跳转到 14 号撰写的所有文章的链接。

氮素有个小问题,2020年5月21日的界面是这样的:

我推断,问题应该出在文件夹里有一个对自己的链接 .,但我也不太确定是不是这个所导致的。至于怎么解决,我暂且搁置一边,毕竟对整体功能没多大影响。

还有一点不是很满意的是,日历能点击的部分我想有个小手手,而不是小箭头,在内部使用 pointer 没有效果,之后有闲情在处理吧。