微信小程序 | 吐血整理的日历及日程时间管理

简介: 微信小程序 | 吐血整理的日历及日程时间管理

需求背景

在小程序项目开发过程中,我们要实现时间管理功能,除了从时间维度方面入手,很多时候日历与日程的管理也是众多时间管理场景中必备的一些功能。

本文总结了时下常用的时间管理功能,从简单的日历显示,到日程规划,再到与日历息息相关的打卡签到功能也是全方面覆盖!收藏起来吧,总有一天你会感谢我的😁😂🤣😃😄😅😆


一、效果预览

1.1 日历带日程管理

b28c31e051c840d0a67630cd9553dc08.gif


1.2 农历阳历结合

f1d76775e5a145c9bbaf64d666856753.gif


1.3 日历带日程样式优化

c445d30cb1024498a16a3866964ba467.gif


1.4 日历之打卡签到与补签

d83ede2f347c43c4b8fceebdb9fc3ca2.gif


二、源码获取

2.1 日历带日程管理

2.1.1 应用建议

该日历是一个常规的日历显示,优点在于配套了日程管理以及背景样式有当前月份显示。应用时可以根据自己的需求将日程管理进行优化!


2.1.2 完整源码

<template>
  <view>
    <view class="date-box">
      <view class="box-list" :style="{'margin-bottom' : list.length > 0 ? '20rpx' : '0'}">
        <view class="date-top">
          <view class="icon left-icon" @click="LastYear">
            <view class="iconfont icon-jiantou_xiangzuoliangci"></view>
          </view>
          <view class="conter-text">
            <view class="icon left-icon" @click="LastMonth">
              <view class="iconfont icon-xiangzuo1"></view>
            </view>
            <text class="month">{{year}}年{{month}}月</text>
            <view class="icon rigth-icon" @click="NextMonth">
              <view class="iconfont icon-xiangyou1"></view>
            </view>
          </view>
          <view class="icon rigth-icon" @click="NextYear">
            <view class="iconfont icon-jiantou_xiangyouliangci"></view>
          </view>
        </view>
        <view class="date-week">
          <view class="week-item" v-for="item in weekList" :key="item"><text>周{{item}}</text></view>
        </view>
        <view class="day-content" :style="{height: isOpen ? '100rpx' : 'auto'}" v-if="dayList.length > 0">
          <view class="day-item day-month" v-if="!isOpen"><text>{{month < 10 ? `0${month}` : month}}</text></view>
          <view 
            class="day-item" 
            v-for="(item, index) in dayList"
            :key="index"
            :data-index="index"
            @click="toActive(item, index)">
            <text class="day-text" v-if="item.day" :class="{ 'actives' : item.day === day }" >{{item.day ? item.day : ''}}</text>
            <text class="value-text" v-if="item.data.status">{{item.data.value}}</text>
            <text class="value-text text-red" v-else>{{item.data.value}}</text>
            <text class="day-dot" v-if="item.data.dot && item.data.active"></text>
            <text class="day-dot dot-gray" v-if="item.data.dot && !item.data.active"></text>
          </view>
        </view>
        <view style="width: 100%;"  v-if="isShrink">
          <view class="toggle" v-if="isOpen" @click="toShrinkClose">
            <view class="iconfont icon-shousuo"></view>
          </view>
          <view class="toggle" v-else @click="toShrink">
            <view class="iconfont icon-zhankai"></view>
          </view>
        </view>
      </view>
      <slot name="task">
      <view class="task-box" v-if="list.length > 0">
        <view class="task-item" v-for="(item, index) in list" :key="index" @click="toTask(item, index)">
          <view class="avatar-box">
            <view class="avatar">
               <image :src="item.avatar"></image>
            </view>
            <view class="title-box">
              <view class="title"><text>{{item.title}}</text></view>
              <view class="date"><text class="branch">时间:{{item.time}}</text><text>{{item.details}}</text></view>
            </view>
          </view>
          <view class="time"><text>{{item.date}}</text></view>
        </view>
      </view>
      </slot>
    </view>
    <view class="modal" v-if="show">
      <view class="mask" @click="close" v-if="closeOnClickOverlay"></view>
      <view class="z-content">
        <view class="modal-content">
          <view class="z-modal" :style="{width: width}">
            <view class="modal-title"><slot name="title"><text>{{title}}</text></slot></view>
            <view class="z-modal-content"><slot name="content"><text>{{content}}</text></slot></view>
            <view class="line"></view>
            <view class="modal-foot">
              <slot name="footer">
                <view class="cancel" @click="cancel" v-if="showCancelButton">
                  <text :style="{color: cancelColor}">{{cancelText}}</text>
                </view>
                <view class="foot-line" v-if="showCancelButton && showConfirmButton"></view>
                <view class="confirm" @click="confirm" v-if="showConfirmButton">
                  <text :style="{color: confirmColor}">{{confirmText}}</text>
                </view>
              </slot>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    name: 'zyfDate',
    props:{
      list:{
        type: Array,
        default: () => {
          return []
        }
      },
      weekList:{
        type:Array,
        default:() => ['日', '一', '二', '三', '四', '五', '六']
      },
      date:{
        type:Object,
        default:() => {
          return {
            year: new Date().getFullYear(),
            month: parseInt(new Date().getMonth() + 1),
            day: parseInt(new Date().getDate())
          }
        }
      },
      extraData: {
        type: Array,
        default: ()=> {
          return [{date: '2022-7-20', value: '签到', status: true, dot: true, active: false},{date: '2022-7-19', value: '未签到', status: false, dot: true, active: true}] // {date: '2020-6-3', value: '签到', dot: true, active: true}
        }
      },
      show:{
        type: Boolean,
        default: false
      },
      title:{
        type: String,
        default: ''
      },
      content:{
        type: String,
        default: '--'
      },
      confirmText:{
        type: String,
        default: '确认'
      },
      cancelText:{
        type: String,
        default: '取消'
      },
      showConfirmButton:{
        type: Boolean,
        default: true
      },
      showCancelButton:{
        type: Boolean,
        default: false
      },
      confirmColor:{
        type: String,
        default: '#2979ff'
      },
      cancelColor:{
        type: String,
        default: '#606266'
      },
      closeOnClickOverlay:{
        type: Boolean,
        default: true
      },
      width:{
        type: [Number,String],
        default: '650rpx'
      },
      isShrink:{
        type: Boolean,
        default: false
      },
      isUnfold:{
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        dayList:[],
        year: 2022,
        month: 10,
        day: 10,
        isOpen: false
      }
    },
    onLoad() {
    },
    created() {
      this.isOpen = this.isUnfold
      const { year, month, day  } = this.date
      this.year = year
      this.month = month
      this.day = day
      this.initTime()
      this.initApi(this.year, this.month)
    },
    onNavigationBarButtonTap(e){
      console.log(e)
      uni.showToast({
        title: '分享',
        duration: 2000
      });
    },
    methods: {
      initTime(){
        const { year, month, day } = this.getTiemNowDate()
        this.year = year
        this.month = month
        this.day = day
        //console.log('今日时间为:' + this.year + '-' + this.month + '-' +this.day )
      },
      toShrink(){
        let falg = null
        const dateArr = this.getTime(this.year, this.month);
        const line = dateArr.map((item,index) => {
          if((index % 7) && this.day == item.day){
            falg = Math.floor(index/7)
            return Math.floor(index/7)
          }
        })
              this.dayList = dateArr.slice(falg * 7, (falg + 1) * 7)
        this.isOpen = true
      },
      toShrinkClose(){
        this.dayList = this.getTime(this.year, this.month)
        this.isOpen = false
      },
      getTiemNowDate(){
        var date  = new Date()
        var year  = date.getFullYear()
        var month = parseInt(date.getMonth() + 1)
        var day   = date.getDate()
        if(month < 10){
          month = '0' + month
        }
        if(day < 10){
          day = '0' + day
        }
        const resultDate = {
          year:year,
          month: parseInt(month),
          day:parseInt(day)
        }
        return resultDate
      },
      initApi(year, month) {
        if(this.isShrink && this.isOpen){
          this.toShrink()
        } else {
          this.dayList = this.getTime(year, month)
        }
      },
      getTime(year, month){
        return this.creatDayList(year, month)
      },
      creatDayList(year, month){
        const count = this.getDayNum(year, month)
        const week = new Date(year + '/' + month + '/1').getDay()
        let list = []
        for(let i = 1; i <= count; i++ ){
          let data = {};
          for(let item of this.extraData){
            let dateString = item.date;
            let dateArr = dateString.indexOf('-') !== -1 ? dateString.split('-') : dateString.indexOf('/') !== -1 ? dateString.split('/') :  [];
            if(dateArr.length === 3 && Number(dateArr[0]) === Number(this.year) && Number(dateArr[1]) === Number(this.month) && Number(dateArr[2]) === Number(i)){
              data = item
            }
          }
          let obj = { day:i, data }
          list.push(obj)
        }
        for(let i = 0; i < week; i++){
          // list.unshift(this.getDayNum(year, month - 1) -i)
          list.unshift({ day:null, data:{}})
        }
        return list
      },
      getDayNum(year, month){
        let dayNum = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
        if((year % 4 !== 0) && (year % 100 === 0) || (year % 400 !== 0)){
          dayNum[1] = 28
        }
        return dayNum[month - 1]
      },
      toActive(item, index){
        this.day = item.day
        this.$emit('click-active', {year:this.year, month:this.month, day:item.day, date:this.year + '-' + this.month + '-' +this.day, index: index})
      },
      toTask(item, index){
        this.$emit('click-task', {row: item, index: index})
      },
      LastMonth(){
        if(this.month > 1){
          this.month = this.month - 1
          this.initApi(this.year, this.month)
        } else {
          this.LastYear(false)
          this.month = 12
          this.initApi(this.year, this.month)
        }
      },
      NextMonth(){
        if(this.month < 12){
          this.month = this.month + 1
        } else {
          this.NextYear(false)
          this.month = 1
        }
        this.initApi(this.year, this.month)
      },
      LastYear(flag = true){
        if(this.year > 2000){
          this.year = this.year - 1
          if(flag){
            this.initApi(this.year, this.month)
          }
        }
      },
      NextYear(flag = true){
        this.year = this.year + 1
        this.initApi(this.year, this.month)
      },
      confirm(){
        this.$emit('confirm')
      },
      cancel(){
        this.$emit('cancel')
      },
      close(){
        this.$emit('close')
      }
    }
  }
