WeChat Mini Program School Calendar Component

The school calendar component can be used as a plugin for campus mini-programs. If you find it useful, give it a star :-)
Github: https://github.com/WindrunnerMax/SCalendar
SHST Mini-program at Shanshui University: https://github.com/WindrunnerMax/SHST

Preview

Usage

Import into WeChat Developer Tools to test the demo

<!-- Parameters Description -->
<calendar term="2019-2020-2" termStart="2020-02-10" weekCount="29" vacationStart="24"></calendar>
<!-- 
    term: current semester 
    termStart: start date
    weekCount: total weeks in the semester
    vacationStart: start week of vacation
-->
<!-- Note: All date entries must conform to the format yyyy-MM-dd, for example, 2020-03-01 instead of 2020-3-1 -->

Implementation

Reference to Utility Classes

This is the time operation utility class written in my mini-program (SHST). It is referenced in the component.

/**
 * yyyy year MM month dd day hh1~12-hour format (1-12) HH24-hour format (0-23) mm minute ss second S millisecond K week
 */
const formatDate = (fmt = "yyyy-MM-dd", date = new Date()) => {
  var week = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
  var o = {
    "M+": date.getMonth() + 1, //month
    "d+": date.getDate(), //day
    "h+": date.getHours(), //hour
    "m+": date.getMinutes(), //minute
    "s+": date.getSeconds(), //second
    "q+": Math.floor((date.getMonth() + 3) / 3), //quarter
    "S": date.getMilliseconds(), //millisecond
    "K": week[date.getDay()]
  };
  if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
  for (var k in o) {
    if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
  }
  return fmt;
}

/**
 * Extend Date prototype
 */
const extDate = () => {
  Date.prototype.addDate = function (years = 0, months = 0, days = 0) {
    if (days !== 0) this.setDate(this.getDate() + days);
    if (months !== 0) this.setMonth(this.getMonth() + months);
    if (years !== 0) this.setFullYear(this.getFullYear() + years);
  }
}

/**
 * Calculate date difference
 */
const dateDiff = (startDateString, endDateString) => {
  var separator = "-"; //date separator
  var startDates = startDateString.split(separator);
  var endDates = endDateString.split(separator);
  var startDate = new Date(startDates[0], startDates[1] - 1, startDates[2]);
  var endDate = new Date(endDates[0], endDates[1] - 1, endDates[2]);
  var diff = parseInt((endDate - startDate) / 1000 / 60 / 60 / 24); //convert the difference in milliseconds to days
  return diff;
}


module.exports = {
  formatDate: formatDate,
  extDate: extDate,
  dateDiff: dateDiff
}

JS Logic

First of all, we need to print out the calendar for the current month based on the year and month. We can use util.formatDate("yyyy-MM-01", date) to get the Date object for the 1st of the current month, then calculate which day of the week this falls on. We can use Date.addDate() to move the date to the Monday of the week that contains the 1st of the month, which will be the first day to print on the calendar for the current month. Since the school calendar usually starts the week on a Monday, we need to adjust this slightly and cannot simply subtract the week count to point the Date object to the first day of the calendar.
Since a calendar month can have at most five weeks, we can directly print the time for five weeks using Date.addDate() to increment the date day by day, and then save the data.

As for the date styles, I used a relatively simple method. I used an if statement to determine the date and assign specific styles to it. I concatenated different class as a string and assigned it to unitObj. As for the display color, I controlled the priority of the CSS to make the unit styles inherit from the container. For styles that need specific display, I provided a detach field to save them.

To jump to a specific date, we can directly get the time to concatenate the month and then call the calendar month handling method.

