实现自定义日历展示每日数据功能

背景

我们有时需要以日历的格式展示每日的统计数据,比如下图所示。但是上网上寻找的插件库中,看起来最像能满足需求的 fullcalendar.io/demos 也远远不能满足需求,非常僵化,而且文档非常反人类。

所以老样子,我们自己实现。

最终效果如下:

实现

其实关键部分就2个:

  1. 实现日期的准确计算

  2. 将日期和每天的数据通过单元格的div结合起来

至于事件啦、提示栏啦之类的,因为每一个单元格都是div而已,我们就可以想干什么干什么了。

废话不多说,直接上代码。

Calendar.vue 组件

依赖

  1. 使用了 element-plus 进行月份选择以及样式优化

  2. 使用了vite官方推荐的 github.com/date-fns/da... 库来计算日期

逻辑

简单一句话就是,获取第一行的第一个格子的日期,后面所有的日期自然就获得了。无非是我们使用了6行的布局。

Calendar.vue 组件:

xml 复制代码
<template>
  <article>
    <div class="top felxbox">
      <section class="felxbox month">
        <p class="title">月份</p>
        <el-date-picker
          :clearable="false"
          v-model="activeStartDate"
          type="month"
          placeholder="选择月份"
          @change="handleMonthChange"
        />
      </section>

      <section>
        <el-select
          v-model="colStartFrom"
          style="width: 100px; transform: translateY(1px)"
          placeholder="Select"
          @change="sendStartEnd"
        >
          <el-option label="周日起始" value="0" />
          <el-option label="周一起始" value="1" />
        </el-select>
        <el-button type="primary" class="ml10" :icon="Refresh" @click="changeMonth(0)">本月</el-button>
        <el-button-group>
          <el-button type="primary" class="ml10" :icon="ArrowLeftBold" @click="changeMonth(-1)">上月</el-button>
          <el-button type="primary" class="" @click="changeMonth(1)"
            >下月<el-icon class="el-icon--right"><ArrowRightBold /></el-icon
          ></el-button>
        </el-button-group>
      </section>
    </div>

    <div ref="tableHead" class="line header">
      <div class="item-header" v-if="colStartFrom === '0'">周日</div>
      <div class="item-header">周一</div>
      <div class="item-header">周二</div>
      <div class="item-header">周三</div>
      <div class="item-header">周四</div>
      <div class="item-header">周五</div>
      <div class="item-header">周六</div>
      <div class="item-header" v-if="colStartFrom === '1'">周日</div>
    </div>
    <div :class="['line', { 'last-line': i === 5 }]" v-for="(dateRow, i) in dateGroupList" :key="i">
      <div
        :class="['item', { active: date.isToday, clicked: selectedCell === date.value,'greeyColor':(j===5||j===6) }]"
        v-for="(date, j) in dateRow"
        :key="date.value"
        @click="handleClickCell(date.value)"
      >
        <span :class="['day', { 'not-this-month': !date.isSameMonth }]">{{ date.label }}</span>
        <slot :date="date.value"></slot>
      </div>
    </div>
  </article>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Refresh, ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue'
import { previousDay, format, addDays, isMonday, isSunday, isToday, isSameMonth, addMonths, setDate } from 'date-fns'

// clickCell 点击单元格事件;sendStartEnd 告知父组件当前的起止日期;
const emit = defineEmits(['clickCell', 'sendStartEnd'])
const tableHead = ref()

/**
 * 获取当前月
 * 获取当前月1日
 * 获取当前月1日所在行的周日(或周一),作为第一个格子
 * 然后加够6行共计42天
 */
const colStartFrom = ref('0')
const activeStartDate = ref(new Date())
const firstDayOfMonthNow = computed(() => setDate(activeStartDate.value, 1))
// 计算日期格子中需要的数据
const formatDate = (date) => {
  return {
    value: format(date, 'yyyy-MM-dd'),
    label: format(date, 'MM月dd日'),
    isToday: isToday(date),
    isSameMonth: isSameMonth(date, firstDayOfMonthNow.value),
  }
}
// 月份选择器触发回调
const handleMonthChange = (e) => {
  activeStartDate.value = e
  sendStartEnd()
}