</script>
<style lang="scss" scoped>
  @import "iconfont.css";
  .date-box{
    display: flex;
    flex-direction: column;
    flex: 1;
    padding: 20rpx;
    .box-list{
      background-color: white;
      border-radius: 20rpx;
      .date-top{
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 40rpx 20rpx;
        .icon{
          width: 50rpx;
          height: 50rpx;
          line-height: 50rpx;
          image {
            width: 50rpx;
            height: 50rpx;
          }
        }
        .conter-text{
          font-size: 32rpx;
          font-weight: bold;
          display: flex;
          flex-direction: row;
          align-items: center;
          justify-content: space-between;
          .month{
            padding: 0 40rpx;
          }
        }
      }
      .date-week{
        display: flex;
        align-items: center;
        justify-content: space-between;
        flex: 1;
        padding: 20rpx;
        border-bottom: 1rpx solid #f3f4f6;
        .week-item{
          display: flex;
          justify-content: center;
          align-items: center;
          width: calc(100%/7);
          height: 52rpx;
          text-align: center;
          font-size: 30rpx;
          color: #909193;
        }
      }
      .day-content{
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        padding: 10rpx 20rpx;
        position: relative;
        .day-item{
          display: flex;
          flex-direction: column;
          // justify-content: center;
          align-items: center;
          width: calc(100%/7);
          height: 95rpx;
          text-align: center;
          font-size: 32rpx;
          z-index: 2;
          position: relative;
          .day-text{
            width: 65rpx;
            height: 65rpx;
            line-height: 65rpx;
            // margin-bottom: 5rpx;
            &.actives{
              color: #fff;
              box-sizing: border-box;
              background-color: #2b85e4;
              border-radius: 6rpx;
              border-radius: 50%;
            }
          }
          .value-text{
            font-size: 24rpx;
            color: #18b566;
            &.text-red {
              color: #dd6161;
            }
          }
          .day-dot{
            margin-top: 5rpx;
            background: #dd6161;
            border-radius: 50%;
            padding: 6rpx;
            position: absolute;
            bottom: 36rpx;
            &.dot-gray {
              background: #18b566;
            }
          }
        }
        .day-month{
          position: absolute;
          top: 0;
          bottom: 0;
          left: 0;
          right: 0;
          display: flex;
          flex-direction: row;
          justify-content: center;
          align-items: center;
          width: 100%;
          height: 100%;
          // color: rgba(231,232,234,.83);
          font-size: 200px;
          font-weight: 700;
          color: #999;
          opacity: .1;
          text-align: center;
          line-height: 1;
          z-index: 1;
        }
      }
      .toggle{
        position: relative;
        padding: 10rpx 0;
        margin: 10rpx 20rpx 0;
        display: flex;
        justify-content: center;
        &:before{
          width: calc(50% - 30rpx);
          border-top: solid 1px #EAEAEA;
          content: '';
          display: block;
          position: absolute;
          top: 50%;
          left: 0;
          transform: translateY(-50%);
        }
        &:after{
          width: calc(50% - 30rpx);
          border-top: solid 1px #EAEAEA;
          content: '';
          display: block;
          position: absolute;
          top: 50%;
          right: 0;
          transform: translateY(-50%);
        }
      }
    }
    .task-box{
      display: flex;
      flex-direction: column;
      .task-item{
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        background-color: white;
        padding: 20rpx;
        box-sizing: border-box;
        border-radius: 10rpx;
        margin-bottom: 20rpx;
        .avatar-box{
          display: flex;
          .avatar{
            width: 100rpx;
            height: 100rpx;
            margin-right: 20rpx;
            border-radius: 50%;
            image{
              width: 100rpx;
              height: 100rpx;
              border-radius: 50%;
            }
          }
          .title-box{
            display: flex;
            flex-direction: column;
            align-content: space-between;
            .title{
              font-size: 30rpx;
              color: #303133;
              margin-bottom: 15rpx;
            }
            .date{
              font-size: 26rpx;
              color: #909193;
              .branch{
                margin-right: 15rpx;
              }
            }
          }
        }
      }
    }
  }
  .modal{
    display: flex;
    flex-direction: column;
    flex: 1;
    .mask{
      transition-duration: 450ms;
      transition-timing-function: ease-out;
      position: fixed;
      inset: 0px;
      z-index: 10070;
      background-color: rgba(0, 0, 0, 0.5);
    }
    .z-content{
      z-index: 10075;
      position: fixed;
      display: flex;
      align-items: center;
      justify-content: center;
      inset: 0px;
      .modal-content{
        border-radius: 6px;
        overflow: hidden;
        margin-top: 0px;
        // height: 200px;
        background-color: #fff;
        position: relative;
        .z-modal{
          width: 289px;
          border-radius: 6px;
          overflow: hidden;
          .modal-title{
            font-size: 16px;
            font-weight: 700;
            color: #606266;
            text-align: center;
            padding-top: 25px;
          }
          .z-modal-content{
            padding: 12px 25px 25px 25px;
            display: flex;
            flex-direction: row;
            justify-content: center;
            font-size: 15px;
            color: #606266;
          }
          .line{
            margin: 0px;
            border-bottom: 1px solid rgb(214, 215, 217);
            width: 100%;
            transform: scaleY(0.5);
            border-top-color: rgb(214, 215, 217);
            border-right-color: rgb(214, 215, 217);
            border-left-color: rgb(214, 215, 217);
            vertical-align: middle;
          }
          .modal-foot{
            display: flex;
            flex-direction: row;
            font-size: 16px;
            text-align: center;
            color: rgb(96, 98, 102);
            .cancel{
              flex: 1;
              display: flex;
              flex-direction: row;
              justify-content: center;
              align-items: center;
              height: 48px;
            }
            .foot-line{
              margin: 0px;
              border-left: 1px solid rgb(214, 215, 217);
              height: 48px;
              transform: scaleX(0.5);
              border-top-color: rgb(214, 215, 217);
              border-right-color: rgb(214, 215, 217);
              border-bottom-color: rgb(214, 215, 217);
            }
            .confirm{
              flex: 1;
              display: flex;
              flex-direction: row;
              justify-content: center;
              align-items: center;
              height: 48px;
              text{
                color: rgb(41, 121, 255);
              }
            }
          }
        }
      }
    }
  }
