| <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 dayjs from '../../libs/util/dayjs.js'; | 
|     export default { | 
|         name: 'u-calendar-month', | 
|         mixins: [uni.$u.mpMixin, uni.$u.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: uni.$u.props.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 = uni.$u.addUnit(dayWidth) | 
|                     // #endif | 
|                     style.height = uni.$u.addUnit(this.rowHeight) | 
|                     if (index2 === 0) { | 
|                         // 获取当前为星期几,如果为0,则为星期天,减一为每月第一天时,需要向左偏移的item个数 | 
|                         week = (week === 0 ? 7 : week) - 1 | 
|                         style.marginLeft = uni.$u.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 = uni.$u.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%可靠的 | 
|                     uni.$u.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://www.uviewui.com/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 = uni.$u.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) { | 
|                                     uni.$u.toast(this.rangePrompt) | 
|                                 } else { | 
|                                     uni.$u.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 (!uni.$u.test.array(this.defaultDate)) { | 
|                         defaultDate = [dayjs(this.defaultDate).format("YYYY-MM-DD")] | 
|                     } else { | 
|                         defaultDate = [this.defaultDate[0]] | 
|                     } | 
|                 } else { | 
|                     // 如果为非数组,则不执行 | 
|                     if (!uni.$u.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) | 
|             } | 
|         } | 
|     } | 
| </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> |