const selectedCell = ref('')
// 点击单元格触发回调
const handleClickCell = (date) => {
  selectedCell.value = date
  emit('clickCell', date)
}
const dateGroupList = computed(() => {
  // 获得第一个格子
  let firstCell
  let judgeFn = colStartFrom.value === 0 ? isSunday : isMonday

  if (judgeFn(firstDayOfMonthNow.value)) {
    firstCell = firstDayOfMonthNow.value
  } else {
    // 如果当天是周日的话,调用这个方法会再往前倒一周,所以要进入上面的if
    firstCell = previousDay(firstDayOfMonthNow.value, +colStartFrom.value)
  }

  let temp = [[formatDate(firstCell)], [], [], [], [], []]
  let j = 0
  // 从1开始计数,因为第一个格子已经存入
  for (let i = 1; i < 42; i++) {
    // 每够7天,就开始二维数组的下一行
    if (i % 7 === 0) {
      j++
    }

    let date = formatDate(addDays(firstCell, i))

    temp[j].push(date)
  }

  return temp
})

const changeMonth = (num) => {

  if (num === 0) {
    activeStartDate.value = new Date()
  } else {
    activeStartDate.value = addMonths(activeStartDate.value, num)
  }

  sendStartEnd()
}

const sendStartEnd = () => {
  let start = dateGroupList.value[0][0].value
  let end = dateGroupList.value[5][6].value
  emit('sendStartEnd', { start, end })
}
onMounted(sendStartEnd)
</script>
<style lang="scss" scoped>
.felxbox {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.f11 {
  color: #f11;
}
.top {
  margin-bottom: 15px;

  .title {
    margin-right: 10px;
  }
}

.line {
  display: flex;
  align-items: center;
  justify-content: space-around;

  .item {
    position: relative;
    flex: 1;
    // width: 100px;
    height: 100px;
    border: 1px solid #ddd;
    border-right-color: transparent;
    border-bottom-color: transparent;
    cursor: pointer;

    &:last-child {
      border-right: 1px solid #ddd;
    }

    &:hover {
      box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.12);
      border-radius: 4px;
    }

    .day {
      position: absolute;
      right: 5px;
      top: 5px;
    }
    .not-this-month {
      opacity: 0.3;
    }
  }

  .active {
    background-color:rgb(214 234 255);
  }
  .clicked {
    // background-color: #79bbff;
    // border: 1px solid #000000;
    // color: #ffffff;
    // box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.12);
    // border-radius: 4px;
  }
}
.last-line .item {
  border-bottom: 1px solid #ddd;
}
.header .item-header {
  position: relative;
  flex: 1;
  height: 60px;
  line-height: 60px;
  border: 1px solid #ddd;
  border-right-color: transparent;
  border-bottom-color: transparent;

  text-align: center;
  font-weight: bolder;
  background-color: #409eff;
  color: #ffffff;
}
.month :deep(.el-input__inner) {
  font-size: 16px;
  // font-weight: bolder;
}
.greeyColor{
  background-color: #f8f8f8;
}
</style>

如何引用组件

直接把想要显示的内容放入slot中,我这里因为需求的原因,使用了 element-plus 的 el-popover,里面还注册了一些方法,不过这个不重要,只是个示范,对于你来说,主要关注的是要使用作用域中的 date 的值。

xml 复制代码
<script setup>
import Calendar from './component/Calendar.vue'
</script>

<template>
<Calendar v-loading="loading" v-slot="{ date }" @clickCell="handleClickCell" @sendStartEnd="handleStartEndChange">
  <el-popover :fallback-placements="['bottom', 'top', 'right', 'left']" :width="440" v-if="dataMap[date]">
    <template #reference>
      <div class="cont blackColor">
        {{date}}
      </div>
      
    </template>
    <el-table :data="dataMap[date].tabelData" stripe>
      <el-table-column width="120" property="key" label="类型" />
      <el-table-column property="" label="xxx" />
    </el-table>
  </el-popover>
</Calendar>
</template>
相关推荐
集成显卡2 分钟前
axios平替!用浏览器自带的fetch处理AJAX(兼容表单/JSON/文件上传)
前端·ajax·json
焚琴煮鹤的熊熊野火11 分钟前
前端垂直居中的多种实现方式及应用分析
前端
我是苏苏31 分钟前
C# Main函数中调用异步方法
前端·javascript·c#
转角羊儿42 分钟前
uni-app文章列表制作⑧
前端·javascript·uni-app
大G哥1 小时前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python
hong_zc1 小时前
初始 html
前端·html
小小吱1 小时前
HTML动画
前端·html
糊涂涂是个小盆友2 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
Py小趴2 小时前
Python自学之Colormaps指南
开发语言·python·数据可视化
浮华似水2 小时前
Javascirpt时区——脱坑指南
前端