</style>


2.2 农历与阳历结合

2.2.1 应用建议

该日历因为配置了每天的农历信息,所以很合适用在需要展示农历以及需要根据农历去拓展相应业务的场景。


2.2.2 完整源码

<template>
  <view class="lunc-calendar">
    <!-- 头部按钮及月份 -->
    <view class="header">
      <view class="head-icon head-pre-month" @click="goPreMonth" v-if="shouChangeBtn"><text></text></view>
      <view class="head-month">{{ selDate.year }}年{{ selDate.month<10?'0'+selDate.month:selDate.month }}月</view>
      <view class="head-icon head-next-month" @click="goNextMonth" v-if="shouChangeBtn"><text></text></view>
      <view class="go-to-today" v-show="showToday" @click="goToday">回到今天</view>
    </view>
    <!-- 星期 -->
    <view class="week-area">
      <view class="week-font" v-for="(item, index) in weekArr" :key="index ">周{{ item }}</view>
    </view>
    <view ref="calendar" class="data-container">
      <!-- 日期 -->
      <swiper class="calendar-swiper" :current="tranIndex" circular :duration="tranDuration" @change="changeMonth" @animationfinish="tranChangeEnd">
        <swiper-item class="swiper-item" v-for="(item, index) in allDataArr" :key="index">
          <view class="bg-month" v-show="showMonthBg">{{ item[15].sMonth<10?'0'+item[15].sMonth:item[15].sMonth }}</view>
          <view class="month-days">
            <view class="day" v-for="(d, j) in item" :key="j" @click="clickDay(d)">
              <view class="day-info" :class="[d.dayClass, (selDate.year == d.sYear && selDate.month == d.sMonth && selDate.day == d.day)?'is-sel':'']">
                <text class="day-solar">{{d.day}}</text>
                <text class="day-tag" v-if="d.daySign && JSON.stringify(d.daySign) != '{}'"></text>
                <text class="day-sign" v-if="d.daySign && JSON.stringify(d.daySign) != '{}'">{{d.daySign.title}}</text>
                <text class="day-lunar" v-else v-show="showLunar">{{d.dayLunar}}</text>
              </view>
            </view>
          </view>
        </swiper-item>
      </swiper>
      <!-- 收缩按钮 -->
      <view></view>
    </view>
  </view>
