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

背景

我们有时需要以日历的格式展示每日的统计数据,比如下图所示。但是上网上寻找的插件库中,看起来最像能满足需求的 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>
相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom9 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom9 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试