const util = require("calendarUtil.js");
const date = new Date();
Component({
  properties: {
    term: {
      type: String,
      value: '2019-2020-2',
    },
    termStart: {
      type: String,
      value: '2020-02-10',
    },
    weekCount: {
      type: Number,
      value: 29
    },
    vacationStart: {
      type: Number,
      value: 24
    }
  },
  data: {
    calendarData: [],
    vacationDateDiff: 0,
    vacationStartDate: "",
    curMonth: util.formatDate("MM", date),
    curYear: util.formatDate("yyyy", date),
    today: util.formatDate(undefined, date)
  },
  created: function () {
    util.extDate();
  },
  ready: function () {
    this.calcVacation();
    this.redayForDate(date);
  },
  methods: {
    jumpDate: function(e){
      var d = new Date(e.currentTarget.dataset.d);
      this.data.curMonth = util.formatDate("MM", d);
      this.data.curYear = util.formatDate("yyyy", d);
      this.redayForDate(d);
      this.setData({
        curMonth: this.data.curMonth,
        curYear: this.data.curYear,
      })
    },
    switchMonth:function(e){
      var d = new Date(this.data.curYear+"-"+this.data.curMonth+"-01");
      var s = e.currentTarget.dataset.s;
      if (s === "l") d.addDate(0,-1);
      else d.addDate(0,1);
      this.data.curMonth = util.formatDate("MM", d);
      this.data.curYear = util.formatDate("yyyy", d);
      this.redayForDate(d);
      this.setData({
        curMonth: this.data.curMonth,
        curYear: this.data.curYear,
      })
    },
    redayForDate: function(date){
      var curMonthDay = util.formatDate("yyyy-MM-01", date);
      var monthStart = new Date(curMonthDay);
      var monthStartWeekDay = monthStart.getDay();
      monthStartWeekDay = monthStartWeekDay === 0 ? 7 : monthStartWeekDay;
      var calendarStart = monthStart;
      calendarStart.addDate(0, 0, -(monthStartWeekDay - 1));
      this.showCalendar(calendarStart);
    },
    showCalendar: function (start) {
      var showArr = [];
      for (let i = 0; i < 6; ++i) {
        let innerArr = [];
        let week = 0;
        for (let k = 0; k < 7; ++k) {
          let unitDate = util.formatDate("yyyy-MM-dd", start);
          if (k === 0) {
            week = parseInt((util.dateDiff(this.data.termStart, unitDate) / 7)) + 1;
            week = week > 0 ? week : 0;
            innerArr.push({ day: week, color: "week ", type: "week number", detach:"" })
          }
          let unitObj = { day: unitDate.split("-")[2], color: "notCurMonth ", type: "--", detach: ""};
          if (util.formatDate("MM", start) === this.data.curMonth) unitObj.color = "curMonth ";
          if (unitDate === this.data.today) unitObj.color = "today ";
          if (unitDate === this.data.termStart) unitObj.color = "termStart ";
          if (unitDate === this.data.vacationStartDate) unitObj.color = "vacationStart ";
          if (k === 5 || k === 6) {
            unitObj.type = "weekend";
            unitObj.color += "weekend ";
          } else if (week && week < this.data.weekCount) {
            var tmpColor = "classes ";
            unitObj.type = "teaching"; unitObj.detach = "cdetach";
            if (week >= this.data.vacationStart) { unitObj.type = "holiday"; tmpColor = "vacation "; unitObj.detach=""; }
            unitObj.color += tmpColor;
          }
          innerArr.push(unitObj);
          start.addDate(0, 0, 1);
        }
        showArr.push(innerArr);
      }
      this.setData({
        calendarData: showArr
      })
    },
    calcVacation: function () {
      var d = new Date(this.data.termStart);
      d.addDate(0, 0, (this.data.vacationStart - 1) * 7);
      var vacationStartDate = util.formatDate(undefined, d);
      this.setData({ 
        vacationStartDate: vacationStartDate,
        vacationDateDiff: util.dateDiff(this.data.today, vacationStartDate)
      })
    }
  }
})

WXML View

The view is just used to create a calendar using loops

<view class="card">
  <view class="y-CenterCon head">
    <view class="y-CenterCon">
      <view class="arrow-left" bindtap="switchMonth" data-s="l"></view>
      <view class="showDate">{{curYear}} Year {{curMonth}} Month</view>
      <view class="arrow-right" bindtap="switchMonth" data-s="r"></view>
    </view>
    <view class="y-CenterCon">
      <view class="opt y-CenterCon x-CenterCon" style="background-color: #1E9FFF;" bindtap="jumpDate" data-d="{{today}}">Today</view>
      <view class="opt y-CenterCon x-CenterCon" style="background-color: #FF6347;" bindtap="jumpDate" data-d="{{termStart}}">Start</view>
      <view class="opt y-CenterCon x-CenterCon" style="background-color: #3CB371;" bindtap="jumpDate" data-d="{{vacationStartDate}}">Vacation</view>
    </view>
  </view>

  <view>
    <view class="y-CenterCon line">
      <view wx:for='{{["Week","Mon","Tue","Wed","Thu","Fri","Sat","Sun"]}}' wx:key="{{index}}" class="unit">{{item}}</view>
    </view>
    <view wx:for="{{calendarData}}" wx:key="{{index}}" class="line">
      <view wx:for="{{item}}" wx:for-item="innerItem" wx:for-index="innerIndex" wx:key="{{innerIndex}}">
        <view class="unitCon {{innerItem.color}}">
          <view class="unit u">{{innerItem.day}}</view>
          <view class="x-CenterCon intro {{innerItem.detach}}">{{innerItem.type}}</view>
        </view>
      </view>
    </view>

  </view>