</template>
<script>
  let { calendar } = require("./calendar.js");
  /**
   * @property {Boolean} showLunar = [true|false] 是否显示农历,默认false
   * @property {String} firstDayOfWeek = [monday|sunday] 周几为每周的第一天,默认monday
   *  @value monday 每周从周一开始(默认)
   *  @value sunday 每周从周日开始
   * @property {Boolean} showMonthBg = [true|false] 是否显示月份背景,默认true
   * @property {Boolean} weekend = [true|false] 周末标红(周六周日日期用红色字体),默认false
   * @property {Boolean} shouChangeBtn = [true|false] 是否显示上月下月箭头按钮,默认false
   * @property {Array} signList 标记数组,若当前有多个标记,则显示最后一个,期待格式[{date: '2021-09-10', title: '生日', info: '八月初四张三生日'}]
   * @property {Boolean} isShrink = [true|false] (暂不可用,待完善)是否可收缩,收起来后以周显示,默认false
   *  @value true 收缩,只显示当前一周的日期
   *  @value false 不收缩,显示当前一个月的日期(默认)
   * @event {Function()} dayChange 点击日期触发事件
   * @event {Function()} monthChange 切换月份触发事件
   */
  export default {
    name: 'LuncCalendar',
    props: {
      //是否显示农历
      showLunar: {
        type: Boolean,
        default: false
      },
      //每周的周几为第一天
      firstDayOfWeek: {
        type: String,
        default: 'monday'
      },
      //是否显示月份背景
      showMonthBg: {
        type: Boolean,
        default: true
      },
      //周末标红
      weekend: {
        type: Boolean,
        default: false
      },
      //是否显示上月下月按钮
      shouChangeBtn: {
        type: Boolean,
        default: false
      },
      //标记
      signList: {
        type: Array,
        default () {
          return []
        }
      },
      //是否可收缩,收起来后以周显示(待完善)
      isShrink: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        weekArr: ['一', '二', '三', '四', '五', '六', '日'], //星期数组
        allDataArr: [], //轮播数组
        tranIndex: 1, //轮播所在位置
        selDate: {}, //当前轮播日期信息 -> year, month, day, week
        today: {}, //今天日期 -> year, month, day, week
        tranDuration: 500, //轮播时间(单位毫秒)
        showToday: false, //显示回到今天(非当月才显示)
      }
    },
    created() {
      let nd = new Date();
      this.today = {
        year: nd.getFullYear(),
        month: nd.getMonth() + 1,
        day: nd.getDate()
      }
      if (this.firstDayOfWeek == "sunday") this.weekArr = ['日', '一', '二', '三', '四', '五', '六'];
      else this.weekArr = ['一', '二', '三', '四', '五', '六', '日'];
      this.initDateTime();
    },
    watch:{
      signList(newList){
        newList.map(info => {
          let [year, month, day] = info.date.split("-");
          let allDataArr = this.allDataArr;
          let mList = [allDataArr[0][15].sMonth, allDataArr[1][15].sMonth, allDataArr[2][15].sMonth];
          let num = mList.indexOf(parseInt(month));
          let dayInfo = this.allDataArr[num].find(d => d.sYear == parseInt(year)&&d.sMonth == parseInt(month)&&d.day == parseInt(day));
          dayInfo.daySign = info;
        })
      }
    },
    methods: {
      initDateTime() { //初始化日期时间
        this.selDate = JSON.parse(JSON.stringify(this.today));
        //获取月数据
        let thisMonth = this.getMonthData(this.selDate, true);
        let preMonth = this.getMonthData(this.getPreMonth(this.selDate), false);
        let nextMonth = this.getMonthData(this.getNextMonth(this.selDate), false);
        this.allDataArr = [preMonth, thisMonth, nextMonth];
        this.tranIndex = 1;
      },
      //获取指定月份的数据,selMonth:是否为显示月
      getMonthData(date, selMonth) {
        const { year, month, day } = date; //指定的月份
        let maxDay = new Date(year, month, 0).getDate(); //当前月最大日期
        let firstWeek = new Date(year + "-" + month + "-1").getDay(); //月份1号的星期数
        if (this.firstDayOfWeek == "sunday") firstWeek = firstWeek;
        else firstWeek = firstWeek - 1 < 0 ? 6 : firstWeek - 1;
        let list = [];
        //每月显示42天,6周,每周7天
        for (var i = 0; i < 42; i++) {
          let dayInfo = {};
          if (i < firstWeek) { //指定月份上月的最后几天
            let preYear = this.getPreMonth(date).year; //指定月份的上月
            let preMonth = this.getPreMonth(date).month;
            let preMaxDay = new Date(preYear, preMonth, 0).getDate(); //上月最大日期
            let day = preMaxDay - firstWeek + i + 1;
            dayInfo = this.getDayInfo({ year: preYear, month: preMonth, day: day }, 'pre');
          } else if (i > maxDay + firstWeek - 1) { //指定月份下月的前几天
            let nextYear = this.getNextMonth(date).year; //指定月份的下个月
            let nextMonth = this.getNextMonth(date).month;
            let day = i - maxDay - firstWeek + 1;
            dayInfo = this.getDayInfo({ year: nextYear, month: nextMonth, day: day }, 'next');
          } else {
            let day = i - firstWeek + 1;
            dayInfo = this.getDayInfo({ year: year, month: month, day: day }, 'normal');
          }
          list.push(dayInfo)
        }
        return list;
      },
      //获取当前日期的详细信息
      getDayInfo(date, showDay) {
        const { year, month, day } = date;
        let dayType = showDay; //日期是否在指定月份
        let isToday = false; //是否今天
        if (year == this.today.year && month == this.today.month && day == this.today.day) isToday = true;
        let lunar = calendar.solar2lunar(year, month, day); //农历
        let dayLunar = lunar.IDayCn == '初一' ? lunar.IMonthCn + lunar.IDayCn : lunar.IDayCn;
        let isFestival = false; //是否为节假日
        if (lunar.lunarFestival || lunar.festival) {
          if (lunar.lunarFestival) dayLunar = lunar.lunarFestival;
          if (lunar.festival) dayLunar = lunar.festival;
          isFestival = true;
        }
        let week = new Date(year + "-" + month + "-" + day).getDay(); //星期数
        let daySign = {}; //日历中显示标记
        this.signList.map(item => {
          let sY = new Date(item.date).getFullYear();
          let sM = new Date(item.date).getMonth() + 1;
          let sD = new Date(item.date).getDate();
          if (sY == year && sM == month && sD == day) daySign = item;
        });
        let dayInfo = { day, sYear: year, sMonth: month, week, lunar, dayLunar, isFestival, isToday, dayType, daySign }
        let dayClass = this.getDayClass(dayInfo);
        if (dayClass) dayInfo["dayClass"] = dayClass;
        return dayInfo;
      },
      //根据日期详细信息添加对应的class
      getDayClass(dayInfo) {
        const { year, month, day } = this.selDate;
        let dClass = "";
        if (dayInfo.isToday) dClass += ' is-today';
        if (this.showLunar && dayInfo.isFestival) dClass += ' is-festival';
        if (dayInfo.dayType != 'normal') dClass += ' un-month';
        if (this.weekend && (dayInfo.week == 0 || dayInfo.week == 6)) dClass += ' week-end';
        return dClass
      },
      //滑动切换轮播图
      changeMonth(e) {
        let current = e.detail.current;
        let oldIndex = this.tranIndex;
        this.tranIndex = current;
        if (current != oldIndex) {
          if (oldIndex - current == -1 || oldIndex - current == 2) this.getNextData();
          else this.getPreData();
        }
        //判断是否为本月
        if (this.selDate.year == this.today.year && this.selDate.month == this.today.month) this.showToday = false;
        else this.showToday = true;
      },
      tranChangeEnd() {
        this.tranDuration = 500; // 动画结束重新设置动画时间
      },
      //回到今天按钮
      goToday() {
        if (this.tranDuration != 0) this.tranDuration = 0;
        this.initDateTime();
        let today = this.today;
        let dayInfo = this.allDataArr[this.tranIndex].filter(item => (item.sYear == today.year && item.sMonth ==
          today.month && item.day == today.day))[0];
        if (dayInfo) this.clickDay(dayInfo);
        this.$emit('monthChange', {
          year: new Date().getFullYear(),
          month: new Date().getMonth() + 1,
          type: "today"
        });
      },
      //点击上月按钮
      goPreMonth() {
        if (this.tranDuration != 0) this.tranDuration = 0;
        let current = this.tranIndex - 1 < 0 ? 2 : this.tranIndex - 1;
        this.tranIndex = current;
        this.getPreData();
      },
      //点击下月按钮
      goNextMonth() {
        if (this.tranDuration != 0) this.tranDuration = 0;
        let current = this.tranIndex + 1 > 2 ? 0 : this.tranIndex + 1;
        this.tranIndex = current;
        this.getNextData();
      },
      //获取上上月数据
      getPreData() {
        let num = this.tranIndex - 1 < 0 ? 2 : this.tranIndex - 1; // 上上月数据在 所有数据数组中的下标
        let preMonth = this.getPreMonth(this.selDate); //获取上月月份
        let preMonthData = this.getMonthData(this.getPreMonth(preMonth), false); //获取上上月的月份数据
        this.allDataArr.splice(num, 1, preMonthData); //上上月的数据
        this.selDate = preMonth;
        let dayInfo = this.allDataArr[this.tranIndex].filter(item => (item.sYear == preMonth.year && item.sMonth ==
          preMonth.month && item.day == preMonth.day))[0];
        if (dayInfo) this.clickDay(dayInfo);
        this.$emit('monthChange', {
          year: preMonth.year,
          month: preMonth.month,
          type: "pre"
        });
      },
      //获取下下月数据
      getNextData() {
        let num = this.tranIndex + 1 > 2 ? 0 : this.tranIndex + 1;
        let nextMonth = this.getNextMonth(this.selDate);
        let nextMonthData = this.getMonthData(this.getNextMonth(nextMonth), false);
        this.allDataArr.splice(num, 1, nextMonthData);
        this.selDate = nextMonth;
        let dayInfo = this.allDataArr[this.tranIndex].filter(item => (item.sYear == nextMonth.year && item
          .sMonth == nextMonth.month && item.day == nextMonth.day))[0];
        if (dayInfo) this.clickDay(dayInfo);
        this.$emit('monthChange', {
          year: nextMonth.year,
          month: nextMonth.month,
          type: "next"
        });
      },
      //获取上月的月份
      getPreMonth(date) {
        let year = date.month - 1 < 1 ? date.year - 1 : date.year;
        let month = date.month - 1 < 1 ? 12 : date.month - 1;
        let day = this.getOtherMonthDay(year, month, date.day);
        return {
          year,
          month,
          day
        }
      },
      //获取下月的月份
      getNextMonth(date) {
        let year = date.month + 1 > 12 ? date.year + 1 : date.year;
        let month = date.month + 1 > 12 ? 1 : date.month + 1;
        let day = this.getOtherMonthDay(year, month, date.day);
        return {
          year,
          month,
          day
        }
      },
      getOtherMonthDay(year, month, day){
        let monthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        let februaryDay = new Date(year, month, 0).getDate();
        monthDays.splice(1, 1, februaryDay);
        let maxDay = day;
        if (maxDay > monthDays[month - 1]) maxDay = monthDays[month - 1];
        return maxDay;
      },
      //点击日期
      clickDay(dayInfo) {
        if (this.selDate.day == dayInfo.day && this.selDate.month == dayInfo.month && this.selDate.year == dayInfo.year) return;
        this.selDate.day = dayInfo.day;
        if (dayInfo.dayType == 'normal') {
          this.selDate = {
            year: dayInfo.sYear,
            month: dayInfo.sMonth,
            day: dayInfo.day
          }
        } else if (dayInfo.dayType == 'pre') {
          this.goPreMonth();
          return;
        } else if (dayInfo.dayType == 'next') {
          this.goNextMonth();
          return;
        }
        let WeekArr = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
        let lunar = {
          Animal: dayInfo.lunar.Animal,
          gzYear: dayInfo.lunar.gzYear,
          gzMonth: dayInfo.lunar.gzMonth,
          gzDay: dayInfo.lunar.gzDay,
          IMonthCn: dayInfo.lunar.IMonthCn,
          IDayCn: dayInfo.lunar.IDayCn,
          festival: dayInfo.lunar.festival,
          lunarFestival: dayInfo.lunar.lunarFestival
        }
        let returnData = {
          date: dayInfo.sYear + "-" + (dayInfo.sMonth < 10 ? '0' + dayInfo.sMonth : dayInfo.sMonth) + "-" + (
            dayInfo.day < 10 ? '0' + dayInfo.day : dayInfo.day),
          day: dayInfo.day,
          month: dayInfo.sMonth,
          year: dayInfo.sYear,
          week: WeekArr[dayInfo.week],
          daySign: dayInfo.daySign
        }
        if (this.showLunar) returnData["lunar"] = lunar;
        this.$emit('dayChange', returnData);
      }
    }
  }
