背景
我们有时需要以日历的格式展示每日的统计数据,比如下图所示。但是上网上寻找的插件库中,看起来最像能满足需求的 fullcalendar.io/demos 也远远不能满足需求,非常僵化,而且文档非常反人类。
所以老样子,我们自己实现。
最终效果如下:
实现
其实关键部分就2个:
-
实现日期的准确计算
-
将日期和每天的数据通过单元格的div结合起来
至于事件啦、提示栏啦之类的,因为每一个单元格都是div而已,我们就可以想干什么干什么了。
废话不多说,直接上代码。
Calendar.vue 组件
依赖
-
使用了 element-plus 进行月份选择以及样式优化
-
使用了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>