小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
TIP 👉 非淡泊无以明志,非宁静无以致远。诸葛亮《诫子书》
前言
在我们日常项目开发中,我们在做业务开发的时候会涉及日期选择的功能,所以封装了这款移动端的日期选择组件。日期选择器
属性
1. defaultDate
- 当前日期值
- 值为Date对象或者Date数组
- type 为 "single" 时为Date对象
- type 为 "range" 时为Date数组
2. type
- 日历类型
- 值为字符串
- single:选择单个日期
- range:选择日期区间
3. minDate
- 可选日期的最小值
- 值为Date对象
4. maxDate
- 可选日期的最大值
- 值为Date对象
5. startDateText
- 开始日期的文字信息(type 为 "range" 时有效)
- 值为字符串
6. endDateText
- 结束日期的文字信息(type 为 "range" 时有效)
- 值为字符串
7. allowSameDay
- 是否允许开始日期和结束日期是同一天(type 为 "range" 时有效)
- 值为布尔类型,默认为:false
8. disabledDate
- 判断不可用日期的方法
- 值为Function类型
- 参数:val {Date} Date类型日期值
- 返回:isDisabled {Boolean} 是否为不可用日期
事件
1. select
- 日期被选中事件
- 参数
- date {Date} 被选中的日期
2. confirm
- 日期范围选中事件(type 为 "range" 时,才会触发)
- 参数
- date {Array} 时间范围开始日期和结束日期数组,数组长度为2
示例
1. 选择单个日期
<template>
<div class="calendar-demo">
<ul class="form-list">
<li class="form-item" @click="popupBox1">
<div class="item-content">
<label>单选日期:</label>
<span>{{value1}}</span>
</div>
</li>
</ul>
</div>
</template>
<script>
import popupFullBox from '@/components/m/fullBox'
import Calendar from '@/components/m/calendar'
import dateUtil from '@/assets/js/date'
export default {
name: 'CalendarDemo',
data () {
return {
value1: null
}
},
methods: {
popupBox1 () {
let minDate = new Date()
minDate.setMonth(minDate.getMonth() - 11)
let maxDate = new Date()
maxDate.setMonth(maxDate.getMonth() + 11)
let defaultDate = this.value1 ? new Date(this.value1) : null
let fullBox = popupFullBox({
title: '日期选择',
scroll: false,
content: Calendar,
contentProps: {
defaultDate,
minDate,
maxDate,
disabledDate: (date) => {
if (date.getDay() === 0 || date.getDay() === 6) {
return true
}
}
},
contentEvents: {
select: (v) => {
if (v && v instanceof Date) {
this.value1 = dateUtil.format(v, 'yyyy-MM-dd')
// 300毫秒后关闭窗口
fullBox.close(300)
}
}
}
})
}
}
}
</script>
2. 选择日期区间
<template>
<div class="calendar-demo">
<ul class="form-list">
<li class="form-item" @click="popupBox2">
<div class="item-content">
<label>选择日期区间:</label>
<span v-if="value2 && value2.length === 2">{{value2[0] + ' 至 ' + value2[1]}}</span>
</div>
</li>
</ul>
</div>
</template>
<script>
import popupFullBox from '@/components/m/fullBox'
import Calendar from '@/components/m/calendar'
import dateUtil from '@/assets/js/date'
export default {
name: 'CalendarDemo',
data () {
return {
value1: null,
value2: null
}
},
methods: {
popupBox2 () {
let defaultDate = this.value2 && this.value2.length === 2 ? [new Date(this.value2[0]), new Date(this.value2[1])] : null
let fullBox = popupFullBox({
title: '日期选择',
scroll: false,
content: Calendar,
contentProps: {
type: 'range',
defaultDate,
startDateText: '出发',
endDateText: '返程',
allowSameDay: true
},
contentEvents: {
confirm: (v) => {
if (v && v instanceof Array && v.length >= 2) {
this.value2 = [dateUtil.format(v[0], 'yyyy-MM-dd'), dateUtil.format(v[1], 'yyyy-MM-dd')]
// 300毫秒后关闭窗口
fullBox.close(300)
}
}
}
})
}
}
}
</script>
实现Calendar.vue
<template>
<div class="calendar">
<div class="calendar-header">
<div class="header-cell">日</div>
<div class="header-cell">一</div>
<div class="header-cell">二</div>
<div class="header-cell">三</div>
<div class="header-cell">四</div>
<div class="header-cell">五</div>
<div class="header-cell">六</div>
</div>
<div class="calendar-content" ref="calendarContent">
<Scroll ref="scroll" :bounce="true"
:pullDownRefresh="{ threshold: 40, stop: 20 }" :pullUpLoad="{ threshold: -60 }"
@onPullUp="pullUpHandle" @onPullDown="pullDownHandle">
<div class="months">
<Month v-for="month in monthList" :key="month.getTime()" :ref="'month' + formatDate(month, 'yyyy-MM')"
:monthDate="month"
:value="curValue"
:type="type"
:minDate="minDate"
:maxDate="maxDate"
:startDateText="startDateText"
:endDateText="endDateText"
:disabledDate="disabledDate"
@select="handleSelect"
></Month>
</div>
</Scroll>
</div>
</div>
</template>
<script>
import Scroll from '@/components/base/scroll'
import Month from './components/Month.vue'
import { formatDate, getFirstDayOfMonth, curMonthStartDate, getMonthList } from './js/dateUtil.js'
export default {
name: 'Calendar',
components: {
Scroll,
Month
},
props: {
// 日期值
defaultDate: {
type: [Date, Array]
},
// 日历类型(single:选择单个日期,range:选择日期区间)
type: {
type: String,
default: 'single'
},
// 可选范围最早日期
minDate: {
type: Date
// default: () => getTodayDate()
},
// 可选范围最晚日期
maxDate: {
type: Date
/* default: () => {
let date = getTodayDate()
date.setMonth(date.getMonth() + 6)
return date
} */
},
// type = 'range' 时(即选择日期区间时)开始日期的文字信息
startDateText: {
type: String,
default: '开始'
},
// type = 'range' 时(即选择日期区间时)结束日期的文字信息
endDateText: {
type: String,
default: '结束'
},
// type = 'range' 时(即选择日期区间时)是否允许开始日期和结束日期是同一天
allowSameDay: {
type: Boolean,
default: false
},
/**
* 不可用日期方法
* @param val {Date} Date类型日期值
* @return isDisabled {Boolean | Undefined} 是否为不可用日期(如果返回undefined,按默认逻辑处理)
*/
disabledDate: Function
},
data () {
return {
// 当前选中日期值
curValue: this.defaultDate,
// 展示月份Date数组
monthList: getMonthList(this.defaultDate, this.minDate, this.maxDate),
// 是否正在上拉加载
isPullUp: false,
// 是否正在下拉加载
isPullDown: false,
}
},
mounted () {
// 滚动条滚动到当前选中月份
this.scrollToCurrentMonth()
},
methods: {
// 处理日期选择事件
handleSelect (cell) {
if (this.type === 'single') { // 如果是选择单个日期
this.curValue = cell.date
this.$emit('select', this.curValue)
} else { // 如果是选择日期区间
if (
!this.curValue || // 无选中日期
// 不是只有开始日期的情况(未选中日期或者开始和结束日期都有)
(this.curValue instanceof Array && this.curValue.length !== 1) ||
// 只有开始日期,并且当前选中日期早于之前选的开始日期
(this.curValue instanceof Array && this.curValue.length === 1 && cell.time < this.curValue[0].getTime())
) { // 认为选中日期为开始日期
this.curValue = [cell.date]
this.$emit('select', cell.date)
} else if (
// 只有开始日期
this.curValue instanceof Array && this.curValue.length === 1 &&
// 当前选中日期晚于之前选的开始日期 或者 允许开始日期和结束日期是同一天的情况下,当前选中日期等于之前选的开始日期
(cell.time > this.curValue[0].getTime() || (this.allowSameDay && cell.time === this.curValue[0].getTime()))
) { // 认为选中日期为结束日期
this.curValue.push(cell.date)
this.$emit('select', cell.date)
this.$emit('confirm', this.curValue)
}
}
},
// 格式化日期
formatDate,
// 滚动条滚动到当前选中月份
scrollToCurrentMonth () {
// 第一个月
let firstMonth = null
if (this.monthList && this.monthList.length > 0) {
firstMonth = this.monthList[0]
}
// 当前月(选中日期的月份或者选中日期范围开始的月份,如果没有值则为当前月份)
let curMonth = null
if (this.curValue instanceof Date) {
curMonth = getFirstDayOfMonth(this.curValue)
} else if (this.curValue instanceof Array && this.curValue.length > 0 && this.curValue[0] instanceof Date) {
curMonth = this.curValue[0]
} else {
curMonth = curMonthStartDate()
}
if (firstMonth && curMonth) {
setTimeout(() => {
// 第一个月的y坐标位置
let firstTop = null
// 当前月的y坐标位置
let curTop = null
let firstRef = this.$refs['month' + formatDate(firstMonth, 'yyyy-MM')]
if (firstRef && firstRef.length > 0) {
let firstEl = firstRef[0].$el
firstTop = firstEl.getBoundingClientRect().top
}
let curRef = this.$refs['month' + formatDate(curMonth, 'yyyy-MM')]
if (curRef && curRef.length > 0) {
let curEl = curRef[0].$el
curTop = curEl.getBoundingClientRect().top
}
if (firstTop !== null && curTop !== null && curTop > firstTop) {
this.$refs.scroll.scroll.scrollTo(0, firstTop - curTop, 0)
}
}, 100)
}
},
// 上拉加载
pullUpHandle () {
if (this.isPullUp) {
return
}
this.isPullUp = true
if (this.monthList && this.monthList.length > 0) {
let lastMonth = this.monthList[this.monthList.length - 1]
let newLastMonth
for (let i = 0; i < 6; i++) {
let newMonth = new Date(lastMonth.getTime())
newMonth.setMonth(newMonth.getMonth() + (i + 1))
if (this.maxDate && newMonth.getTime() > getFirstDayOfMonth(this.maxDate).getTime()) {
break
} else {
newLastMonth = newMonth
this.monthList.push(newMonth)
}
}
if (newLastMonth) {
this.$nextTick(() => {
this.$refs.scroll.scroll.refresh()
})
}
}
this.$refs.scroll.finish('PullUp')
this.isPullUp = false
},
// 下拉加载
pullDownHandle () {
if (this.isPullDown) {
return
}
this.isPullDown = true
if (this.monthList && this.monthList.length > 0) {
let firstMonth = this.monthList[0]
if (this.minDate && firstMonth.getTime() < this.minDate.getTime()) {
this.$refs.scroll.finish('PullDown')
this.isPullDown = false
return
}
let newFirstMonth = null
for (let i = 0; i < 6; i++) {
let newMonth = new Date(firstMonth.getTime())
newMonth.setMonth(newMonth.getMonth() - (i + 1))
if (this.minDate && newMonth.getTime() < getFirstDayOfMonth(this.minDate).getTime()) {
break
} else {
newFirstMonth = newMonth
this.monthList.unshift(newMonth)
}
}
if (newFirstMonth) {
this.$nextTick(() => {
let newRef = this.$refs['month' + formatDate(newFirstMonth, 'yyyy-MM')]
let oldRef = this.$refs['month' + formatDate(firstMonth, 'yyyy-MM')]
if (newRef && newRef.length > 0 && oldRef && oldRef.length > 0) {
let newEl = newRef[0].$el
let oldEl = oldRef[0].$el
let scrollY = newEl.getBoundingClientRect().top - oldEl.getBoundingClientRect().top
this.$refs.scroll.scroll.scrollTo(0, scrollY, 0)
setTimeout(() => {
this.$refs.scroll.scroll.scrollTo(0, scrollY + 260, 1000)
}, 0)
}
})
}
this.$refs.scroll.finish('PullDown')
this.isPullDown = false
}
}
}
}
</script>
<style lang="scss" scoped>
$header-height: 80px;
.calendar {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
font-size: 28px;
background: #FFF;
.calendar-header {
position: relative;
display: flex;
height: $header-height;
line-height: $header-height;
box-shadow: 0 2px 10px rgba(125, 126, 128, .16);
z-index: 2;
.header-cell {
flex: 1;
text-align: center;
font-size: 24px;
}
}
.calendar-content {
position: absolute;
top: $header-height;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
.months {
padding-top: 50px;
}
}
}
</style>
Mounth.vue
<template>
<div class="month-block">
<div class="month-title">{{year}}年{{month + 1}}月</div>
<div class="calendar-days" :class="'month-' + (month + 1)">
<div class="calendar-day" v-for="cell in cells" :key="cell.id">
<div class="day-cell" v-if="cell.type === 'prev-month'"></div>
<div class="day-cell" :class="cell.type" v-else @click="handleSelect(cell)">
<div v-if="cell.selected" class="active">{{cell.text}}</div>
<template v-else>{{cell.text}}</template>
<div v-if="cell.topInfo" class="top-info">{{cell.topInfo}}</div>
<div v-if="cell.bottomInfo" class="bottom-info">{{cell.bottomInfo}}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { curMonthStartDate, getDayCountOfMonth, getDateTimestamp, formatDate } from '../js/dateUtil.js'
export default {
name: 'Month',
props: {
monthDate: {
type: Date,
default: () => curMonthStartDate()
},
// 当前日期值
value: {
type: [Date, Array]
},
// 日历类型(single:选择单个日期,range:选择日期区间)
type: {
type: String,
default: 'single'
},
// 可选范围最早日期
minDate: {
type: Date
},
// 可选范围最晚日期
maxDate: {
type: Date
},
// type = 'range' 时(即选择日期区间时)开始日期的文字信息
startDateText: {
type: String,
default: '开始'
},
// type = 'range' 时(即选择日期区间时)结束日期的文字信息
endDateText: {
type: String,
default: '结束'
},
/**
* 不可用日期方法
* @param val {Date} Date类型日期值
* @return isDisabled {Boolean | Undefined} 是否为不可用日期(如果返回undefined,按默认逻辑处理)
*/
disabledDate: Function
},
data () {
return {
// 日期格式,默认为:yyyy-MM-dd
format: 'yyyy-MM-dd'
}
},
computed: {
// 年份
year () {
return this.monthDate.getFullYear()
},
// 月份,从0开始,即1月份为0
month () {
return this.monthDate.getMonth()
},
// 范围选择的开始日期
startDate () {
if (this.type === 'range' && this.value instanceof Array &&
this.value.length > 0 && this.value[0] instanceof Date) {
return new Date(getDateTimestamp(this.value[0]))
}
return null
},
// 范围选择的结束日期
endDate () {
if (this.type === 'range' && this.value instanceof Array &&
this.value.length > 1 && this.value[1] instanceof Date) {
return new Date(getDateTimestamp(this.value[1]))
}
return null
},
// 计算所有日期的数组
cells () {
const firstDay = this.monthDate // 当月第一天
// 当月1号是星期几(值为0至6,0表示周日)
let firstWeekDay = firstDay.getDay()
// 当前月的天数
const dateCountOfMonth = getDayCountOfMonth(firstDay.getFullYear(), firstDay.getMonth())
// 上个月的年份
const yearOfLastMonth = firstDay.getMonth() === 0 ? firstDay.getFullYear() - 1 : firstDay.getFullYear()
// 上个月的月份
const monthOfLastMonth = firstDay.getMonth() === 0 ? 11 : firstDay.getMonth() - 1
// 上个月的天数
const dateCountOfLastMonth = getDayCountOfMonth(yearOfLastMonth, monthOfLastMonth)
// 日历展示所有日期数组
let cells = []
// 计算上个月的日期
if (firstWeekDay !== 0) { // 当月的第一天不是星期日时,需要添加上个月的日期
let prevMonth = null
let prevYear = null
if (this.month === 0) { // 当前月份为1月时
prevMonth = 11 // 月份设置为12月
prevYear = this.year - 1 // 年份减1
} else { // 当前月份不是1月时
prevMonth = this.month - 1 // 月份减1
prevYear = this.year // 年份不变
}
for (let i = 1; i <= firstWeekDay; i++) {
let prevDay = dateCountOfLastMonth - (firstWeekDay - i)
cells.push(this.createCell(prevYear, prevMonth, prevDay, 'prev-month'))
}
}
// 计算本月的日期
for (let i = 1; i <= dateCountOfMonth; i++) {
cells.push(this.createCell(this.year, this.month, i))
}
return cells
}
},
methods: {
// 创建日期数据
createCell (year, month, day, type = '') {
const date = new Date(year, month, day)
const time = getDateTimestamp(date)
const value = formatDate(date, this.format)
const cell = {}
cell.id = type + value
cell.date = date // 当前日期Date对象
cell.time = time // 当前日期时间戳
cell.value = value // 当前日期字符串
cell.type = type // 日期类型:normal:普通;prev-month:上个月的日期,用于占位;
cell.text = day // 当前日期显示的文字
if (type !== 'prev-month') { // 不是上个月的日期时
// 是否选中(单选日期时使用)
cell.selected = this.value && this.value instanceof Date && value === formatDate(this.value)
// 是否禁用
cell.disabled = this.checkDisabled(date)
// 当前日期类型
cell.type = cell.disabled ? 'disabled' : this.getDateType(date)
cell.topInfo = cell.type === 'start-end' ? this.startDateText : ''
cell.bottomInfo = (cell.type === 'start-end' || cell.type === 'end') ? this.endDateText : (cell.type === 'start' ? this.startDateText : '')
}
return cell
},
// 获取日期类型
getDateType (date) {
if (this.startDate) {
// 是否开始日期
let isStart = this.startDate.getTime() === date.getTime()
// 是否结束日期
let isEnd = false
// 是否开始日期和结束日期之间的日期
let isMiddle = false
if (this.endDate) {
isEnd = this.endDate.getTime() === date.getTime()
if (!isStart && !isEnd) {
isMiddle = date.getTime() > this.startDate.getTime() && date.getTime() < this.endDate.getTime()
}
}
if (isStart && isEnd) {
return 'start-end'
} else if (isStart) {
return 'start'
} else if (isEnd) {
return 'end'
} else if (isMiddle) {
return 'middle'
} else {
return 'normal'
}
}
},
// 校验日期是否禁用
checkDisabled (date) {
let time = getDateTimestamp(date)
let isDisabled = false
if ((this.minDate && time < getDateTimestamp(this.minDate)) ||
(this.maxDate && time > getDateTimestamp(this.maxDate))) {
isDisabled = true
}
// 用户自定义是否禁用
let customerDisabled = this.disabledDate && this.disabledDate(date)
if (typeof customerDisabled === 'boolean') {
// 如果用户自定义 disabledDate 方法,返回 boolean 类型值则以此值为准
isDisabled = customerDisabled
}
return isDisabled
},
// 处理日期选中事件
handleSelect (cell) {
if (!cell.disabled) {
this.$emit('select', cell)
}
}
}
}
</script>
<style lang="scss" scoped>
$cell-height: 86px;
.month-block {
margin-bottom: 30px;
.month-title {
text-align: center;
margin-bottom: 20px;
font-weight: bold;
}
.calendar-days {
margin: 20px;
display: flex;
flex-wrap: wrap;
background-position: center;
background-repeat: no-repeat;
background-size: 300px 300px;
/*&.month-2 {
background-image: url(../img/2.svg);
}*/
@for $i from 1 through 12 {
&.month-#{$i} {
background-image: url(../img/#{$i}.svg);
}
}
.calendar-day {
width: 14.285%;
height: $cell-height;
.day-cell {
position: relative;
height: 100%;
line-height: $cell-height;
text-align: center;
&.disabled {
color: #c8c9cc;
}
&.middle {
color: $base-color;
@include base-background-color(.1);
}
&.start, &.end, &.start-end {
background: $base-color;
color: #FFF;
}
&.start-end {
border-radius: 4px;
}
&.start {
border-radius: 4px 0 0 4px;
}
&.end {
border-radius: 0 4px 4px 0;
}
.active {
width: 54px;
height: 54px;
line-height: 54px;
margin: ($cell-height - 54px) / 2 auto;
background: $base-color;
color: #FFF;
border-radius: 4px;
}
.top-info, .bottom-info {
position: absolute;
left: 0;
right: 0;
font-size: 20px;/*yes*/
line-height: 24px;
color: #FFF;
text-align: center;
}
.top-info {
top: 0;
}
.bottom-info {
bottom: 0;
}
}
}
}
}
</style>
「欢迎在评论区讨论」