</script>
<style lang="scss">
  .lunc-calendar {
    background-color: #FFF;
    margin-bottom: 40rpx;
    .header {
      display: flex;
      flex-direction: row;
      justify-content: center;
      position: relative;
      height: 90rpx;
      line-height: 90rpx;
      border-bottom: 1px solid #DDD;
      .head-month {
        font-size: 36rpx;
        text-align: center;
        padding: 0 40rpx;
      }
      .head-icon {
        width: 60rpx;
        text-align: center;
        align-items: center;
        justify-content: center;
        text {
          display: inline-block;
          width: 20rpx;
          height: 20rpx;
          border-top: 4rpx solid #AAA;
          border-left: 4rpx solid #AAA;
        }
      }
      .head-pre-month {
        transform: rotate(-45deg);
      }
      .head-next-month {
        transform: rotate(135deg);
      }
      .go-to-today {
        position: absolute;
        right: 0;
        top: 20rpx;
        padding: 8rpx 12rpx 8rpx 22rpx;
        background-color: rgba(255, 184, 0, 1);
        border-radius: 22rpx 0 0 22rpx;
        font-size: 24rpx;
        line-height: 24rpx;
        color: #FFF;
      }
    }
    .week-area {
      display: flex;
      flex-direction: row;
      align-items: center;
      border-bottom: 1px solid #EEE;
      padding: 16rpx 0;
      margin: 0 20rpx;
      .week-font {
        flex: 1;
        text-align: center;
        color: #666;
        font-size: 14px;
      }
    }
    .data-container {
      margin: 0 20rpx;
      .calendar-swiper{
        height: 550rpx;
      }
      .swiper-item {
        position: relative;
        display: -webkit-box;
        .bg-month {
          position: absolute;
          font-size: 200px;
          font-weight: bold;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -55%);
          opacity: 0.05;
          z-index: -1;
          word-break: initial;
        }
        .month-days {
          margin-top: 15rpx;
          display: block;
          .day {
            width: 14.28%;
            display: inline-block;
            text-align: center;
            height: 84rpx;
            color: #000;
            padding: 0 6rpx;
            box-sizing: border-box;
            .day-info {
              height: 100%;
              overflow: hidden;
              display: flex;
              flex-direction: column;
              justify-content: center;
              align-items: center;
              position: relative;
              .day-solar {
                display: block;
                font-size: 34rpx;
                line-height: 34rpx;
              }
              .day-lunar {
                color: #666;
                font-size: 24rpx;
                line-height: 24rpx;
                transform: scale(0.8);
                white-space: nowrap;
              }
              .day-sign {
                color: rgba(247, 88, 88, 1);
                font-size: 24rpx;
                line-height: 24rpx;
                transform: scale(0.8);
                white-space: nowrap;
              }
              .day-tag {
                position: absolute;
                top: 6rpx;
                right: 14rpx;
                width: 10rpx;
                height: 10rpx;
                border-radius: 50%;
                display: inline-block;
                background-color: rgba(247, 88, 88, 1);
              }
            }
            //非当前月日期
            .un-month text {
              opacity: 0.25;
            }
            //周末
            .week-end .day-solar {
              color: rgba(255, 137, 137, 1);
            }
            //节假日
            .is-festival .day-lunar,
            .is-festival .day-solar {
              color: rgba(247, 88, 88, 1);
            }
            //今天日期
            .is-today .day-solar,
            .is-today .day-lunar {
              color: #409eff;
            }
            //选择日期
            .is-sel {
              background-color: rgba(198, 226, 255, 1);
            }
          }
        }
      }
    }
  }
</style>


2.3 日历带日程样式优化

2.3.1 应用建议

该样例的效果在于日历配合了日程相使用,而且如果用户的某一天的日程安排过多的话,可以实现动态折叠效果。


2.3.2 完整源码

<template>
  <view>
  <view class="calendar">
    <view class="top-bar">
      <image class="top-change-month" @click="turning('prev')" src="../../static/attend-calendar/attendance_left.png"></image>
      <view class="top-bar-ym">{{ y }}{{ text.year }}{{ m + 1 }}{{ text.month }}</view>
      <image class="top-change-month" @click="turning('next')" src="../../static/attend-calendar/attendance_right.png"></image>
    </view>
    <view class="week">
      <view class="week-day" v-for="(item, index) in weekDay" :key="index">{{ item }}</view>
    </view>
    <view :class="{ hide: !monthOpen }" class="content" :style="{ height: height }">
      <view :style="{ top: positionTop + 'upx' }" class="days">
        <view class="item" v-for="(item, index) in dates" :key="index">
          <view class="day" @click="selectOne(item, $event)"
            :class="{ choose: choose == `${item.year}-${item.month + 1}-${item.date}`, nolm: !item.lm }">
            {{ item.date }}</view>
          <view class="late" v-if="isLateed(item.year, item.month + 1, item.date)"></view>
          <view class="truancy" v-if="isTruancyed(item.year, item.month + 1, item.date)"></view>
          <view class="today-text" v-if="isToday(item.year, item.month, item.date)">今</view>
        </view>
      </view>
    </view>
    <image src="../../static/attend-calendar/attendance-arrow-up.png" mode="scaleToFill" @click="trgWeek()" class="weektoggel"
      :class="{ down: !monthOpen }"></image>
  </view>
  <view class="task-box" v-if="list.length > 0">
    <view class="task-item" v-for="(item, index) in list" :key="index" @click="toTask(item, index)">
      <view class="avatar-box">
        <view class="avatar">
           <image :src="item.avatar"></image>
        </view>
        <view class="title-box">
          <view class="title"><text>{{item.title}}</text></view>
          <view class="date"><text class="branch">时间:{{item.time}}</text><text>{{item.details}}</text></view>
        </view>
      </view>
      <view class="time"><text>{{item.date}}</text></view>
    </view>
  </view>
  <view class="modal" v-if="show">
    <view class="mask" @click="close" v-if="closeOnClickOverlay"></view>
    <view class="z-content">
      <view class="modal-content">
        <view class="z-modal" :style="{width: width}">
          <view class="modal-title"><slot name="title"><text>{{title}}</text></slot></view>
          <view class="z-modal-content"><slot name="content"><text>{{content}}</text></slot></view>
          <view class="line"></view>
          <view class="modal-foot">
            <slot name="footer">
              <view class="cancel" @click="cancel" v-if="showCancelButton">
                <text :style="{color: cancelColor}">{{cancelText}}</text>
              </view>
              <view class="foot-line" ></view>
              <view class="confirm" @click="confirm">
                <text style=" color: rgb(71 190 190);">确定</text>
              </view>
            </slot>
          </view>
        </view>
      </view>
    </view>
  </view>
  </view>
