|
|
<template> <view class="u-calendar-month-wrapper" ref="u-calendar-month-wrapper"> <view v-for="(item, index) in months" :key="index" :class="[`u-calendar-month-${index}`]" :ref="`u-calendar-month-${index}`" :id="`month-${index}`"> <text v-if="index !== 0" class="u-calendar-month__title">{{ item.year }}年{{ item.month }}月</text> <view class="u-calendar-month__days"> <view v-if="showMark" class="u-calendar-month__days__month-mark-wrapper"> <text class="u-calendar-month__days__month-mark-wrapper__text">{{ item.month }}</text> </view> <view class="u-calendar-month__days__day" v-for="(item1, index1) in item.date" :key="index1" :style="[dayStyle(index, index1, item1)]" @tap="clickHandler(index, index1, item1)" :class="[item1.selected && 'u-calendar-month__days__day__select--selected']"> <view class="u-calendar-month__days__day__select" :style="[daySelectStyle(index, index1, item1)]"> <text class="u-calendar-month__days__day__select__info" :class="[item1.disabled && 'u-calendar-month__days__day__select__info--disabled']" :style="[textStyle(item1)]">{{ item1.day }}</text> <text v-if="getBottomInfo(index, index1, item1)" class="u-calendar-month__days__day__select__buttom-info" :class="[item1.disabled && 'u-calendar-month__days__day__select__buttom-info--disabled']" :style="[textStyle(item1)]">{{ getBottomInfo(index, index1, item1) }}</text> <text v-if="item1.dot" class="u-calendar-month__days__day__select__dot"></text> </view> </view> </view> </view> </view> </template>
<script> // #ifdef APP-NVUE
// 由于nvue不支持百分比单位,需要查询宽度来计算每个日期的宽度
const dom = uni.requireNativePlugin('dom') // #endif
import { mpMixin } from '../../libs/mixin/mpMixin'; import { mixin } from '../../libs/mixin/mixin'; import { addUnit, deepClone, toast, sleep } from '../../libs/function/index'; import { colorGradient } from '../../libs/function/colorGradient'; import test from '../../libs/function/test'; import defProps from '../../libs/config/props'; import dayjs from 'dayjs/esm/index' export default { name: 'u-calendar-month', mixins: [mpMixin, mixin], props: { // 是否显示月份背景色
showMark: { type: Boolean, default: true }, // 主题色,对底部按钮和选中日期有效
color: { type: String, default: '#3c9cff' }, // 月份数据
months: { type: Array, default: () => [] }, // 日期选择类型
mode: { type: String, default: 'single' }, // 日期行高
rowHeight: { type: [String, Number], default: 58 }, // mode=multiple时,最多可选多少个日期
maxCount: { type: [String, Number], default: Infinity }, // mode=range时,第一个日期底部的提示文字
startText: { type: String, default: '开始' }, // mode=range时,最后一个日期底部的提示文字
endText: { type: String, default: '结束' }, // 默认选中的日期,mode为multiple或range是必须为数组格式
defaultDate: { type: [Array, String, Date], default: null }, // 最小的可选日期
minDate: { type: [String, Number], default: 0 }, // 最大可选日期
maxDate: { type: [String, Number], default: 0 }, // 如果没有设置maxDate,则往后推多少个月
maxMonth: { type: [String, Number], default: 2 }, // 是否为只读状态,只读状态下禁止选择日期
readonly: { type: Boolean, default: () => defProps.calendar.readonly }, // 日期区间最多可选天数,默认无限制,mode = range时有效
maxRange: { type: [Number, String], default: Infinity }, // 范围选择超过最多可选天数时的提示文案,mode = range时有效
rangePrompt: { type: String, default: '' }, // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效
showRangePrompt: { type: Boolean, default: true }, // 是否允许日期范围的起止时间为同一天,mode = range时有效
allowSameDay: { type: Boolean, default: false } }, data() { return { // 每个日期的宽度
width: 0, // 当前选中的日期item
item: {}, selected: [] } }, watch: { selectedChange: { immediate: true, handler(n) { this.setDefaultDate() } } }, computed: { // 多个条件的变化,会引起选中日期的变化,这里统一管理监听
selectedChange() { return [this.minDate, this.maxDate, this.defaultDate] }, dayStyle(index1, index2, item) { return (index1, index2, item) => { const style = {} let week = item.week // 不进行四舍五入的形式保留2位小数
const dayWidth = Number(parseFloat(this.width / 7).toFixed(3).slice(0, -1)) // 得出每个日期的宽度
// #ifdef APP-NVUE
style.width = addUnit(dayWidth) // #endif
style.height = addUnit(this.rowHeight) if (index2 === 0) { // 获取当前为星期几,如果为0,则为星期天,减一为每月第一天时,需要向左偏移的item个数
week = (week === 0 ? 7 : week) - 1 style.marginLeft = addUnit(week * dayWidth) } if (this.mode === 'range') { // 之所以需要这么写,是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
style.paddingLeft = 0 style.paddingRight = 0 style.paddingBottom = 0 style.paddingTop = 0 } return style } }, daySelectStyle() { return (index1, index2, item) => { let date = dayjs(item.date).format("YYYY-MM-DD"), style = {} // 判断date是否在selected数组中,因为月份可能会需要补0,所以使用dateSame判断,而不用数组的includes判断
if (this.selected.some(item => this.dateSame(item, date))) { style.backgroundColor = this.color } if (this.mode === 'single') { if (date === this.selected[0]) { // 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等
style.borderTopLeftRadius = '3px' style.borderBottomLeftRadius = '3px' style.borderTopRightRadius = '3px' style.borderBottomRightRadius = '3px' } } else if (this.mode === 'range') { if (this.selected.length >= 2) { const len = this.selected.length - 1 // 第一个日期设置左上角和左下角的圆角
if (this.dateSame(date, this.selected[0])) { style.borderTopLeftRadius = '3px' style.borderBottomLeftRadius = '3px' } // 最后一个日期设置右上角和右下角的圆角
if (this.dateSame(date, this.selected[len])) { style.borderTopRightRadius = '3px' style.borderBottomRightRadius = '3px' } // 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this .selected[len]))) { style.backgroundColor = colorGradient(this.color, '#ffffff', 100)[90] // 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符
style.opacity = 0.7 } } else if (this.selected.length === 1) { // 之所以需要这么写,是因为DCloud公司的iOS客户端的开发者能力有限导致的bug
// 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现
style.borderTopLeftRadius = '3px' style.borderBottomLeftRadius = '3px' } } else { if (this.selected.some(item => this.dateSame(item, date))) { style.borderTopLeftRadius = '3px' style.borderBottomLeftRadius = '3px' style.borderTopRightRadius = '3px' style.borderBottomRightRadius = '3px' } } return style } }, // 某个日期是否被选中
textStyle() { return (item) => { const date = dayjs(item.date).format("YYYY-MM-DD"), style = {} // 选中的日期,提示文字设置白色
if (this.selected.some(item => this.dateSame(item, date))) { style.color = '#ffffff' } if (this.mode === 'range') { const len = this.selected.length - 1 // 如果是范围选择模式,第一个和最后一个之间的日期,文字颜色设置为高亮的主题色
if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this .selected[len]))) { style.color = this.color } } return style } }, // 获取底部的提示文字
getBottomInfo() { return (index1, index2, item) => { const date = dayjs(item.date).format("YYYY-MM-DD") const bottomInfo = item.bottomInfo // 当为日期范围模式时,且选择的日期个数大于0时
if (this.mode === 'range' && this.selected.length > 0) { if (this.selected.length === 1) { // 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始”
if (this.dateSame(date, this.selected[0])) return this.startText else return bottomInfo } else { const len = this.selected.length - 1 // 如果数组中的日期大于2个时,第一个和最后一个显示为开始和结束日期
if (this.dateSame(date, this.selected[0]) && this.dateSame(date, this.selected[1]) && len === 1) { // 如果长度为2,且第一个等于第二个日期,则提示语放在同一个item中
return `${this.startText}/${this.endText}` } else if (this.dateSame(date, this.selected[0])) { return this.startText } else if (this.dateSame(date, this.selected[len])) { return this.endText } else { return bottomInfo } } } else { return bottomInfo } } } }, mounted() { this.init() }, methods: { init() { // 初始化默认选中
this.$emit('monthSelected', this.selected) this.$nextTick(() => { // 这里需要另一个延时,因为获取宽度后,会进行月份数据渲染,只有渲染完成之后,才有真正的高度
// 因为nvue下,$nextTick并不是100%可靠的
sleep(10).then(() => { this.getWrapperWidth() this.getMonthRect() }) }) }, // 判断两个日期是否相等
dateSame(date1, date2) { return dayjs(date1).isSame(dayjs(date2)) }, // 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度
getWrapperWidth() { // #ifdef APP-NVUE
dom.getComponentRect(this.$refs['u-calendar-month-wrapper'], res => { this.width = res.size.width }) // #endif
// #ifndef APP-NVUE
this.$uGetRect('.u-calendar-month-wrapper').then(size => { this.width = size.width }) // #endif
}, getMonthRect() { // 获取每个月份数据的尺寸,用于父组件在scroll-view滚动事件中,监听当前滚动到了第几个月份
const promiseAllArr = this.months.map((item, index) => this.getMonthRectByPromise( `u-calendar-month-${index}`)) // 一次性返回
Promise.all(promiseAllArr).then( sizes => { let height = 1 const topArr = [] for (let i = 0; i < this.months.length; i++) { // 添加到months数组中,供scroll-view滚动事件中,判断当前滚动到哪个月份
topArr[i] = height height += sizes[i].height } // 由于微信下,无法通过this.months[i].top的形式(引用类型)去修改父组件的month的top值,所以使用事件形式对外发出
this.$emit('updateMonthTop', topArr) }) }, // 获取每个月份区域的尺寸
getMonthRectByPromise(el) { // #ifndef APP-NVUE
// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://ijry.github.io/uview-plus/js/getRect.html
// 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
return new Promise(resolve => { this.$uGetRect(`.${el}`).then(size => { resolve(size) }) }) // #endif
// #ifdef APP-NVUE
// nvue下,使用dom模块查询元素高度
// 返回一个promise,让调用此方法的主体能使用then回调
return new Promise(resolve => { dom.getComponentRect(this.$refs[el][0], res => { resolve(res.size) }) }) // #endif
}, // 点击某一个日期
clickHandler(index1, index2, item) { if (this.readonly) { return; } this.item = item const date = dayjs(item.date).format("YYYY-MM-DD") if (item.disabled) return // 对上一次选择的日期数组进行深度克隆
let selected = deepClone(this.selected) if (this.mode === 'single') { // 单选情况下,让数组中的元素为当前点击的日期
selected = [date] } else if (this.mode === 'multiple') { if (selected.some(item => this.dateSame(item, date))) { // 如果点击的日期已在数组中,则进行移除操作,也就是达到反选的效果
const itemIndex = selected.findIndex(item => item === date) selected.splice(itemIndex, 1) } else { // 如果点击的日期不在数组中,且已有的长度小于总可选长度时,则添加到数组中去
if (selected.length < this.maxCount) selected.push(date) } } else { // 选择区间形式
if (selected.length === 0 || selected.length >= 2) { // 如果原来就为0或者大于2的长度,则当前点击的日期,就是开始日期
selected = [date] } else if (selected.length === 1) { // 如果已经选择了开始日期
const existsDate = selected[0] // 如果当前选择的日期小于上一次选择的日期,则当前的日期定为开始日期
if (dayjs(date).isBefore(existsDate)) { selected = [date] } else if (dayjs(date).isAfter(existsDate)) { // 当前日期减去最大可选的日期天数,如果大于起始时间,则进行提示
if(dayjs(dayjs(date).subtract(this.maxRange, 'day')).isAfter(dayjs(selected[0])) && this.showRangePrompt) { if(this.rangePrompt) { toast(this.rangePrompt) } else { toast(`选择天数不能超过 ${this.maxRange} 天`) } return } // 如果当前日期大于已有日期,将当前的添加到数组尾部
selected.push(date) const startDate = selected[0] const endDate = selected[1] const arr = [] let i = 0 do { // 将开始和结束日期之间的日期添加到数组中
arr.push(dayjs(startDate).add(i, 'day').format("YYYY-MM-DD")) i++ // 累加的日期小于结束日期时,继续下一次的循环
} while (dayjs(startDate).add(i, 'day').isBefore(dayjs(endDate))) // 为了一次性修改数组,避免computed中多次触发,这里才用arr变量一次性赋值的方式,同时将最后一个日期添加近来
arr.push(endDate) selected = arr } else { // 选择区间时,只有一个日期的情况下,且不允许选择起止为同一天的话,不允许选择自己
if (selected[0] === date && !this.allowSameDay) return selected.push(date) } } } this.setSelected(selected) }, // 设置默认日期
setDefaultDate() { if (!this.defaultDate) { // 如果没有设置默认日期,则将当天日期设置为默认选中的日期
const selected = [dayjs().format("YYYY-MM-DD")] return this.setSelected(selected, false) } let defaultDate = [] const minDate = this.minDate || dayjs().format("YYYY-MM-DD") const maxDate = this.maxDate || dayjs(minDate).add(this.maxMonth - 1, 'month').format("YYYY-MM-DD") if (this.mode === 'single') { // 单选模式,可以是字符串或数组,Date对象等
if (!test.array(this.defaultDate)) { defaultDate = [dayjs(this.defaultDate).format("YYYY-MM-DD")] } else { defaultDate = [this.defaultDate[0]] } } else { // 如果为非数组,则不执行
if (!test.array(this.defaultDate)) return defaultDate = this.defaultDate } // 过滤用户传递的默认数组,取出只在可允许最大值与最小值之间的元素
defaultDate = defaultDate.filter(item => { return dayjs(item).isAfter(dayjs(minDate).subtract(1, 'day')) && dayjs(item).isBefore(dayjs( maxDate).add(1, 'day')) }) this.setSelected(defaultDate, false) }, setSelected(selected, event = true) { this.selected = selected event && this.$emit('monthSelected', this.selected,'tap') } } } </script>
<style lang="scss" scoped> @import "../../libs/css/components.scss";
.u-calendar-month-wrapper { margin-top: 4px; }
.u-calendar-month {
&__title { font-size: 14px; line-height: 42px; height: 42px; color: $u-main-color; text-align: center; font-weight: bold; }
&__days { position: relative; @include flex; flex-wrap: wrap;
&__month-mark-wrapper { position: absolute; top: 0; bottom: 0; left: 0; right: 0; @include flex; justify-content: center; align-items: center;
&__text { font-size: 155px; color: rgba(231, 232, 234, 0.83); } }
&__day { @include flex; padding: 2px; /* #ifndef APP-NVUE */ // vue下使用css进行宽度计算,因为某些安卓机会无法进行js获取父元素宽度进行计算得出,会有偏移
width: calc(100% / 7); box-sizing: border-box; /* #endif */
&__select { flex: 1; @include flex; align-items: center; justify-content: center; position: relative;
&__dot { width: 7px; height: 7px; border-radius: 100px; background-color: $u-error; position: absolute; top: 12px; right: 7px; }
&__buttom-info { color: $u-content-color; text-align: center; position: absolute; bottom: 5px; font-size: 10px; text-align: center; left: 0; right: 0;
&--selected { color: #ffffff; }
&--disabled { color: #cacbcd; } }
&__info { text-align: center; font-size: 16px;
&--selected { color: #ffffff; }
&--disabled { color: #cacbcd; } }
&--selected { background-color: $u-primary; @include flex; justify-content: center; align-items: center; flex: 1; border-radius: 3px; }
&--range-selected { opacity: 0.3; border-radius: 0; }
&--range-start-selected { border-top-right-radius: 0; border-bottom-right-radius: 0; }
&--range-end-selected { border-top-left-radius: 0; border-bottom-left-radius: 0; } } } } } </style>
|