思路/难点
一.排版
- 需满足6 * 7 的排版,否则可能选择的某月无法显示完全
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NadMNZqy-1657784151904)(/Users/wangyongjie/Library/Application Support/typora-user-images/image-20220714104458559.png)]
二.获取传入时间的年份以及月份
- 将传入时间通过格式化转为
****年 **月
三.确定日历的第一个展示的是哪天
// date 为选中月的1号日期
date.setDate(date.getDate() - date.getDay() + 第一位排的是星期几)
如 7月14日 该选中月份1号为 7月1日。则 date.setDate(1-5+0)
即 date.setDate(-4) => 6月26日
如果第一位排的不是周日,而是周六。 date.setDate(1-5+6)
即 date.setDate(2) 此时设置日期比初始1号日期还要大,那么就需要往前推一周
即 date.setDate(2-7) => 6月25日
四.切换月份设定方式
不可以通过当前选中日期加减一个月!
如:当前选中日期 为 7月31日,此时减去一个月,直接变成了6月31日这个非法的值,2月的日期还不固定。
因此左右切换的时候,传入的永远为当前月的第一天。
如:传入7月1日,减去一个月 为6月1日,不会产生问题
五.观察者模式来处理数据传递
- 发布者 【头部切换年月】
- 触发notify,向订阅者发出通知
- 订阅者 【主题部分日期变化】
- 观察者注册,便于之后收到通知后执行更新方法
----全部代码----
Utils.js
import { Subject } from './subject'
let transfer = function (date, fmt) {
const _date = new Date(date)
let o = {
'M+': _date.getMonth() + 1, // 月份
'd+': _date.getDate(), // 日
'h+': _date.getHours(), // 小时
'm+': _date.getMinutes(), // 分
's+': _date.getSeconds(), // 秒
'q+': Math.floor((_date.getMonth() + 3) / 3), // 季度
S: _date.getMilliseconds(), // 毫秒
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (_date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (let 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
}
/**
* 用于format日期格式
* @param {*} timeSpan
* @param {*} fmt
* @param {*} formatDateNullValue
*/
export const dateFormat = function (timeSpan, fmt, formatDateNullValue) {
if (!timeSpan) {
if (formatDateNullValue) {
return formatDateNullValue
}
return '无'
}
let date = new Date(timeSpan)
return transfer(date, fmt)
}
/**
* 获取日历header内容 格式为:****年 **月
* @param {*} date
*/
export const getHeaderContent = function (date) {
let _date = new Date(date)
return dateFormat(_date, 'yyyy年 MM月')
}
/**
* 获取当前月的第一天
* @param {*} date
*/
export const getFirstDayOfMonth = function (date) {
let _date = new Date(date)
_date.setDate(1)
return _date
}
/**
* 获取当前月日历的第一天
* @param {*} date
*/
export const getFirstDayOfCalendar = function (date, weekLabelIndex) {
let _date = new Date(date)
_date = new Date(_date.setDate(_date.getDate() - _date.getDay() + weekLabelIndex))
// 如果当前日期大于当前月第一天,则需要减去7天
if (_date > date) {
_date = new Date(_date.setDate(_date.getDate() - 7))
}
return _date
}
/**
* 根据传入index确认weeklabel的顺序
* @param {*} weekIndexOfFirstWeekDay
*/
export const getWeekLabelList = function (weekIndexOfFirstWeekDay) {
let weekLabelArray = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
for (let index = 0; index < weekIndexOfFirstWeekDay; index++) {
let weekLabel = weekLabelArray.shift() || ''
weekLabelArray.push(weekLabel)
}
return weekLabelArray
}
/**
* 启动观察者模式,并且初始化
*/
export const initObserver = function () {
let subject = new Subject()
return subject
}
/**
* 格式化日期为两个单词,例如:‘1’号 格式为 ‘01’
* @param {*} dateNumber
*/
export const formatDayWithTwoWords = function (dateNumber) {
if (dateNumber < 10) {
return '0' + dateNumber
}
return dateNumber
}
/**
* 比较当前日期是否为本月日期,用于进行本月数据高亮显示
* @param {*} firstDayOfMonth
* @param {*} date
*/
export const isCurrentMonth = function (firstDayOfMonth, date) {
return firstDayOfMonth.getMonth() === date.getMonth()
}
/**
* 比较当前日期是否为系统当前日期
* @param {*} date
*/
export const isCurrentDay = function (date) {
let _date = new Date()
return (
date.getFullYear() === _date.getFullYear() &&
date.getMonth() === _date.getMonth() &&
date.getDate() === _date.getDate()
)
}
/**
*
* @param {*} firstDayOfCurrentMonth
* @returns
*/
export const isMoreCurrentDay = function (date) {
let _date = new Date().getTime()
return new Date(date).getTime() > _date
}
/**
* 以传入参数作为基准获取下个月的第一天日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfNextMonth = function (firstDayOfCurrentMonth) {
return new Date(
firstDayOfCurrentMonth.getFullYear(),
firstDayOfCurrentMonth.getMonth() + 1,
1
)
}
/**
* 以传入参数作为基准获取上个月的第一天日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfPrevMonth = function (firstDayOfCurrentMonth) {
return new Date(
firstDayOfCurrentMonth.getFullYear(),
firstDayOfCurrentMonth.getMonth() - 1,
1
)
}
/**
* 以传入参数作为基准获取当前月的日期
* @param {*} firstDayOfCurrentMonth
*/
export const getFirstDayOfCurrntMonth = function (firstDayOfCurrentMonth) {
return new Date(firstDayOfCurrentMonth.getFullYear(), firstDayOfCurrentMonth.getMonth(), 1)
}
subject.js
/*
* Subject
* 内部创建了三个方法,内部维护了一个ObserverList。
*/
export class Subject {
constructor() {
this._observers = new ObserverList()
}
// addObserver: 调用内部维护的ObserverList的add方法
addObserver(observer) {
this._observers.add(observer)
}
// removeObserver: 调用内部维护的ObserverList的removeAt方法
removeObserver(observer) {
this._observers.removeAt(this._observers.indexOf(observer, 0))
}
// notify: 通知函数,用于通知观察者并且执行update函数,update是一个实现接口的方法,是一个通知的触发方法。
notify(context) {
let observerCount = this._observers.count()
for (let i = 0; i < observerCount; i++) {
this._observers.get(i).update(context)
}
}
}
/*
* ObserverList
* 内部维护了一个数组,4个方法用于数组的操作,这里相关的内容还是属于subject,因为ObserverList的存在是为了将subject和内部维护的observers分离开来,清晰明了的作用。
*/
class ObserverList {
constructor() {
this._observerList = []
}
add(obj) {
return this._observerList.push(obj)
}
count() {
return this._observerList.length
}
get(index) {
if (index > -1 && index < this._observerList.length) {
return this._observerList[index]
}
throw new Error(`_observerList ${index} 未知为null`)
}
indexOf(obj, startIndex) {
let i = startIndex
while (i < this._observerList.length) {
if (this._observerList[i] === obj) {
return i
}
i++
}
return -1
}
removeAt(index) {
this._observerList.splice(index, 1)
}
}
export class Observer {
update() {}
}
index.vue
<template>
<div class="gongshi-zj">
<Calendar @change="onChange" @dayChange="onDayChange" />
</div>
</template>
<script>
import Calendar from '@/components/calendar/Calendar.vue'
export default {
name: 'gongShiAndZJ',
methods: {
onChange(weekList) {
const startDate = weekList[0][0].date
const endDate =
weekList[weekList.length - 1][weekList[weekList.length - 1].length - 1].date
console.log('开始时间', startDate)
console.log('结束时间', endDate)
},
onDayChange(dayItem) {
console.log(dayItem)
},
},
components: {
Calendar,
},
}
</script>
<style lang="scss" scoped>
.gongshi-zj {
overflow: auto;
}
</style>
Calendar.vue
<template>
<div class="calendar" :style="{ height: height + 'px', width: width + 'px' }">
<CalendarHeader :observer="calendarObserver" />
<CalendarBody
:observer="calendarObserver"
:weekLabelIndex="weekLabelIndex"
@change="onChange"
@dayChange="onDayChange"
/>
<slot>
<CalendarFooter />
</slot>
</div>
</template>
<script>
import CalendarHeader from './components/CalendarHeader.vue'
import CalendarBody from './components/CalendarBody.vue'
import CalendarFooter from './components/CalendarFooter.vue'
import { initObserver } from '@/lib/calendar/utils'
export default {
name: 'Calendar',
components: {
CalendarHeader,
CalendarBody,
CalendarFooter,
},
props: {
height: {
type: Number,
default: 700,
},
width: {
type: Number,
default: 500,
},
// 从周几开始排列 0为周日
weekLabelIndex: {
type: Number,
default: 0,
},
},
data() {
return {
calendarObserver: initObserver(),
}
},
methods: {
onChange(weekList) {
this.$emit('change', weekList)
},
onDayChange(dayItem) {
this.$emit('dayChange', dayItem)
},
},
}
</script>
<style lang="scss" scoped>
.calendar {
padding: 10px;
}
</style>
CalendarHeader.vue
<template>
<div class="calendar-header">
<div class="time">
<md-button
class="margin-right-4"
size="small"
type="primary"
plain
icon="md-icon-shuangzuojiantou"
@click="onGoPrevYear"
></md-button>
<md-button
size="small"
type="primary"
plain
icon="md-icon-zuojiantou-k"
@click="onGoPrev"
></md-button>
<h2 class="margin-left-8 margin-right-8">{{ headerContent }}</h2>
<md-button
size="small"
type="primary"
plain
icon="md-icon-youjiantou-k"
@click="onGoNext"
></md-button>
<md-button
class="margin-left-4"
size="small"
type="primary"
plain
icon="md-icon-shuangyoujiantou"
@click="onGoNextYear"
></md-button>
</div>
<md-button type="primary" noneBg size="small" class="now" @click="onBackToday"
>回到今天</md-button
>
</div>
</template>
<script>
import {
getHeaderContent,
getFirstDayOfNextMonth,
getFirstDayOfPrevMonth,
getFirstDayOfCurrntMonth,
} from '@/lib/calendar/utils.js'
export default {
name: 'CalendarHeader',
props: {
observer: {
type: Object,
required: true,
},
},
data() {
return {
headerContent: '',
firstDayOfMonth: new Date(),
}
},
methods: {
// 返回上月
onGoPrev() {
// 上月第一天
const preFirstDayOfMonth = getFirstDayOfPrevMonth(this.firstDayOfMonth)
this.setFirstDayOfMonth(preFirstDayOfMonth)
this.observerNotify(preFirstDayOfMonth)
},
// 跳转下个月
onGoNext() {
// 下月第一天
const nextFirstDayOfMonth = getFirstDayOfNextMonth(this.firstDayOfMonth)
this.setFirstDayOfMonth(nextFirstDayOfMonth)
this.observerNotify(nextFirstDayOfMonth)
},
onGoPrevYear() {
const prevDate = new Date(
this.firstDayOfMonth.setFullYear(this.firstDayOfMonth.getFullYear() - 1)
)
const preFirstDayOfMonth = getFirstDayOfCurrntMonth(prevDate)
this.setFirstDayOfMonth(preFirstDayOfMonth)
this.observerNotify(preFirstDayOfMonth)
},
onGoNextYear() {
const nextDate = new Date(
this.firstDayOfMonth.setFullYear(this.firstDayOfMonth.getFullYear() + 1)
)
const nextFirstDayOfMonth = getFirstDayOfCurrntMonth(nextDate)
this.setFirstDayOfMonth(nextFirstDayOfMonth)
this.observerNotify(nextFirstDayOfMonth)
},
// 返回今天
onBackToday() {
const currentFirstDayOfMonth = getFirstDayOfCurrntMonth(new Date())
this.setFirstDayOfMonth(currentFirstDayOfMonth)
this.observerNotify(currentFirstDayOfMonth)
},
// 设置头部日期
setHeaderContent(val) {
this.headerContent = val
},
setFirstDayOfMonth(val) {
this.firstDayOfMonth = val
},
// 主题发布信息,通知观察者
observerNotify(currentFirstDayOfMonth) {
this.setHeaderContent(getHeaderContent(currentFirstDayOfMonth))
this.observer.notify(currentFirstDayOfMonth)
},
},
created() {
this.setHeaderContent(getHeaderContent(new Date()))
this.setFirstDayOfMonth(new Date())
},
}
</script>
<style lang="scss" scoped>
.calendar-header {
display: flex;
align-items: center;
position: relative;
border-bottom: 1px solid;
border-color: #f5f3f2;
height: 76px;
& > .time {
display: flex;
margin: 0 auto;
}
& > .now {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
}
</style>
CalendarBody.vue
<template>
<div class="calendar-body">
<ul class="week">
<li class="body-block" v-for="week in weekLabelArray" :key="week">
<span>{{ week }}</span>
</li>
</ul>
<ul class="month-day" v-for="(weekItem, index) in weekList" :key="index">
<li
class="day-body"
v-for="(dayItem, indey) in weekItem"
:key="indey"
@click="onClickDay(dayItem)"
>
<div
class="day-block"
:class="{
active: dayItem.isCurrentDay,
disabled: dayItem.isMoreCurrentDay,
}"
>
<span>{{ dayItem.monthDay }}</span>
</div>
</li>
</ul>
</div>
</template>
<script>
import { cloneDeep } from 'lodash'
import {
getFirstDayOfMonth,
getFirstDayOfCalendar,
formatDayWithTwoWords,
isCurrentMonth,
isCurrentDay,
isMoreCurrentDay,
getWeekLabelList,
} from '@/lib/calendar/utils'
export default {
name: 'CalendarBody',
props: {
observer: {
type: Object,
required: true,
},
weekLabelIndex: {
type: Number,
default: 0,
},
},
data() {
return {
firstDayOfMonth: new Date(),
weekList: [],
weekLabelArray: [],
// 当前锁定的日期
currentDay: {},
}
},
methods: {
setFirstDayOfMonth(val) {
this.firstDayOfMonth = val
},
// 设置日列表
setWeekList(val) {
this.weekList = val
this.$emit('change', val)
},
// 设置周列表
setWeekLabelArray(val) {
this.weekLabelArray = val
},
setWeekListValue(firstDayOfmonth) {
let newWeekList = []
let dayOfCalendar = getFirstDayOfCalendar(firstDayOfmonth, this.weekLabelIndex)
// 遍历层数为6,因为日历显示当前月数据为6行
for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
let weekItem = []
// 每一周为7天
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
let dayItem = {
date: cloneDeep(dayOfCalendar),
monthDay: formatDayWithTwoWords(dayOfCalendar.getDate()),
isCurrentMonth: isCurrentMonth(this.firstDayOfMonth, dayOfCalendar),
isCurrentDay: isCurrentDay(dayOfCalendar),
isMoreCurrentDay: isMoreCurrentDay(dayOfCalendar),
}
// 抛出当前锁定的日期,方便后期进行点击修改
if (dayItem.isCurrentDay) {
this.currentDay = dayItem
}
weekItem.push(dayItem)
// 当前日期加1,以此类推得到42条记录
dayOfCalendar.setDate(dayOfCalendar.getDate() + 1)
}
newWeekList.push(weekItem)
}
this.setWeekList(newWeekList)
},
update(content) {
this.setFirstDayOfMonth(content)
this.setWeekListValue(content)
},
// 业务方法
onClickDay(dayItem) {
// 去除原有所定的值,绑定新的锁定值
this.currentDay.isCurrentDay = false
dayItem.isCurrentDay = true
this.currentDay = dayItem
this.$emit('dayChange', dayItem)
},
},
created() {
// 注册观察者对象
this.observer.addObserver({
update: this.update,
})
// 设置当前月的第一天,用来数据初始话以及进行日期是否为当前月判断
this.setFirstDayOfMonth(getFirstDayOfMonth(new Date()))
// 设置每周label标识数据
this.setWeekLabelArray(getWeekLabelList(this.weekLabelIndex))
// 初始设置当前月日历数据
this.setWeekListValue(getFirstDayOfMonth(new Date()))
},
}
</script>
<style lang="scss" scoped>
@import '@mediinfo-ued/base/_index.scss';
.calendar-body {
padding-top: 24px;
.body-block {
width: 66px;
height: 66px;
// width: 50px;
// height: 50px;
padding: 16px;
}
& > .week {
display: flex;
justify-content: space-evenly;
color: #222222;
font-size: 16px;
& > li:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
color: #aaaaaa;
background-color: #e2e7ef;
}
& > li:last-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
color: #aaaaaa;
background-color: #e2e7ef;
}
}
.month-day {
display: flex;
justify-content: space-evenly;
align-items: center;
text-align: center;
& > li:first-child,
& > li:last-child {
color: #aaaaaa;
background-color: #e2e7ef;
&:hover {
border-radius: 0;
background: #e2e7ef;
}
}
.day-body {
padding: 9px;
}
.day-block {
padding-top: 4px;
height: 48px;
width: 48px;
}
.day-block:hover {
cursor: pointer;
}
.day-block.active {
border-radius: 50%;
@include md-def('background', 'color-2');
@include md-def('color', 'color-6');
cursor: pointer;
}
.day-block.disabled {
color: #aaaaaa;
}
}
& .month-day:last-child {
& > li:first-child,
& > li:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
</style>