</template>
<script>
  export default {
    name: 'attend-calendar',
    props: {
      // 第一列星期几
      weekstart: {
        type: Number,
        default: 1
      },
      // 只有迟到的日期
      lateddates: {
        type: Array,
        default: () => []
      },
      // 有旷课的日期
      truancyeddates: {
        type: Array,
        default: () => []
      },
      // 是否展开
      open: {
        type: Boolean,
        default: true
      }
    },
    data() {
      return {
        show:false,
        title:'添加日程',
        content:'与迪丽热巴约会',
        list: [
                   {
                       avatar: 'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/6acec660-4f31-11eb-a16f-5b3e54966275.jpg',
                       title: '与古力娜扎约会',
                       time: '45分钟',
                       details:'首要任务',
                       date: '10:30'
                   }
               ],
        text: {
          year: '年',
          month: '月',
          week: ['一', '二', '三', '四', '五', '六', '日'],
          today: '今'
        },
        y: new Date().getFullYear(), // 年
        m: new Date().getMonth(), // 月
        dates: [], // 当前月日期集合
        positionTop: 0,
        monthOpen: true,
        choose: ''
      }
    },
    created() {
    this.dates = this.monthDay(this.y, this.m)
    !this.open && this.trgWeek()
    },
    mounted() {
      let date = new Date()
      let y = date.getFullYear()
      let m = date.getMonth()
      let d = date.getDate()
      this.choose = `${y}-${m + 1}-${d}`
      console.log(this.choose)
    },
    computed: {
      // 顶部星期栏目
      weekDay() {
        return this.text.week.slice(this.weekstart - 1).concat(this.text.week.slice(0, this.weekstart - 1))
      },
      height() {
        return (this.dates.length / 7) * 80 + 'upx'
      }
    },
    methods: {
      // 获取当前月份天数
      monthDay(y, m) {
        let firstDayOfMonth = new Date(y, m, 1).getDay() // 当月第一天星期几
        let lastDateOfMonth = new Date(y, m + 1, 0).getDate() // 当月最后一天
        let lastDayOfLastMonth = new Date(y, m, 0).getDate() // 上一月的最后一天
        let dates = [] // 所有渲染日历
        let weekstart = this.weekstart == 7 ? 0 : this.weekstart // 方便进行日期计算,默认星期从0开始
        let startDay = (() => {
          // 周初有几天是上个月的
          if (firstDayOfMonth == weekstart) {
            return 0
          } else if (firstDayOfMonth > weekstart) {
            return firstDayOfMonth - weekstart
          } else {
            return 7 - weekstart + firstDayOfMonth
          }
        })()
        let endDay = 7 - ((startDay + lastDateOfMonth) % 7) // 结束还有几天是下个月的
        for (let i = 1; i <= startDay; i++) {
          dates.push({
            date: lastDayOfLastMonth - startDay + i,
            day: weekstart + i - 1 || 7,
            month: m - 1 >= 0 ? m - 1 : 12,
            year: m - 1 >= 0 ? y : y - 1
          })
        }
        for (let j = 1; j <= lastDateOfMonth; j++) {
          dates.push({
            date: j,
            day: (j % 7) + firstDayOfMonth - 1 || 7,
            month: m,
            year: y,
            lm: true
          })
        }
        for (let k = 1; k <= endDay; k++) {
          dates.push({
            date: k,
            day: (lastDateOfMonth + startDay + weekstart + k - 1) % 7 || 7,
            month: m + 1 <= 11 ? m + 1 : 0,
            year: m + 1 <= 11 ? y : y + 1
          })
        }
        return dates
      },
      // 迟到处理
      isLateed(y, m, d) {
        let flag = false
        for (let i = 0; i < this.lateddates.length; i++) {
          let dy = `${y}-${m}-${d}`
          if (this.lateddates[i] == dy) {
            flag = true
            break
          }
        }
        return flag
      },
      // 旷课处理
      isTruancyed(y, m, d) {
        let flag = false
        for (let i = 0; i < this.truancyeddates.length; i++) {
          let dy = `${y}-${m}-${d}`
          if (this.truancyeddates[i] == dy) {
            flag = true
            break
          }
        }
        return flag
      },
      isToday(y, m, d) {
        let date = new Date()
        return y == date.getFullYear() && m == date.getMonth() && d == date.getDate()
      },
      // 切换成周模式
      trgWeek() {
        this.monthOpen = !this.monthOpen
        if (this.monthOpen) {
          this.positionTop = 0
        } else {
          let index = -1
          this.dates.forEach((i, x) => {
            this.isToday(i.year, i.month, i.date) && (index = x)
          })
          this.positionTop = -((Math.ceil((index + 1) / 7) || 1) - 1) * 80
        }
      },
      // 点击回调
      selectOne(i, event) {
        let date = `${i.year}-${i.month + 1}-${i.date}`
        if (i.month != this.m) {
          console.log('不在可选范围内')
          return false
        }
        this.choose = date
        this.$emit('on-click', date)
        this.show = true
      },
      // 上个月,下个月
      turning(_action) {
        if (_action === 'next') {
          if (this.m + 1 == 12) {
            this.m = 0
            this.y = this.y + 1
          } else {
            this.m = this.m + 1
          }
        } else {
          if (this.m + 1 == 1) {
            this.m = 11
            this.y = this.y - 1
          } else {
            this.m = this.m - 1
          }
        }
        this.dates = this.monthDay(this.y, this.m)
      },
      confirm(){
        this.show=false
        this.list.push( {
                       avatar: 'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/6acec660-4f31-11eb-a16f-5b3e54966275.jpg',
                       title: '与迪丽热巴约会',
                       time: '45分钟',
                       details:'首要任务',
                       date: '10:30'
                   })
      }
    }
  }