</view>

<view class="card y-CenterCon info">
  <view style="width: 40%;">
    <view class="a-dot" style="background: #3CB371;"></view>
    <view >Term: {{term}}</view>
  </view>
  <view style="width: 24%;">
    <view class="a-dot" style="background: #9F8BEC;"></view>
    <view>Weeks: {{weekCount}}</view>
  </view>
  <view style="width: 36%;">
    <view class="a-dot" style="background: #FF6347;"></view>
    <view >Start: {{termStart}}</view>
  </view>
</view>
<view class="card y-CenterCon info">
  <view style="width: 40%;">
    <view class="a-dot" style="background: #3CB371;"></view>
    <view >Vacation: {{vacationStartDate}}</view>
  </view>
  <view style="width: 24%;">
    <view class="a-dot" style="background: #9F8BEC;"></view>
    <view>Week: {{vacationStart}}</view>
  </view>
  <view style="width: 36%;">
    <view class="a-dot" style="background: #FF6347;"></view>
    <view >Days until vacation: {{vacationDateDiff}} days</view>
  </view>
</view>

Wxss style

page{
  margin: 0;
    font-family: Arial, Helvetica, 'STHeiti STXihei', 'Microsoft YaHei', Tohoma, sans-serif;
    padding: 10px;
    box-sizing: border-box;
    font-size: 13px;
    background-color: #F8F8F8;
}

.card{
  padding: 10px;
  background: #fff;
  margin-bottom: 10px;
  box-sizing: border-box;
}

.head{
  justify-content: space-between;
  margin: 5px 5px 5px 10px;
}

.showDate{
  margin: 10px 20px;
  font-weight: bold;
}

.opt{
  width: 20px;
  line-height: 20px;
  padding: 4px;
  margin: 0 10px;
  color: #fff;
  background-color: #9F8BEC;
  border-radius: 30px;
}

.line{
  display: flex;
  justify-content: space-around;
  align-content: center;
  margin: 10px 0;
}

.unitCon{
  color: #333;
}

.unitCon view{
  color: inherit;
}

.unit{
  line-height: 25px;
  width: 25px;
  margin: 2.5px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.notCurMonth{
  color: #ddd !important;
}

.today > .u,.termStart > .u,.vacationStart > .u{
  color: #fff !important;
  border-radius: 30px;
  background: #1E9FFF;
}

.termStart > .u{
  background: #FF6347;
}

.vacationStart > .u{
  background: #3CB371;
}

.week{
  color:#9F8BEC;
}

.intro{
    font-size: 11px;
}

.curMonth > .cdetach{
  color: #999;
}

weekend,.vacation{
  color: #3CB371;
}

x-CenterCon {
    display: flex;
    justify-content: center;
}

y-CenterCon {
    display: flex;
    align-items: center;
}

.a-full{
  flex: 1;
}
.arrow-left, .arrow-right {
  width: 25px;
  height: 25px;
  background-size: 25px 25px;
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3C?xml version='1.0' standalone='no'?%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg t='1583569368665' class='icon' viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' p-id='1602' xmlns:xlink='http://www.w3.org/1999/xlink' width='200' height='200'%3E%3Cdefs%3E%3Cstyle type='text/css'%3E%3C/style%3E%3C/defs%3E%3Cpath d='M641.28 278.613333l-45.226667-45.226666-278.634666 278.762666 278.613333 278.485334 45.248-45.269334-233.365333-233.237333z' p-id='1603' fill='%238a8a8a'%3E%3C/path%3E%3C/svg%3E");
}

.arrow-right {
  transform: rotate(180deg);
}

.a-hr {
  display: block;
  height: 1px;
  background: #EEEEEE;
  border: none;
  margin: 10px 5px;
}

.a-dot {
  width: 8px;
  height: 8px;
  border-radius: 8px;
  background-color: #1E9FFF;
  margin-right: 5px;
}

.a-dot + view {
  margin-right: 5px;
}

.info view {
  display: flex;
  align-items: center;
}