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
+
+
+ 常规日历组件
+ 起始时间:{{toastText[0]}}
+ 结束时间:{{toastText[1]}}
+
+
+
+
+```
+
+
+
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 @@
+
+
+ 常规日历组件
+ 起始时间:{{toastText[0]}}
+ 结束时间:{{toastText[1]}}
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+ 起始时间
+ 年份:{{currentStartDate.year || '暂未选择'}}
+ 月份:{{currentStartDate.month || '暂未选择'}}
+ 天份:{{currentStartDate.day || '暂未选择'}}
+ 时间戳:{{currentStartDate.date || '暂未选择'}}
+
+
+ 结束时间
+ 年份:{{currentEndDate.year || '暂未选择'}}
+ 月份:{{currentEndDate.month || '暂未选择'}}
+ 天份:{{currentEndDate.day || '暂未选择'}}
+ 时间戳:{{currentEndDate.date || '暂未选择'}}
+
+
+
+
+```
+
+
+
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 @@
+
+
+
+
+ 起始时间
+ 年份:{{currentStartDate.year || '暂未选择'}}
+ 月份:{{currentStartDate.month || '暂未选择'}}
+ 天份:{{currentStartDate.day || '暂未选择'}}
+ 时间戳:{{currentStartDate.date || '暂未选择'}}
+
+
+ 结束时间
+ 年份:{{currentEndDate.year || '暂未选择'}}
+ 月份:{{currentEndDate.month || '暂未选择'}}
+ 天份:{{currentEndDate.day || '暂未选择'}}
+ 时间戳:{{currentEndDate.date || '暂未选择'}}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ {{title}}
+
+
+
+
+ {{buttonText}}
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+
+
+ {{item.day}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+}