</script>
<style lang="scss" scoped>
  .calendar {
    color: #fff;
    font-size: 28upx;
    text-align: center;
    background-color: #2356FE;
    padding-bottom: 10upx;
    .top-bar {
      display: flex;
      height: 80upx;
      >view {
        flex: 1;
      }
      .top-bar-ym{
        font-size: 32upx;
        height: 80upx;
        line-height: 80upx;
      }
      .top-change-month{
        height: 80upx;
        width: 80upx;
      }
    }
    .week {
      display: flex;
      align-items: center;
      height: 80upx;
      line-height: 80upx;
      view {
        flex: 1;
      }
      .week-day{
        font-size: 24upx;
        color: #A9C4FF;
      }
    }
    .content {
      position: relative;
      overflow: hidden;
      transition: height 0.4s ease;
      .days {
        transition: top 0.3s;
        display: flex;
        align-items: center;
        flex-wrap: wrap;
        position: relative;
        .item {
          position: relative;
          display: block;
          height: 80upx;
          line-height: 80upx;
          width: calc(100% / 7);
          .day {
            font-style: normal;
            display: inline-block;
            vertical-align: middle;
            width: 60upx;
            height: 60upx;
            line-height: 60upx;
            overflow: hidden;
            border-radius: 14upx;
            &.choose {
              background-color: #3DB7BA;
              color: #FFFFFF;
            }
            &.nolm {
              color: #fff;
              opacity: 0.3;
            }
          }
          .late {
            bottom: 0;
            left: 50%;
            font-style: normal;
            width: 12upx;
            height: 12upx;
            background: #F7B300;
            border-radius: 6upx;
            position: absolute;
            margin-left: -6upx;
            pointer-events: none;
          }
          .truancy {
            bottom: 0;
            left: 50%;
            font-style: normal;
            width: 12upx;
            height: 12upx;
            background: #FF2222;
            border-radius: 6upx;
            position: absolute;
            margin-left: -6upx;
            pointer-events: none;
          }
          .today-text {
            position: absolute;
            font-size: 20upx;
            font-weight: normal;
            width: 20upx;
            height: 20upx;
            line-height: 20upx;
            right: 0;
            top: 10upx;
            color: #fff;
          }
        }
      }
    }
    .hide {
      height: 80upx !important;
    }
    .weektoggel {
      width: 80upx;
      height: 40upx;
      margin: 10upx auto 0;
      &.down {
        transform: rotate(180deg);
      }
    }
  }
  .modal{
    display: flex;
    flex-direction: column;
    flex: 1;
    .mask{
      transition-duration: 450ms;
      transition-timing-function: ease-out;
      position: fixed;
      inset: 0px;
      z-index: 10070;
      background-color: rgba(0, 0, 0, 0.5);
    }
    .z-content{
      z-index: 10075;
      position: fixed;
      display: flex;
      align-items: center;
      justify-content: center;
      inset: 0px;
      .modal-content{
        border-radius: 6px;
        overflow: hidden;
        margin-top: 0px;
        // height: 200px;
        background-color: #fff;
        position: relative;
        .z-modal{
          width: 289px;
          border-radius: 6px;
          overflow: hidden;
          .modal-title{
            font-size: 16px;
            font-weight: 700;
            color: #606266;
            text-align: center;
            padding-top: 25px;
          }
          .z-modal-content{
            padding: 12px 25px 25px 25px;
            display: flex;
            flex-direction: row;
            justify-content: center;
            font-size: 15px;
            color: #606266;
          }
          .line{
            margin: 0px;
            border-bottom: 1px solid rgb(214, 215, 217);
            width: 100%;
            transform: scaleY(0.5);
            border-top-color: rgb(214, 215, 217);
            border-right-color: rgb(214, 215, 217);
            border-left-color: rgb(214, 215, 217);
            vertical-align: middle;
          }
          .modal-foot{
            display: flex;
            flex-direction: row;
            font-size: 16px;
            text-align: center;
            color: rgb(96, 98, 102);
            .cancel{
              flex: 1;
              display: flex;
              flex-direction: row;
              justify-content: center;
              align-items: center;
              height: 48px;
            }
            .foot-line{
              margin: 0px;
              border-left: 1px solid rgb(214, 215, 217);
              height: 48px;
              transform: scaleX(0.5);
              border-top-color: rgb(214, 215, 217);
              border-right-color: rgb(214, 215, 217);
              border-bottom-color: rgb(214, 215, 217);
            }
            .confirm{
              flex: 1;
              display: flex;
              flex-direction: row;
              justify-content: center;
              align-items: center;
              height: 48px;
              text{
                color: rgb(41, 121, 255);
              }
            }
          }
        }
      }
    }
  }
    .task-box{
      display: flex;
      flex-direction: column;
      .task-item{
        display: flex;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        background-color: white;
        padding: 20rpx;
        box-sizing: border-box;
        border-radius: 10rpx;
        margin-bottom: 20rpx;
        -moz-box-shadow:2px 1px 10px #3333334f; -webkit-box-shadow:2px 1px 10px #3333334f; box-shadow:2px 1px 10px #3333334f;
        border-radius: 10px;
        margin: 5px 5px;
        .avatar-box{
          display: flex;
          .avatar{
            width: 100rpx;
            height: 100rpx;
            margin-right: 20rpx;
            border-radius: 50%;
            image{
              width: 100rpx;
              height: 100rpx;
              border-radius: 50%;
            }
          }
          .title-box{
            display: flex;
            flex-direction: column;
            align-content: space-between;
            .title{
              font-size: 30rpx;
              color: #303133;
              margin-bottom: 15rpx;
            }
            .date{
              font-size: 26rpx;
              color: #909193;
              .branch{
                margin-right: 15rpx;
              }
            }
          }
        }
      }
    }
</style>


2.4 日历之打卡签到与补签

2.4.1 应用建议

该样例在满足了日期展示的前提下,又兼顾了用户的考勤功能。你可以根据用户的打卡信息对用户的考勤数据进行管理。


2.4.2 完整源码

<template>
  <!-- 打卡日历页面 -->
  <view class='all'>
    <view class="bar">
      <!-- 上一个月 -->
      <view class="previous" @click="changeMonth(-1)">
        <button class="barbtn">{{langType=='ch'?'上一月':'Last'}}</button>
      </view>
      <!-- 显示年月 -->
      <view class="date">{{nowYear || "--"}} 年 {{nowMonth || "--"}} 月</view>
      <!-- 下一个月 -->
      <view class="next" @click="changeMonth(1)">
        <button class="barbtn">{{langType=='ch'?'下一月':'Nex/'}}</button>
      </view>
    </view>
    <!-- 显示星期 -->
    <view class="week-area" >
      <view class="week-txt" v-for="(item,index) in weeksTxt[langType]" :key="index">{{item}}</view>
    </view>
    <view  class="myDateTable">
      <view v-for="(item,j) in calendarDays" :key="j" class='dateCell'>
        <view v-if="item.date==undefined||item.date == null" class='cell'></view>
        <template v-else>
          <!-- 已签到日期 -->
          <view v-if="item.isSign == true" class='cell greenColor bgWhite'>
            {{item.date}}
          </view>
          <!-- 漏签 -->
          <view @click="clickSign(item.date,0)" class="cell outSignStyle"  v-else-if="item.isBeforeToday&&item.isThisMonth">
            <!-- redColor bgGray -->
            {{item.date}}
            <view class="redDot"></view>
          </view>
          <!-- 今日未签到-->
          <view @click="clickSign(item.date,1)" class="cell whiteColor bgBlue" v-else-if="item.date==today&&nowMonth==toMonth&&nowYear==toYear">
            签到
          </view>
          <!-- 当前日期之后 -->
          <view class="whiteColor cell" v-else>
            {{item.date}}
          </view>
        </template>
      </view>
    </view>
  </view>
