diff --git a/docs/config/sidebar.js b/docs/config/sidebar.js index b1234fc8..4a1fdf52 100644 --- a/docs/config/sidebar.js +++ b/docs/config/sidebar.js @@ -68,6 +68,10 @@ module.exports = [ { title: 'TabBar 选项卡', path: '/components/base/tab-bar' + }, + { + title: 'Calendar 日历', + path: '/components/base/calendar' } // { // title: 'Style 内置样式' @@ -158,6 +162,10 @@ module.exports = [ { title: 'ActionSheet 操作列表', path: '/components/base/action-sheet' + }, + { + title: 'CalendarModal 日历弹框', + path: '/components/base/calendar-modal' } ] }, diff --git a/example/app.mpx b/example/app.mpx index 92736e07..20d75c70 100644 --- a/example/app.mpx +++ b/example/app.mpx @@ -52,7 +52,9 @@ "./pages/loading/index", "./pages/input/index", "./pages/action-sheet/index", - "./pages/tab-bar/index" + "./pages/tab-bar/index", + "./pages/calendar-modal/index", + "./pages/calendar/index" ] if (__mpx_mode__ === 'ios' || __mpx_mode__ === 'android') { pages = [ diff --git a/example/common/config.ts b/example/common/config.ts index 8d827562..f1b3288b 100644 --- a/example/common/config.ts +++ b/example/common/config.ts @@ -1,6 +1,6 @@ export default { - 'entryMap': { - 'default': [ + entryMap: { + default: [ 'button', 'button-group', 'collapse', @@ -46,7 +46,8 @@ export default { 'float-ball', 'loading', 'collapse', - 'tab-bar' + 'tab-bar', + 'calendar' ] }, { @@ -89,7 +90,8 @@ export default { 'picker-popup', 'cascade-picker-popup', 'date-picker-popup', - 'time-picker-popup' + 'time-picker-popup', + 'calendar-modal', ] } ], diff --git a/example/pages/calendar-modal/README.md b/example/pages/calendar-modal/README.md new file mode 100644 index 00000000..4560bb8a --- /dev/null +++ b/example/pages/calendar-modal/README.md @@ -0,0 +1,66 @@ +## cube-calendar-modal + + + +### 介绍 + +日历选择弹框 + + + +### 示例 + + + +### 用法 + + + + +```vue + + +``` + + + diff --git a/example/pages/calendar-modal/index.mpx b/example/pages/calendar-modal/index.mpx new file mode 100644 index 00000000..e5cb3ff7 --- /dev/null +++ b/example/pages/calendar-modal/index.mpx @@ -0,0 +1,25 @@ + + + + + + + diff --git a/example/pages/calendar-modal/normal-calendar.mpx b/example/pages/calendar-modal/normal-calendar.mpx new file mode 100644 index 00000000..87924fc2 --- /dev/null +++ b/example/pages/calendar-modal/normal-calendar.mpx @@ -0,0 +1,66 @@ + + + + + + + diff --git a/example/pages/calendar/README.md b/example/pages/calendar/README.md new file mode 100644 index 00000000..fcf8705c --- /dev/null +++ b/example/pages/calendar/README.md @@ -0,0 +1,72 @@ +## cube-calendar + + + +### 介绍 + +日历组件 + + + +### 示例 + + + +### 用法 + + + + +```vue + + +``` + + + diff --git a/example/pages/calendar/calendar.mpx b/example/pages/calendar/calendar.mpx new file mode 100644 index 00000000..87a9681a --- /dev/null +++ b/example/pages/calendar/calendar.mpx @@ -0,0 +1,71 @@ + + + + + + + diff --git a/example/pages/calendar/index.mpx b/example/pages/calendar/index.mpx new file mode 100644 index 00000000..b200e26a --- /dev/null +++ b/example/pages/calendar/index.mpx @@ -0,0 +1,25 @@ + + + + + + + diff --git a/packages/mpx-cube-ui/src/common/stylus/theme/components/calendar-modal.styl b/packages/mpx-cube-ui/src/common/stylus/theme/components/calendar-modal.styl new file mode 100644 index 00000000..fdf1ea0d --- /dev/null +++ b/packages/mpx-cube-ui/src/common/stylus/theme/components/calendar-modal.styl @@ -0,0 +1,9 @@ +// @type body +$calendar-modal-content-padding-top := 15px // 容器距上内边距 +$calendar-modal-content-border-radius := 10px // 容器圆角边框 +$calendar-modal-title-margin := 9px 0 9px 15px // 标题外边距 +$calendar-modal-title-size := 22px // 标题字体 +$calendar-modal-cross-top := 10px // 隐藏图标距上距离 +$calendar-modal-cross-right := 10px // 隐藏图标距右距离 +$calendar-modal-button-padding := 10px // 按钮内边距 + diff --git a/packages/mpx-cube-ui/src/common/stylus/theme/components/calendar.styl b/packages/mpx-cube-ui/src/common/stylus/theme/components/calendar.styl new file mode 100644 index 00000000..28fb82ed --- /dev/null +++ b/packages/mpx-cube-ui/src/common/stylus/theme/components/calendar.styl @@ -0,0 +1,30 @@ +// @type +$calendar-inner-padding := 0 8px 10px 8px // 容器内边距 +$calendar-inner-border-radius := 10px 10px 0 0 // 容器圆角 +$calendar-height := 310px // 容器高度 +$calendar-days-li-lh := 23px // 日期文案行高 +$calendar-days-li-size := 14px // 日期文案字体大小 +$calendar-days-li-padding := 10px 0 // 日期文案内边距 +$calendar-title-size := 22px // 标题字体大小 +$calendar-title-margin := 9px 0 // 标题字体大小 + +$calendar-render-wrapper-padding := 10px 0 10px 0 // 日期容器内边距 + + +$calendar-date-header-padding-top := 10px // 日期标题top内边距 +$calendar-date-header-padding-bottom := 10px // 日期标题bottom内边距 +$calendar-date-header-margin-bottom := 6px // 日期标题bottom外边距 +$calendar-date-header-size := 16px // 日期方格标题字体大小 +$calendar-date-header-lh := 1.4 // 日期方格标题行高 + +$calendar-date-li-min-height := 25px // 日期方格最小高度 +$calendar-date-li-margin-bottom := 8px // 日期方格底步外边距 +$calendar-date-li-size := 14px // 日期元素字体大小 + +$calendar-date-start-border-radius := 6px 0 0 6px // 日期元素开始时圆角 +$calendar-date-end-border-radius := 0 6px 6px 0 // 日期元素结束时圆角 +$calendar-date-width := 30px // 日期元素宽度 +$calendar-date-min-height := 25px // 日期元素最小高度 +$calendar-date-border-radius := 6px // 日期元素圆角 + + diff --git a/packages/mpx-cube-ui/src/components/calendar-modal/calendar-modal.ts b/packages/mpx-cube-ui/src/components/calendar-modal/calendar-modal.ts new file mode 100644 index 00000000..0fb037d7 --- /dev/null +++ b/packages/mpx-cube-ui/src/components/calendar-modal/calendar-modal.ts @@ -0,0 +1,105 @@ +import { createComponent } from '@mpxjs/core' +import { visibilityMixin } from '../../common/mixins' +import { getCurrentOrNextYearDay } from '@mpxjs/mpx-cube-ui/src/components/calendar/utils' +const EVENT_MASK_CLOSE = 'maskClose' +const EVENT_CONFIRM = 'confirm' +const EVENT_CANCEL = 'cancel' +const SELECT_DATE_OVER_RANGE = 'selectDateOverRange' + +createComponent({ + mixins: [visibilityMixin], + options: { + multipleSlots: true, + styleIsolation: 'shared' + }, + properties: { + // 可选择的最小日期 + min: { + type: Number, + // 今年1月1日 + value: getCurrentOrNextYearDay() + }, + // 可选择的最大日期 + max: { + type: Number, + // 明年12月31日 + value: getCurrentOrNextYearDay(false) + }, + // 可选的最大范围,0 为不限制 + maxRange: { + type: Number, + value: 30 + }, + // 日期默认值,区间选择Array格式 + defaultDate: { + type: Array, + value: [] + }, + // 容器高度 + height: { + type: String, + value: '300px' + }, + // 点击遮罩层是非关闭弹框 + maskClosable: { + type: Boolean, + value: true + }, + // 标题 + title: { + type: String, + value: '选择日期' + }, + // 按钮文案 + buttonText: { + type: String, + value: '完成' + }, + // 展示超出范围提示语 + showOverRangeTips: { + type: Boolean, + value: true + } + }, + data: { + isVisible: false, + lastValue: [] as any[] + }, + methods: { + maskClick() { + // 点击遮盖层 + this.triggerEvent(EVENT_MASK_CLOSE) + this.hide() + }, + cancel() { + if (!this.lastValue.length) { + this.lastValue = this.defaultDate + } + console.log('this.lastValue', this.lastValue) + const dateRange = this.$refs.calendar.getSelectDate() + // 点击叉号 + // @arg event.detail = { value }, 表当前选中的时间范围 + this.triggerEvent(EVENT_CANCEL, { value: dateRange }) + this.hide() + }, + confirm() { + const dateRange = this.$refs.calendar.getSelectDate() + this.lastValue = [dateRange[0].date, dateRange[1].date] + // 点击确认 + // @arg event.detail = { value }, 表当前选中的时间范围 + this.triggerEvent(EVENT_CONFIRM, { value: dateRange }) + this.hide() + }, + // @vuese + // 显示 + showCalendar() { + this.$refs.calendar.reset(this.lastValue) + this.show() + }, + // 显示 + selectDateOverRange() { + // 选择的日期超过最大范围时触发 + this.triggerEvent(SELECT_DATE_OVER_RANGE) + } + } +}) diff --git a/packages/mpx-cube-ui/src/components/calendar-modal/index.mpx b/packages/mpx-cube-ui/src/components/calendar-modal/index.mpx new file mode 100644 index 00000000..2ee4536a --- /dev/null +++ b/packages/mpx-cube-ui/src/components/calendar-modal/index.mpx @@ -0,0 +1,73 @@ + + + + + + + diff --git a/packages/mpx-cube-ui/src/components/calendar/calendar.ts b/packages/mpx-cube-ui/src/components/calendar/calendar.ts new file mode 100644 index 00000000..230121de --- /dev/null +++ b/packages/mpx-cube-ui/src/components/calendar/calendar.ts @@ -0,0 +1,321 @@ +import { createComponent } from '@mpxjs/core' +import { + getWeekInMonth, + getWeeksCountInMonth, + getRangeDaysCount, + getDaysCountInMonth, + getDayInWeek, + getDateObj, + getCurrentOrNextYearDay +} from './utils' + +createComponent({ + options: { + styleIsolation: 'shared' + }, + properties: { + // 可选择的最小日期 + min: { + type: Number, + // 今年1月1日 + value: getCurrentOrNextYearDay() + }, + // 可选日期的最大时间 + max: { + type: Number, + // 明年12月31日 + value: getCurrentOrNextYearDay(false) + }, + // 可选的最大范围,0 为不限制 + maxRange: { + type: Number, + value: 30 + }, + // 日期默认值,区间选择Array格式 + defaultDate: { + type: Array, + value: [] + }, + // 容器高度 + height: { + type: String, + value: '300px' + }, + // 展示超出范围提示语 + showOverRangeTips: { + type: Boolean, + value: true + } + }, + data: { + days: ['日', '一', '二', '三', '四', '五', '六'], + dateList: [], + selectDateSet: [] as any[], // 记录已选起始和结束的时间 + dateClass: '', + agoClickIndex: { + listIndex: null, + weekInMonthIndex: null, + index: null + }, + toastText: '' + }, + watch: { + selectDateSet(v) { + const startDate = v[0] || {} + const endDate = v.length > 1 ? v[v.length - 1] : {} + // 选择的日期改变时触发 + // @arg event.detail = { len, startDate, endDate }, len表当前选中的时间间隔,startDate表当前选中的开始时间,endDate表当前选中的结束时间 + this.triggerEvent('dateChange', { + len: v.length, + startDate, + endDate + }) + } + }, + lifetimes: { + ready() { + this.getRangeDateArray() + this.reset(this.defaultDate) + } + }, + methods: { + selectDate(item, listIndex, weekInMonthIndex, index) { + let selectDaysCount = 0 + if (item.disable || !item.date) return + this.resetDateRender(item) + // 选择开始时间 + if (!this.selectDateSet.length) { + this.selectDateSet.push(item) + } else { + // 选择结束时间 + selectDaysCount = getRangeDaysCount(+(this.selectDateSet[0] as any).date, item.date) + if (this.maxRange && selectDaysCount > this.maxRange) { + if (this.showOverRangeTips) { + this.toastText = `最多选择${this.maxRange}天` + this.$refs.toast.show() + } + // 选择的日期超过最大范围时触发 + this.triggerEvent('selectDateOverRange') + return + } + if (selectDaysCount > 0) { + this.renderSelectedRangeDate(this.selectDateSet[0], item) + } + } + if (item.date) { + // eslint-disable-next-line + let currentDate = (this.dateList[listIndex] as any).dateArr[weekInMonthIndex][index] + const { date: selectStartTime } = this.selectDateSet[0] + const { date: selectEndTime } = this.selectDateSet[this.selectDateSet.length - 1] + if (currentDate.date === selectStartTime || currentDate.date === selectEndTime) { + // eslint-disable-next-line + currentDate['active'] = true + } + this.agoClickIndex = { listIndex, weekInMonthIndex, index } + } + }, + reset(dateRange) { + if (dateRange && dateRange.length === 2) { + this.clear() + const startDateObj = getDateObj(dateRange[0]) + const endDateObj = getDateObj(dateRange[1]) + this.selectDateSet.push(startDateObj) + this.renderSelectedRangeDate(startDateObj, endDateObj) + this.resetSelectDate() + this.$set(this.selectDateSet[0], 'active', true) + this.$set(this.selectDateSet[this.selectDateSet.length - 1], 'active', true) + } + }, + clear() { + if (!this.selectDateSet || !this.selectDateSet.length) { return } + this.selectDateSet.forEach((item, index) => { + this.$set(item, 'dateClass', '') + item.active && this.$set(item, 'active', false) + }) + this.selectDateSet = [] + }, + resetSelectDate() { + const { listIndex, weekInMonthIndex, index } = this.agoClickIndex + if (listIndex !== null && weekInMonthIndex !== null && index !== null) { + // eslint-disable-next-line + (this.dateList[listIndex] as any).dateArr[weekInMonthIndex][index]['active'] = false + } + }, + resetDateRender(item) { + if (this.selectDateSet.length && (this.selectDateSet.length >= 2 || item.date <= this.selectDateSet[0].date)) { + this.resetSelectDate() + for (let i = 0; i < this.selectDateSet.length; i++) { + this.$set(this.selectDateSet[i], 'dateClass', '') + this.$set(this.selectDateSet[i], 'active', false) + // 遍历重置样式后,清空数组 + if (i === this.selectDateSet.length - 1) { + this.selectDateSet.length = 0 + } + } + } + }, + getMonthDateGroup(year, month) { + let monthGroupIndex + let monthGroupData = '' as any + + monthGroupData = this.dateList.find((item: any, index) => { + if (item.year === year && item.month === month) { + monthGroupIndex = index + return item + } + return '' + }) + return { + data: monthGroupData, + index: monthGroupIndex + } + }, + renderSelectedRangeDate(startDateObj, endDateObj) { + let startDateWeekInMonth = startDateObj.weekInMonth + let endDateWeekInMonth = endDateObj.weekInMonth + let startDateInWeek = startDateObj.dayInWeek + const startMonthIndex = this.getMonthDateGroup(startDateObj.year, startDateObj.month).index + const endMonthIndex = this.getMonthDateGroup(endDateObj.year, endDateObj.month).index + let monthDateGroup + for (let currentMonthIndex = startMonthIndex; currentMonthIndex <= endMonthIndex; currentMonthIndex++) { + monthDateGroup = this.dateList[currentMonthIndex] + + if (currentMonthIndex !== startMonthIndex) { + startDateInWeek = 0 + startDateWeekInMonth = 0 + } else { + startDateWeekInMonth = startDateObj.weekInMonth + startDateInWeek = startDateObj.dayInWeek + } + endDateWeekInMonth = currentMonthIndex !== endMonthIndex + ? monthDateGroup.dateArr.length - 1 // 取该月最后一周 + : endDateObj.weekInMonth + this.renderDateInOneMonth(startDateInWeek, monthDateGroup, startDateWeekInMonth, endDateWeekInMonth, endDateObj.date) + } + this.selectDateSet.length && this.selectDateSet.shift() + }, + renderDateInOneMonth(startDateInWeek, monthDateGroup, startDateWeekInMonth, endDateWeekInMonth, endDate) { + let day = startDateInWeek + const rangeArr = [] + let weekDateGroup + let dateClass + + const endDateTimestamp = new Date(endDate).setHours(0, 0, 0, 0) + for (let week = startDateWeekInMonth; week <= endDateWeekInMonth; week++) { + weekDateGroup = monthDateGroup.dateArr[week] + + for (day; day <= 7; day++) { + if (day === 7) { + day = 0 + break + } + + // 渲染开始日期样式 + const selectDateSetTime = new Date(this.selectDateSet[0].date).setHours(0, 0, 0, 0) + + if (weekDateGroup[day].date === +selectDateSetTime) { + this.$set(weekDateGroup[day], 'dateClass', 'start-date') + } + dateClass = weekDateGroup[day].dateClass && weekDateGroup[day].date + ? `${weekDateGroup[day].dateClass} transition-date` + : 'transition-date' + this.$set(weekDateGroup[day], 'dateClass', dateClass) + rangeArr.push(weekDateGroup[day] as never) + if (weekDateGroup[day].date >= +endDateTimestamp) { + break + } + } + } + this.selectDateSet = [...this.selectDateSet, ...rangeArr] + + // 渲染结束日期样式 + if (this.selectDateSet.length >= 2 && +weekDateGroup[day].date === +(this.selectDateSet[this.selectDateSet.length - 1] as any).date) { + this.$set(weekDateGroup[day], 'dateClass', weekDateGroup[day].dateClass ? `${weekDateGroup[day].dateClass} end-date` : 'end-date') + } + }, + getRangeDateArray() { + // TODO: 校验传入的日期格式 + const minDate = new Date(this.min) + const maxDate = new Date(this.max) + const minYear = minDate.getFullYear() + const maxYear = maxDate.getFullYear() + const minMonth = minDate.getMonth() + 1 + const maxMonth = maxDate.getMonth() + 1 + + if (this.min >= this.max) { + console.warn('传入props错误:时间的max值应大于min值!') + return + } + + for (let year = minYear; year <= maxYear; year++) { + const monthLowerLimit = year === minYear ? minMonth : 1 + const monthUpperLimit = year === maxYear ? maxMonth : 12 + for (let month = monthLowerLimit; month <= monthUpperLimit; month++) { + this.dateList.push(this.getCurrentMonthDaysArray(year, month) as never) + } + } + }, + getCurrentMonthDaysArray(year, month) { + const days = getDaysCountInMonth(year, month) + const weeksCountInMonth = getWeeksCountInMonth(year, month) + // 根据周数,初始化二维数组 + const daysArray = [] as any[] + for (let i = 0; i < weeksCountInMonth; i++) { + daysArray[i] = [] + } + + // 当月日历面板中的排列 + for (let day = 1; day <= days; day++) { + const currentWeekInMonth = getWeekInMonth(year, month, day) + const disable = +new Date(year, month - 1, day) < this.min || +new Date(year, month - 1, day) > this.max + daysArray[currentWeekInMonth - 1].push({ + day, + month, + year, + date: +new Date(year, month - 1, day), + dayInWeek: getDayInWeek(year, month, day), + weekInMonth: currentWeekInMonth - 1, + active: false, + disable + }) + } + this.fillDaysInMonth(year, month, days, weeksCountInMonth, daysArray) + + return { + title: `${year}年${month}月`, + dayCount: days, + year, + month, + dateArr: [...daysArray] + } + }, + fillDaysInMonth(year, month, days, weeksCountInMonth, daysArray) { + const firstDayInWeek = getDayInWeek(year, month, 1) + const lastDayInWeek = getDayInWeek(year, month, days) + if (firstDayInWeek !== 0) { + const fillArr = [...new Array(firstDayInWeek).fill({ date: '' })] + daysArray[0] = [...fillArr, ...daysArray[0]] + } + if (lastDayInWeek !== 6) { + const fillArr = [...new Array(6 - lastDayInWeek).fill({ date: '' })] + daysArray[weeksCountInMonth - 1] = [...daysArray[weeksCountInMonth - 1], ...fillArr] + } + }, + getSelectDate() { + const startDateObj = this.selectDateSet[0] || {} + const endDateObj = this.selectDateSet[this.selectDateSet.length - 1] || {} + + return [{ + date: startDateObj.date, + year: startDateObj.year, + month: startDateObj.month, + day: startDateObj.day + }, { + date: endDateObj.date, + year: endDateObj.year, + month: endDateObj.month, + day: endDateObj.day + }] + } + } +}) diff --git a/packages/mpx-cube-ui/src/components/calendar/index.mpx b/packages/mpx-cube-ui/src/components/calendar/index.mpx new file mode 100644 index 00000000..0153bc44 --- /dev/null +++ b/packages/mpx-cube-ui/src/components/calendar/index.mpx @@ -0,0 +1,153 @@ + + + + + + + diff --git a/packages/mpx-cube-ui/src/components/calendar/utils.ts b/packages/mpx-cube-ui/src/components/calendar/utils.ts new file mode 100644 index 00000000..97cf7f3c --- /dev/null +++ b/packages/mpx-cube-ui/src/components/calendar/utils.ts @@ -0,0 +1,108 @@ +/** + * 获得某天在当月是第几周 + * @param a + * @param b + * @param c + * @returns {number} + */ +function getWeekInMonth(a, b, c) { + const date = new Date(a, parseInt(b) - 1, c) + const w = date.getDay() + const d = date.getDate() + return Math.ceil((d + 6 - w) / 7) +} + +/** + * 获取本月有几周 + * @param year + * @param month + * @returns {number} + */ +function getWeeksCountInMonth(year, month) { + const firstDayInWeek = getDayInWeek(year, month, 1) + const daysCountInMonth = getDaysCountInMonth(year, month) + let weekCount + + if (daysCountInMonth === 31 && (firstDayInWeek === 5 || firstDayInWeek === 6)) { + weekCount = 6 + } else if (daysCountInMonth === 30 && firstDayInWeek === 6) { + weekCount = 6 + } else if (daysCountInMonth === 28 && firstDayInWeek === 0) { + weekCount = 4 + } else { + weekCount = 5 + } + return weekCount +} + +/** + * 计算时间差(天数) + * @param startDate + * @param endDate + * @returns {number} + */ +function getRangeDaysCount(startDate, endDate) { + // TODO: + return Math.floor((endDate - startDate) / (24 * 3600 * 1000) + 1) +} + +/** + * 计算一个月有几天 + * @param year + * @param month + * @returns {number} + */ +function getDaysCountInMonth(year, month) { + return new Date(year, month, 0).getDate() +} + +/** + * 计算某天是周几 + * @param year + * @param month + * @param day + * @returns {number} + */ +function getDayInWeek(year, month, day) { + return new Date(`${year}/${month}/${day}`).getDay() +} + +/** + * 获取今年或明年日期初始值 + * @param isCurrentYear + */ +function getCurrentOrNextYearDay(isCurrentYear = true) { + const now = new Date() + const year = now.getFullYear() + return isCurrentYear ? +new Date(year, 0, 1) : +new Date(year + 1, 11, 30) +} +/** + * 获取日期对象 + * @param inpuDate + * @returns {{date: *, month: number, year: number, day: number, dayInWeek: number, weekInMonth: number}} + */ +function getDateObj(inpuDate) { + const date = new Date(inpuDate) + const month = date.getMonth() + 1 + const year = date.getFullYear() + const day = date.getDate() + + return { + date, + month, + year, + day, + dayInWeek: getDayInWeek(year, month, day), + weekInMonth: getWeekInMonth(year, month, day) - 1 + } +} + +export { + getWeekInMonth, + getWeeksCountInMonth, + getRangeDaysCount, + getDaysCountInMonth, + getDayInWeek, + getDateObj, + getCurrentOrNextYearDay +}