</template>
<script>
  export default {
    data() {
      return {
        calendarDays: [],
        SignData: [],// 已经签到的数据
        nowYear: 0, //当前选的年
        nowMonth: 0, //当前选的月
        today: parseInt(new Date().getDate()), //系统本日
        toMonth: parseInt(new Date().getMonth() + 1), //系统本月
        toYear: parseInt(new Date().getFullYear()), //系统本年
        weeksTxt: {
          ch:['日', '一', '二', '三', '四', '五', '六'],
          en: ['Sun', 'Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat'],
        },
      };
    },
    props: {
      isReplenishSign: { // 是否允许过期补签
        type: Boolean,
        default: false
      },
      isFullCalendar: { // 是否需要填满日历,前后空的格子填入上/下个月的日期
        type: Boolean,
        default: true
      },
      yearMonth: { // 2022-01 这种格式,默认当前年月
        type: String,
        default: new Date().getFullYear()+'-'+  parseInt(new Date().getMonth() + 1)
      },
      dataSource: { //已签到的数据源,例: 5、6号已签到: ["2022-01-05","2022-01-06"],兼容个位数前可加可不加0
        type: Array,
        default: () => {
          return []
        }
      },
      langType: { //只是示例一个翻译而已,要想所有都翻译自己可以再加加
        type: String,
        default: "ch" //en
      },
    },
    created() {
      if(!(/en|ch/g.test(this.langType))){
        this.langType='ch'; // 非中英,则固定中文
      }
      const ymArr=this.yearMonth.split('-');
      this.buildCalendar(ymArr[0], ymArr[1]);
      this.onSignDataChange(this.dataSource);
    },
    watch: {
      dataSource: 'onSignDataChange',
    },
    methods: {
      clickSign(date, type) { //type=0补签,type=1当日签到 
        var strTip = "签到";
        if (type == 0) {
          if(!this.isReplenishSign){ // 未开启补签,阻止继续执行
            console.log("————补签功能未开启————");
            return;
          }
          strTip = "补签";
        }
        uni.showToast({
          title:  date + "号" + strTip + "成功",
          icon: 'success',
          position:"bottom",
        });
        this.$emit('clickChange', this.nowYear + "-" + this.nowMonth + "-" + date); //传给调用模板页面
        // 父页面要在clickChange里去修改输入的数据源dataSource,否则视图不更新的!
      },
      //构建日历数据
      buildCalendar(y, m){
        this.nowYear = y;
        this.nowMonth = m;
        this.calculateEmptyGrids(y, m);
        this.calculateDays(y, m);
        if(this.isFullCalendar){
          this.fullCell()
        }
      },
      // 监听到签到数据源改变
      onSignDataChange(newData, oldData = []) {
        this.SignData = newData;
        this.matchSign();
      },
      //匹配标记已经签到的日子
      matchSign() {
        var signs = this.SignData;
        var daysArr = this.calendarDays;
        for (var i = 0; i < signs.length; i++) {
          var current = new Date(this.toIOSDate(signs[i])).getTime(); // ios只认/格式的日期
          for (var j = 0; j < daysArr.length; j++) {
            if(current == new Date(this.toIOSDate(daysArr[j].fullDate)).getTime()){
                daysArr[j].isSign = true; 
            }
          }
        }
        this.calendarDays = daysArr;
        console.log(this.calendarDays );
      },
      // 计算当月1号前空了几个格子,把它填充在calendarDays数组的前面
      calculateEmptyGrids(year, month) {
        //计算每个月时要清零
        this.calendarDays = [];
        const firstDayOfWeek = this.getFirstDayOfWeek(year, month);
        if (firstDayOfWeek > 0) {
          for (let i = 0; i < firstDayOfWeek; i++) {
            this.calendarDays.push({
              date: null, // 显示的日期
              fullDate: null, // 日期yyyy-mm-dd格式
              isBeforeToday: true, // 今日之前
              isSign: false, // 是否签到
              isThisMonth:false, // 是本月
            });
          }
        }
      },
      // 绘制当月天数占的格子,并把它放到days数组中
      calculateDays(year, month) {
        const thisMonthDays = this.getMonthDayLength(year, month);
        const toDate= new Date(this.toYear+'/'+this.toMonth+'/'+this.today);
        for (let i = 1; i <= thisMonthDays; i++) {
          const fullDate=year+'-'+month+'-'+ i;
          const isBeforeToday=new Date(this.toIOSDate(fullDate)) < toDate;
          this.calendarDays.push({
            date: i,
            fullDate,
            isBeforeToday,
            isSign: false,
            isThisMonth:true,
          });
        }
        //console.log(this.calendarDays);
      },
      // 切换控制年月,上一个月,下一个月
      changeMonth(type) {
        const nowYear = parseInt(this.nowYear);
        const nowMonth = parseInt(this.nowMonth);
        const newObj=this.getOperateMonthDate(nowYear,nowMonth,type);
        this.buildCalendar(newObj.year, newObj.month); // 重新构建日历数据
        this.$emit('dateChange', this.nowYear + "-" + this.nowMonth); //传给调用模板页面去拿新数据       
      },
      // 获取当月共多少天,也就是获取月的最后一天
      getMonthDayLength(year, month) {
        return new Date(year, month, 0).getDate()
      },
      // 获取当月第一天星期几
      getFirstDayOfWeek(year, month, day=1) {
        return new Date(Date.UTC(year, month - 1, day)).getDay();
      },
      toIOSDate(strDate){ // iso不认识"-"拼接的日期,所以转/
        return strDate?strDate.replace(/-/g,'/'):strDate;
      },
      // 需要填满格子,上/下个月的日期拉出来填满格子
      fullCell(){
        const endDay= this.getMonthDayLength(this.nowYear, this.nowMonth);
        const beforeEmptyLength = this.getFirstDayOfWeek(this.nowYear, this.nowMonth);
        const afterEmptyLength =  6 - this.getFirstDayOfWeek(this.nowYear, this.nowMonth, endDay);
        const last=this.getOperateMonthDate(this.nowYear,this.nowMonth,-1);
        const lastMonthEndDay=this.getMonthDayLength(last.year, last.month);
        for (let i = 0; i < beforeEmptyLength; i++) {
          const date=lastMonthEndDay - beforeEmptyLength + i + 1;
          this.calendarDays[i].date=date;
          this.calendarDays[i].fullDate=last.year+"-"+last.month+"-"+date;
        }
        const next=this.getOperateMonthDate(this.nowYear,this.nowMonth,1);
        for (let i = 1; i <= afterEmptyLength; i++) {
          this.calendarDays.push({
            date: i, // 显示的日期
            fullDate: next.year+"-"+next.month+"-"+i, // 日期yyyy-mm-dd格式
            isBeforeToday: false, // 今日之前
            isSign: false, // 是否签到
            isThisMonth:false, // 是本月
          });
        }
        // console.log(beforeEmptyLength,afterEmptyLength,lastMonthEndDay);
        // console.log(this.calendarDays);
      },
      // 获取加/减一个月的日期
      getOperateMonthDate(yy,mm,num){
        let month=parseInt(mm) +  parseInt(num);
        let year=parseInt(yy);
        if(month>12){
          month=1;
          year++;
        }else if(month<1){
          month=12;
          year--;
        }
        return {
          month,
          year,
        }
      },
    }
  }
</script>
<style>
  .all {
    margin-top: 20rpx;
  }
  .all .bar {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    margin: 30rpx 20rpx;
    padding: 10rpx;
  }
  .bar .barbtn {
    height: 30px;
    line-height: 30px;
    font-size: 12px;
  }
  .all .week-area {
    display: flex;
    justify-content: space-between;
    padding: 10px 0;
    box-sizing: border-box;
    width: 91vw;
    margin: 10px auto;
    border-radius: 10px;
  }
  .all .week-txt {
    text-align: center;
    width: 13vw;
  }
  .myDateTable {
    margin: 0 auto;
    width: 91vw;
    padding: 2vw;
    border-radius: 10px;
    background: linear-gradient(#74AADA, #94db98);
  }
  .myDateTable .dateCell {
    width: 13vw;
    padding: 1vw;
    display: inline-block;
    text-align: center;
    font-size: 16px;
    box-sizing: border-box;
    overflow: hidden;
  }
  .dateCell .cell {
    display: flex;
    border-radius: 50%;
    height: 11vw;
    justify-content: center;
    align-items: center;
    box-sizing: border-box;
    flex-direction: column;
  }
  .whiteColor {
    color: #fff;
  }
  .greenColor {
    color: #01b90b;
    font-weight: bold;
  }
  .bgWhite {
    background-color: #fff;
  }
  .bgGray {
    background-color: rgba(255, 255, 255, 0.42);
  }
  .bgBlue {
    font-size: 14px;
    background-color: #4b95e6;
  }
  .redColor {
    color: #ff0000;
  }
  .outSignStyle{
    border:1px solid #ffffff;
    color: #ffffff;
  }
  .redDot{
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background-color: red;
  }
</style>
相关文章
|
3月前
|
JSON 小程序 安全
【经验分享】如何实现小程序日历范围选择功能
【经验分享】如何实现小程序日历范围选择功能
144 2
|
6月前
|
JSON 小程序 JavaScript
微信小程序-vant-weapp日历组件的使用(年月日)
微信小程序-vant-weapp日历组件的使用(年月日)
154 0
|
6月前
|
JSON 小程序 JavaScript
小程序vant-weapp的日历组件的使用
小程序vant-weapp的日历组件的使用
58 0
|
9月前
|
小程序
微信小程序--日历模块页面
微信小程序--日历模块页面
206 0
|
11月前
|
Web App开发 缓存 小程序
教你如何写一个符合自己需求的小程序日历组件
教你如何写一个符合自己需求的小程序日历组件
|
小程序 定位技术 C#
C#编程学习(01):北斗时转日历时的小程序
C#编程学习(01):北斗时转日历时的小程序
C#编程学习(01):北斗时转日历时的小程序
|
JavaScript 前端开发 小程序
小程序日历日期单选选择实现
###小程序日历日期单选选择实现拿走不谢 看图说话 ![1575796331_1_](https://yqfile.alicdn.com/51bf41a1c0d8c45753874db1776d692395757936.
1534 0
小程序日历日期单选选择实现