1. 创建component - calendar
calendar.wxml
<view class="flex box box-tb box-align-center" wx:if="{{calendar}}">
<view class="calendar pink-color box box-tb">
<view class="top-handle fs28 box box-lr box-align-center box-pack-center">
<view class="prev box box-rl" bindtap="choosePrevMonth" data-handle="prev">
<view class="prev-handle box box-lr box-align-center box-pack-center">《</view>
</view>
<view class="date-area box box-lr box-align-center box-pack-center">{{calendar.curYear || "--"}} 年 {{calendar.curMonth || "--"}} 月</view>
<view class="next box box-lr" bindtap="chooseNextMonth" data-handle="next">
<view class="next-handle box box-lr box-align-center box-pack-center">》</view>
</view>
</view>
<view class="weeks box box-lr box-align-center">
<view class="week fs28" wx:for="{{calendar.weeksCh}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view>
</view>
<view class="perspective">
<view class="days box box-lr box-wrap {{calendar.leftSwipe ? 'leftRoate' : ''}} {{calendar.rightSwipe ? 'rightRoate' : ''}}"
bindtouchstart="calendarTouchstart"
bindtouchmove="calendarTouchmove"
bindtouchend="calendarTouchend">
<view wx:if="{{calendar.empytGrids}}" class="grid disable-day-color box box-align-center box-pack-center"
wx:for="{{calendar.empytGrids}}"
wx:key="{{index}}"
data-idx="{{index}}">
<view class="day box box-align-center box-pack-center">{{item}}</view>
</view>
<view class="grid normal-day-color box box-align-center box-pack-center"
wx:for="{{calendar.days}}"
wx:key="{{index}}"
data-disable="{{item.disable}}"
data-idx="{{index}}"
bindtap="tapDayItem">
<view class="day-with-dot box box-tb box-align-center box-pack-center">
<view wx:if="{{item.showTodoLabel && calendar.todoLabelPos === 'top'}}" class="{{item.todoText ? 'todo-text' : 'todo-dot'}}" style="background-color: {{calendar.todoLabelColor}}">{{item.todoText}}</view>
<view class="day border-radius {{item.choosed ? 'day-choosed-color pink-bg' : ''}} {{ item.disable ? 'disable-day-color disable-day-circle' : '' }} box box-align-center box-pack-center">{{item.day}}</view>
<view wx:if="{{item.showTodoLabel && calendar.todoLabelPos === 'bottom'}}" class="{{item.todoText ? 'todo-text' : 'todo-dot'}}" style="background-color: {{calendar.todoLabelColor}}">{{item.todoText}}</view>
</view>
</view>
<view class="grid disable-day-color box box-align-center box-pack-center"
wx:for="{{calendar.lastEmptyGrids}}"
wx:key="{{index}}"
data-idx="{{index}}">
<view class="day box box-align-center box-pack-center">{{item}}</view>
</view>
</view>
</view>
</view>
</view>
calendar.wxss
.box {
display: flex;
}
.box-lr {
flex-direction: row;
}
.box-rl {
flex-direction: row-reverse;
}
.box-tb {
flex-direction: column;
}
.box-pack-center {
justify-content: center;
}
.box-align-center {
align-items: center;
}
.box-wrap {
flex-wrap: wrap;
}
.flex {
flex-grow: 1;
}
.bg {
background-image: linear-gradient(to bottom, #faefe7, #ffcbd7);
overflow: hidden;
}
.pink-color {
color: #ff629a;
}
.white-color {
color: #fff;
}
.fs24 {
font-size: 24rpx;
}
.fs28 {
font-size: 28rpx;
}
.fs32 {
font-size: 32rpx;
}
.fs36 {
font-size: 36rpx;
}
.calendar {
width: 100%;
box-sizing: border-box;
}
.top-handle {
height: 80rpx;
}
.prev {
text-align: right;
height: 80rpx;
}
.next {
height: 80rpx;
}
.prev-handle {
width: 80rpx;
height: 100%;
}
.next-handle {
width: 80rpx;
height: 100%;
}
.date-area {
width: 375rpx;
height: 80rpx;
text-align: center;
}
.weeks {
height: 50rpx;
line-height: 50rpx;
opacity: 0.5;
}
.week {
text-align: center;
line-height: 104rpx;
}
.grid,
.week {
width: 104rpx;
height: 104rpx;
}
.day {
width: 60rpx;
height: 60rpx;
font-size: 26rpx;
font-weight: 200;
}
.normal-day-color {
color: #88d2ac;
}
.day-choosed-color {
color: #fff;
}
.todo-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #cc5226;
}
.todo-text {
font-size: 22rpx;
color: #c2c2c2;
}
.day-with-dot {
height: 72rpx;
}
.disable-day-color {
color: #cacaca;
}
.disable-day-circle {
background-color: #f6f6f7;
}
.border-radius {
border-radius: 50%;
position: relative;
left: 0;
top: 0;
}
.pink-bg {
background-color: #ff629a;
transition: all 0.3s;
animation-name: choosed;
animation-duration: 0.5s;
animation-timing-function: linear;
animation-iteration-count: 1;
}
@keyframes choosed {
from {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
to {
transform: scale(1);
}
}
.purple-bg {
background-color: #b8b8f1;
}
.right-triangle::after {
content: '';
display: block;
width: 0;
height: 0;
border: 15rpx solid transparent;
border-left-color: #ff629a;
position: absolute;
right: -22rpx;
top: 18rpx;
}
.left-triangle::before {
content: '';
display: block;
width: 0;
height: 0;
border: 15rpx solid transparent;
border-right-color: #ff629a;
position: absolute;
left: -22rpx;
top: 18rpx;
}
.tips {
text-align: center;
margin-top: 20rpx;
margin-bottom: 20rpx;
}
.types {
background-color: #ffedf4;
height: 50rpx;
}
.types-desc {
padding: 0 20rpx;
}
.type-name {
margin-top: 50rpx;
margin-bottom: 30rpx;
}
.type-desc {
padding: 0 35rpx;
line-height: 38rpx;
}
.explain {
border-top: 1px solid #eee;
width: 90%;
margin: 20rpx 5% 20rpx 5%;
padding: 20rpx 0;
}
.explain-title {
font-weight: bold;
margin-bottom: 15rpx;
}
.explain-item {
padding: 8rpx 20rpx;
color: #fff;
}
.left-border-radius {
border-top-left-radius: 20rpx;
border-bottom-left-radius: 20rpx;
}
.right-border-radius {
border-top-right-radius: 20rpx;
border-bottom-right-radius: 20rpx;
}
.perspective {
perspective: 750rpx;
}
.leftRoate {
transition: all 1s;
transform: rotateY(-5deg);
}
.rightRoate {
transition: all 1s;
transform: rotateY(5deg);
}
calendar.js
import {
isLeftSlide,
isRightSlide,
getCurrentPage,
whenChangeMonth,
renderCalendar,
whenMulitSelect,
whenSingleSelect,
calculateNextWeekDays,
calculatePrevWeekDays
} from './main.js';
let currentPage = {};
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: {
calendar: {
type: Object
}
},
lifetimes: {
attached: function() {
currentPage = getCurrentPage();
}
},
attached: function() {
currentPage = getCurrentPage();
},
methods: {
/**
* 选择上一月
*/
choosePrevMonth() {
const { curYear, curMonth } = this.data.calendar;
let newYear = curYear;
let newMonth = curMonth - 1;
if (newMonth < 1) {
newYear = curYear - 1;
newMonth = 12;
}
whenChangeMonth.call(currentPage, {
curYear,
curMonth,
newYear,
newMonth
});
currentPage.setData({
'calendar.curYear': newYear,
'calendar.curMonth': newMonth
});
renderCalendar.call(currentPage, newYear, newMonth);
},
/**
* 选择下一月
*/
chooseNextMonth() {
const { curYear, curMonth } = this.data.calendar;
let newYear = curYear;
let newMonth = curMonth + 1;
if (newMonth > 12) {
newYear = curYear + 1;
newMonth = 1;
}
whenChangeMonth.call(currentPage, {
curYear,
curMonth,
newYear,
newMonth
});
currentPage.setData({
'calendar.curYear': newYear,
'calendar.curMonth': newMonth
});
renderCalendar.call(currentPage, newYear, newMonth);
},
/**
* 日期点击事件
* @param {!object} e 事件对象
*/
tapDayItem(e) {
const { idx, disable } = e.currentTarget.dataset;
if (disable) return;
let currentSelected = {}; // 当前选中日期
let { days, selectedDay: selectedDays } = this.data.calendar || []; // 所有选中日期
const config = currentPage.config;
const { multi, onTapDay } = config;
const opts = {
e,
idx,
onTapDay,
currentSelected,
selectedDays,
days: days.slice()
};
if (multi) {
whenMulitSelect.call(currentPage, opts);
} else {
whenSingleSelect.call(currentPage, opts);
}
},
/**
* 日历滑动开始
* @param {object} e
*/
calendarTouchstart(e) {
const t = e.touches[0];
const startX = t.clientX;
const startY = t.clientY;
currentPage.slideLock = true; // 滑动事件加锁
currentPage.setData({
'gesture.startX': startX,
'gesture.startY': startY
});
},
/**
* 日历滑动中
* @param {object} e
*/
calendarTouchmove(e) {
const self = currentPage;
if (isLeftSlide.call(self, e)) {
self.setData({
'calendar.leftSwipe': 1
});
if (currentPage.weekMode)
return calculateNextWeekDays.call(currentPage);
this.chooseNextMonth();
}
if (isRightSlide.call(self, e)) {
self.setData({
'calendar.rightSwipe': 1
});
if (currentPage.weekMode)
return calculatePrevWeekDays.call(currentPage);
this.choosePrevMonth();
}
},
calendarTouchend(e) {
currentPage.setData({
'calendar.leftSwipe': 0,
'calendar.rightSwipe': 0
});
}
}
});
main.js
let info;
let currentPage = {};
function getSystemInfo() {
if (info) return info;
info = wx.getSystemInfoSync();
return info;
}
function isIos() {
const sys = getSystemInfo();
return /iphone|ios/i.test(sys.platform);
}
/**
* 获取当前页面实例
*/
export function getCurrentPage() {
const pages = getCurrentPages();
const last = pages.length - 1;
return pages[last];
}
/**
* new Date 区分平台
* @param {number} year
* @param {number} month
* @param {number} day
*/
function newDate(year, month, day) {
let cur = `${year}-${month}-${day}`;
if (isIos()) {
cur = `${year}/${month}/${day}`;
}
return new Date(cur);
}
/**
* todo 数组去重
* @param {array} array todo 数组
*/
function uniqueTodoLabels(array = []) {
let uniqueObject = {};
let uniqueArray = [];
array.forEach(item => {
uniqueObject[`${item.year}-${item.month}-${item.day}`] = item;
});
for (let i in uniqueObject) {
uniqueArray.push(uniqueObject[i]);
}
return uniqueArray;
}
/**
* 上滑
* @param {object} e 事件对象
* @returns {boolean} 布尔值
*/
export function isUpSlide(e) {
const { startX, startY } = this.data.gesture;
if (this.slideLock) {
const t = e.touches[0];
const deltaX = t.clientX - startX;
const deltaY = t.clientY - startY;
if (deltaY < -60 && deltaX < 20 && deltaX > -20) {
this.slideLock = false;
return true;
} else {
return false;
}
}
}
/**
* 下滑
* @param {object} e 事件对象
* @returns {boolean} 布尔值
*/
export function isDownSlide(e) {
const { startX, startY } = this.data.gesture;
if (this.slideLock) {
const t = e.touches[0];
const deltaX = t.clientX - startX;
const deltaY = t.clientY - startY;
if (deltaY > 60 && deltaX < 20 && deltaX > -20) {
this.slideLock = false;
return true;
} else {
return false;
}
}
}
/**
* 左滑
* @param {object} e 事件对象
* @returns {boolean} 布尔值
*/
export function isLeftSlide(e) {
const { startX, startY } = this.data.gesture;
if (this.slideLock) {
const t = e.touches[0];
const deltaX = t.clientX - startX;
const deltaY = t.clientY - startY;
if (deltaX < -60 && deltaY < 20 && deltaY > -20) {
this.slideLock = false;
return true;
} else {
return false;
}
}
}
/**
* 右滑
* @param {object} e 事件对象
* @returns {boolean} 布尔值
*/
export function isRightSlide(e) {
const { startX, startY } = this.data.gesture;
if (this.slideLock) {
const t = e.touches[0];
const deltaX = t.clientX - startX;
const deltaY = t.clientY - startY;
if (deltaX > 60 && deltaY < 20 && deltaY > -20) {
this.slideLock = false;
return true;
} else {
return false;
}
}
}
// function info(msg) {
// console.log('%cInfo: %c' + msg, 'color:#FF0080;font-weight:bold', 'color: #FF509B');
// }
function warn(msg) {
console.log(
'%cWarn: %c' + msg,
'color:#FF6600;font-weight:bold',
'color: #FF9933'
);
}
function tips(msg) {
console.log(
'%cTips: %c' + msg,
'color:#00B200;font-weight:bold',
'color: #00CC33'
);
}
const conf = {
/**
* 计算指定月份共多少天
* @param {number} year 年份
* @param {number} month 月份
*/
getThisMonthDays(year, month) {
return new Date(year, month, 0).getDate();
},
/**
* 计算指定月份第一天星期几
* @param {number} year 年份
* @param {number} month 月份
*/
getFirstDayOfWeek(year, month) {
return new Date(Date.UTC(year, month - 1, 1)).getDay();
},
/**
* 计算指定日期星期几
* @param {number} year 年份
* @param {number} month 月份
* @param {number} date 日期
*/
getDayOfWeek(year, month, date) {
return new Date(Date.UTC(year, month - 1, date)).getDay();
},
/**
* 渲染日历
* @param {number} curYear
* @param {number} curMonth
* @param {number} curDate
*/
renderCalendar(curYear, curMonth, curDate) {
conf.calculateEmptyGrids.call(this, curYear, curMonth);
conf.calculateDays.call(this, curYear, curMonth, curDate);
const { todoLabels } = this.data.calendar || {};
const { afterCalendarRender } = this.config;
if (todoLabels && todoLabels instanceof Array) {
conf.setTodoLabels.call(this);
}
if (
afterCalendarRender &&
typeof afterCalendarRender === 'function' &&
!this.firstRender
) {
afterCalendarRender(this);
this.firstRender = true;
}
},
/**
* 计算当前月份前后两月应占的格子
* @param {number} year 年份
* @param {number} month 月份
*/
calculateEmptyGrids(year, month) {
conf.calculatePrevMonthGrids.call(this, year, month);
conf.calculateNextMonthGrids.call(this, year, month);
},
/**
* 计算上月应占的格子
* @param {number} year 年份
* @param {number} month 月份
*/
calculatePrevMonthGrids(year, month) {
let empytGrids = [];
const prevMonthDays = conf.getThisMonthDays(year, month - 1);
const firstDayOfWeek = conf.getFirstDayOfWeek(year, month);
if (firstDayOfWeek > 0) {
const len = prevMonthDays - firstDayOfWeek;
for (let i = prevMonthDays; i > len; i--) {
empytGrids.push(i);
}
this.setData({
'calendar.empytGrids': empytGrids.reverse()
});
} else {
this.setData({
'calendar.empytGrids': null
});
}
},
/**
* 计算下月应占的格子
* @param {number} year 年份
* @param {number} month 月份
*/
calculateNextMonthGrids(year, month) {
let lastEmptyGrids = [];
const thisMonthDays = conf.getThisMonthDays(year, month);
const lastDayWeek = conf.getDayOfWeek(year, month, thisMonthDays);
if (+lastDayWeek !== 6) {
const len = 7 - (lastDayWeek + 1);
for (let i = 1; i <= len; i++) {
lastEmptyGrids.push(i);
}
this.setData({
'calendar.lastEmptyGrids': lastEmptyGrids
});
} else {
this.setData({
'calendar.lastEmptyGrids': null
});
}
},
/**
* 设置日历面板数据
* @param {number} year 年份
* @param {number} month 月份
*/
calculateDays(year, month, curDate) {
let days = [];
const {
todayTimestamp,
disableDays = [],
enableArea = [],
enableDays = [],
enableAreaTimestamp = []
} = this.data.calendar;
const thisMonthDays = conf.getThisMonthDays(year, month);
let expectEnableDaysTimestamp = converEnableDaysToTimestamp(enableDays);
if (enableArea.length) {
expectEnableDaysTimestamp = delRepeatedEnableDay(enableDays, enableArea);
}
let selectedDay = [];
if (this.config.noDefault) {
selectedDay = [];
this.config.noDefault = false;
} else {
selectedDay = curDate
? [
{
day: curDate,
choosed: true,
year,
month
}
]
: this.data.calendar.selectedDay;
}
for (let i = 1; i <= thisMonthDays; i++) {
days.push({
day: i,
choosed: false,
year,
month
});
}
const selectedDayCol = selectedDay.map(
d => `${d.year}-${d.month}-${d.day}`
);
const disableDaysCol = disableDays.map(
d => `${d.year}-${d.month}-${d.day}`
);
days.forEach(item => {
const cur = `${item.year}-${item.month}-${item.day}`;
if (selectedDayCol.indexOf(cur) !== -1) item.choosed = true;
if (disableDaysCol.indexOf(cur) !== -1) item.disable = true;
const timestamp = newDate(item.year, item.month, item.day).getTime();
let setDisable = false;
if (enableAreaTimestamp.length) {
if (
(+enableAreaTimestamp[0] > +timestamp ||
+timestamp > +enableAreaTimestamp[1]) &&
!expectEnableDaysTimestamp.includes(+timestamp)
) {
setDisable = true;
}
} else if (
expectEnableDaysTimestamp.length &&
!expectEnableDaysTimestamp.includes(+timestamp)
) {
setDisable = true;
}
if (setDisable) {
item.disable = true;
item.choosed = false;
}
if (
this.config.disablePastDay &&
timestamp - todayTimestamp < 0 &&
!item.disable
) {
item.disable = true;
}
});
const tmp = { 'calendar.days': days };
if (curDate) {
tmp['calendar.selectedDay'] = selectedDay;
}
this.setData(tmp);
},
/**
* 当改变月份时触发
* @param {object} param
*/
whenChangeMonth({ curYear, curMonth, newYear, newMonth }) {
const { whenChangeMonth } = this.config || {};
if (typeof whenChangeMonth === 'function') {
whenChangeMonth(
{
year: curYear,
month: curMonth
},
{
year: newYear,
month: newMonth
}
);
}
},
/**
* 点击日期后触发事件
* @param {object} currentSelected 当前选择的日期
* @param {array} selectedDays 多选状态下选中的日期
*/
afterTapDay(currentSelected, selectedDays) {
const config = this.config;
const { multi, afterTapDay } = config;
if (afterTapDay && typeof afterTapDay === 'function') {
if (!multi) {
config.afterTapDay(currentSelected);
} else {
config.afterTapDay(currentSelected, selectedDays);
}
}
},
/**
* 多选
* @param {object} opts
*/
whenMulitSelect(opts = {}) {
let { currentSelected, selectedDays } = opts;
const { days, idx, onTapDay, e } = opts;
days[idx].choosed = !days[idx].choosed;
if (!days[idx].choosed) {
days[idx].cancel = true; // 该次点击是否为取消日期操作
currentSelected = days[idx];
selectedDays = selectedDays.filter(
item =>
`${item.year}-${item.month}-${item.day}` !==
`${currentSelected.year}-${currentSelected.month}-${
currentSelected.day
}`
);
} else {
currentSelected = days[idx];
selectedDays.push(currentSelected);
}
if (onTapDay && typeof onTapDay === 'function') {
return this.config.onTapDay(currentSelected, e);
}
this.setData({
'calendar.days': days,
'calendar.selectedDay': selectedDays
});
conf.afterTapDay.call(this, currentSelected, selectedDays);
},
/**
* 单选
* @param {object} opts
*/
whenSingleSelect(opts = {}) {
let { currentSelected, selectedDays = [] } = opts;
let shouldMarkerTodoDay = [];
const { days, idx, onTapDay, e } = opts;
const { month: sMonth, year: sYear } = selectedDays[0] || {};
const { month: dMonth, year: dYear } = days[0] || {};
const { calendar = {} } = this.data;
if (sMonth === dMonth && sYear === dYear && !this.weekMode) {
days[selectedDays[0].day - 1].choosed = false;
}
if (this.weekMode) {
days.forEach((item, idx) => {
if (item.day === selectedDays[0].day) days[idx].choosed = false;
});
}
if (calendar.todoLabels) {
// 过滤所有待办日期中当月有待办事项的日期
shouldMarkerTodoDay = calendar.todoLabels.filter(
item => +item.year === dYear && +item.month === dMonth
);
}
shouldMarkerTodoDay.forEach(item => {
// hasTodo 是否有待办事项
if (this.weekMode) {
days.forEach((_item, idx) => {
if (+_item.day === +item.day) {
const day = days[idx];
day.hasTodo = true;
day.todoText = item.todoText;
if (
selectedDays &&
selectedDays.length &&
+selectedDays[0].day === +item.day
) {
day.showTodoLabel = true;
}
}
});
} else {
const day = days[item.day - 1];
day.hasTodo = true;
day.todoText = item.todoText;
if (
selectedDays &&
selectedDays.length &&
+selectedDays[0].day === +item.day
) {
// showTodoLabel 是否显示待办标记
days[selectedDays[0].day - 1].showTodoLabel = true;
}
}
});
const currentDay = days[idx];
if (currentDay.showTodoLabel) currentDay.showTodoLabel = false;
currentDay.choosed = true;
currentSelected = currentDay;
if (onTapDay && typeof onTapDay === 'function') {
return this.config.onTapDay(currentSelected, e);
}
this.setData({
'calendar.days': days,
'calendar.selectedDay': [currentSelected]
});
conf.afterTapDay.call(this, currentSelected);
},
/**
* 设置代办事项标志
* @param {object} options 代办事项配置
*/
setTodoLabels(options = {}) {
const { calendar } = this.data;
if (!calendar || !calendar.days) {
return warn('请等待日历初始化完成后再调用该方法');
}
const days = calendar.days.slice();
const { curYear, curMonth } = calendar;
const { days: todoDays = [], pos = 'bottom', dotColor = '' } = options;
const { todoLabels = [], todoLabelPos, todoLabelColor } = calendar;
const shouldMarkerTodoDay = todoDays.filter(
item => +item.year === curYear && +item.month === curMonth
);
let currentMonthTodoLabels = todoLabels.filter(
item => +item.year === curYear && +item.month === curMonth
);
shouldMarkerTodoDay.concat(currentMonthTodoLabels).forEach(item => {
let target = {};
if (this.weekMode) {
target = days.find(d => +d.day === +item.day);
} else {
target = days[item.day - 1];
}
if (target) target.showTodoLabel = !target.choosed;
if (target.showTodoLabel && item.todoText)
target.todoText = item.todoText;
});
const o = {
'calendar.days': days,
'calendar.todoLabels': uniqueTodoLabels(todoDays.concat(todoLabels))
};
if (pos && pos !== todoLabelPos) o['calendar.todoLabelPos'] = pos;
if (dotColor && dotColor !== todoLabelColor) {
o['calendar.todoLabelColor'] = dotColor;
}
this.setData(o);
},
/**
* 筛选待办事项
* @param {array} todos 需要删除待办标记的日期
*/
filterTodos(todos) {
const { todoLabels } = this.data.calendar;
const deleteTodo = todos.map(
item => `${item.year}-${item.month}-${item.day}`
);
return todoLabels.filter(
item =>
deleteTodo.indexOf(`${item.year}-${item.month}-${item.day}`) === -1
);
},
/**
* 删除指定日期的待办标识
* @param {array} todos 需要删除待办标记的日期
*/
deleteTodoLabels(todos) {
if (!(todos instanceof Array) || !todos.length) return;
const todoLabels = conf.filterTodos.call(this, todos);
const { days, curYear, curMonth } = this.data.calendar;
const currentMonthTodoLabels = todoLabels.filter(
item => curYear === +item.year && curMonth === +item.month
);
days.forEach(item => {
item.showTodoLabel = false;
});
currentMonthTodoLabels.forEach(item => {
days[item.day - 1].showTodoLabel = !days[item.day - 1].choosed;
});
this.setData({
'calendar.days': days,
'calendar.todoLabels': todoLabels
});
},
/**
* 清空所有日期的待办标识
*/
clearTodoLabels() {
const { days = [] } = this.data.calendar;
const _days = [].concat(days);
_days.forEach(item => {
item.showTodoLabel = false;
});
this.setData({
'calendar.days': _days,
'calendar.todoLabels': []
});
},
/**
* 跳转至今天
*/
jumpToToday() {
const date = new Date();
const curYear = date.getFullYear();
const curMonth = date.getMonth() + 1;
const curDate = date.getDate();
const timestamp = newDate(curYear, curMonth, curDate).getTime();
this.setData({
'calendar.curYear': curYear,
'calendar.curMonth': curMonth,
'calendar.selectedDay': [
{
year: curYear,
day: curDate,
month: curMonth,
choosed: true
}
],
'calendar.todayTimestamp': timestamp
});
conf.renderCalendar.call(this, curYear, curMonth, curDate);
},
/**
* 更新当前年月
*/
updateCurrYearAndMonth(type) {
let { days, curYear, curMonth } = this.data.calendar;
const { month: firstMonth } = days[0];
const { month: lastMonth } = days[days.length - 1];
const lastDayOfThisMonth = conf.getThisMonthDays(curYear, curMonth);
const lastDayOfThisWeek = days[days.length - 1];
const firstDayOfThisWeek = days[0];
if (
(lastDayOfThisWeek.day + 7 > lastDayOfThisMonth ||
(curMonth === firstMonth && firstMonth !== lastMonth)) &&
type === 'next'
) {
curMonth = curMonth + 1;
if (curMonth > 12) {
curYear = curYear + 1;
curMonth = 1;
}
} else if (
(+firstDayOfThisWeek.day <= 7 ||
(curMonth === lastMonth && firstMonth !== lastMonth)) &&
type === 'prev'
) {
curMonth = curMonth - 1;
if (curMonth <= 0) {
curYear = curYear - 1;
curMonth = 12;
}
}
return {
Uyear: curYear,
Umonth: curMonth
};
},
/**
* 计算周视图下当前这一周和当月的最后一天
*/
calculateLastDay() {
const { days, curYear, curMonth } = this.data.calendar;
const lastDayInThisWeek = days[days.length - 1].day;
const lastDayInThisMonth = conf.getThisMonthDays(curYear, curMonth);
return { lastDayInThisWeek, lastDayInThisMonth };
},
/**
* 计算周视图下当前这一周第一天
*/
calculateFirstDay() {
const { days } = this.data.calendar;
const firstDayInThisWeek = days[0].day;
return { firstDayInThisWeek };
},
/**
* 当月第一周所有日期范围
* @param {number} year
* @param {number} month
*/
firstWeekInMonth(year, month) {
const firstDay = conf.getDayOfWeek(year, month, 1);
const firstWeekDays = [1, 1 + (6 - firstDay)];
const { days } = this.data.calendar;
const daysCut = days.slice(firstWeekDays[0] - 1, firstWeekDays[1]);
return daysCut;
},
/**
* 当月最后一周所有日期范围
* @param {number} year
* @param {number} month
*/
lastWeekInMonth(year, month) {
const lastDay = conf.getThisMonthDays(year, month);
const lastDayWeek = conf.getDayOfWeek(year, month, lastDay);
const lastWeekDays = [lastDay - lastDayWeek, lastDay];
const { days } = this.data.calendar;
const daysCut = days.slice(lastWeekDays[0] - 1, lastWeekDays[1]);
return daysCut;
},
/**
* 渲染日期之前初始化已选日期
* @param {array} days 当前日期数组
*/
initSelectedDay(days) {
const daysCopy = days.slice();
const { selectedDay = [], todoLabels = [] } = this.data.calendar;
const selectedDayStr = selectedDay.map(
item => `${item.year}+${item.month}+${item.day}`
);
const todoLabelsCol = todoLabels.map(d => `${d.year}-${d.month}-${d.day}`);
daysCopy.forEach(item => {
if (
selectedDayStr.indexOf(`${item.year}+${item.month}+${item.day}`) !== -1
) {
item.choosed = true;
} else {
item.choosed = false;
}
const idx = todoLabelsCol.indexOf(
`${item.year}-${item.month}-${item.day}`
);
if (idx !== -1) {
item.showTodoLabel = !item.choosed;
if (item.showTodoLabel && todoLabels[idx].todoText)
item.todoText = todoLabels[idx].todoText;
}
});
return daysCopy;
},
/**
* 周视图下设置可选日期范围
* @param {object} days 当前展示的日期
*/
setEnableAreaOnWeekMode(days) {
let {
todayTimestamp,
enableAreaTimestamp = [],
enableDaysTimestamp = []
} = this.data.calendar;
days.forEach(item => {
const timestamp = newDate(item.year, item.month, item.day).getTime();
let setDisable = false;
if (enableAreaTimestamp.length) {
if (
(+enableAreaTimestamp[0] > +timestamp ||
+timestamp > +enableAreaTimestamp[1]) &&
!enableDaysTimestamp.includes(+timestamp)
) {
setDisable = true;
}
} else if (
enableDaysTimestamp.length &&
!enableDaysTimestamp.includes(+timestamp)
) {
setDisable = true;
}
if (setDisable) {
item.disable = true;
item.choosed = false;
}
if (
this.config.disablePastDay &&
timestamp - todayTimestamp < 0 &&
!item.disable
) {
item.disable = true;
}
});
},
/**
* 计算下一周的日期
*/
calculateNextWeekDays() {
let { lastDayInThisWeek, lastDayInThisMonth } = conf.calculateLastDay.call(
this
);
let { curYear, curMonth } = this.data.calendar;
let days = [];
if (lastDayInThisMonth - lastDayInThisWeek >= 7) {
const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'next');
curYear = Uyear;
curMonth = Umonth;
for (let i = lastDayInThisWeek + 1; i <= lastDayInThisWeek + 7; i++) {
days.push({
year: curYear,
month: curMonth,
day: i
});
}
} else {
for (let i = lastDayInThisWeek + 1; i <= lastDayInThisMonth; i++) {
days.push({
year: curYear,
month: curMonth,
day: i
});
}
const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'next');
curYear = Uyear;
curMonth = Umonth;
for (let i = 1; i <= 7 - (lastDayInThisMonth - lastDayInThisWeek); i++) {
days.push({
year: curYear,
month: curMonth,
day: i
});
}
}
days = conf.initSelectedDay.call(this, days);
conf.setEnableAreaOnWeekMode.call(this, days);
this.setData({
'calendar.curYear': curYear,
'calendar.curMonth': curMonth,
'calendar.days': days
});
},
/**
* 计算上一周的日期
*/
calculatePrevWeekDays() {
let { firstDayInThisWeek } = conf.calculateFirstDay.call(this);
let { curYear, curMonth } = this.data.calendar;
let days = [];
if (firstDayInThisWeek - 7 > 0) {
const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'prev');
curYear = Uyear;
curMonth = Umonth;
for (let i = firstDayInThisWeek - 7; i < firstDayInThisWeek; i++) {
days.push({
year: curYear,
month: curMonth,
day: i
});
}
} else {
let temp = [];
for (let i = 1; i < firstDayInThisWeek; i++) {
temp.push({
year: curYear,
month: curMonth,
day: i
});
}
const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'prev');
curYear = Uyear;
curMonth = Umonth;
const prevMonthDays = conf.getThisMonthDays(curYear, curMonth);
for (
let i = prevMonthDays - Math.abs(firstDayInThisWeek - 7);
i <= prevMonthDays;
i++
) {
days.push({
year: curYear,
month: curMonth,
day: i
});
}
days = days.concat(temp);
}
days = conf.initSelectedDay.call(this, days);
conf.setEnableAreaOnWeekMode.call(this, days);
this.setData({
'calendar.curYear': curYear,
'calendar.curMonth': curMonth,
'calendar.days': days
});
},
/**
* 计算当前选中日期所在周,并重新渲染日历
* @param {object} currentDay 当前选择日期
*/
selectedDayWeekAllDays(currentDay) {
let { days, curYear, curMonth } = this.data.calendar;
let { year, month, day } = currentDay;
let lastWeekDays = conf.lastWeekInMonth.call(this, year, month);
let empytGrids = [];
let lastEmptyGrids = [];
const firstWeekDays = conf.firstWeekInMonth.call(this, year, month);
// 判断选中日期的月份是否与当前月份一致
if (curYear !== year || curMonth !== month) day = 1;
if (curYear !== year) year = curYear;
if (curMonth !== month) month = curMonth;
if (firstWeekDays.find(item => item.day === day)) {
// 当前选择的日期为该月第一周
let temp = [];
const lastDayInThisMonth = conf.getThisMonthDays(year, month - 1);
const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'prev');
curYear = Uyear;
curMonth = Umonth;
for (
let i = lastDayInThisMonth - (7 - firstWeekDays.length) + 1;
i <= lastDayInThisMonth;
i++
) {
temp.push({
year: curYear,
month: curMonth,
day: i
});
}
days = temp.concat(firstWeekDays);
} else if (lastWeekDays.find(item => item.day === day)) {
// 当前选择的日期为该月最后一周
const temp = [];
if (lastWeekDays && lastWeekDays.length < 7) {
const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(
this,
'next'
);
curYear = Uyear;
curMonth = Umonth;
for (let i = 1, len = 7 - lastWeekDays.length; i <= len; i++) {
temp.push({
year: curYear,
month: curMonth,
day: i
});
}
}
days = lastWeekDays.concat(temp);
} else {
const week = conf.getDayOfWeek(year, month, day);
const range = [day - week, day + (6 - week)];
days = days.slice(range[0] - 1, range[1]);
}
days = conf.initSelectedDay.call(this, days);
this.setData({
'calendar.days': days,
'calendar.empytGrids': empytGrids,
'calendar.lastEmptyGrids': lastEmptyGrids
});
},
/**
* 周、月视图切换
* @param {string} view 视图 [week, month]
*/
switchWeek(view) {
if (this.config.multi) return warn('多选模式不能切换周月视图');
const { selectedDay = [], curYear, curMonth } = this.data.calendar;
if (!selectedDay.length) return;
const currentDay = selectedDay[0];
if (view === 'week') {
if (this.weekMode) return;
this.weekMode = true;
conf.selectedDayWeekAllDays.call(this, currentDay);
} else {
this.weekMode = false;
let { year, month, day } = currentDay;
if (curYear !== year || curMonth !== month) day = 1;
conf.renderCalendar.call(this, curYear, curMonth, day);
}
},
/**
* 禁用指定日期
* @param {array} days 禁用
*/
disableDays(data) {
const { disableDays = [], days } = this.data.calendar;
if (Object.prototype.toString.call(data) !== '[object Array]') {
return warn('disableDays 参数为数组');
}
const _disableDays = data.concat(disableDays);
const disableDaysCol = _disableDays.map(
d => `${d.year}-${d.month}-${d.day}`
);
days.forEach(item => {
const cur = `${item.year}-${item.month}-${item.day}`;
if (disableDaysCol.indexOf(cur) !== -1) item.disable = true;
});
this.setData({
'calendar.days': days,
'calendar.disableDays': _disableDays
});
}
};
export const whenChangeMonth = conf.whenChangeMonth;
export const renderCalendar = conf.renderCalendar;
export const whenSingleSelect = conf.whenSingleSelect;
export const whenMulitSelect = conf.whenMulitSelect;
export const calculatePrevWeekDays = conf.calculatePrevWeekDays;
export const calculateNextWeekDays = conf.calculateNextWeekDays;
/**
* 获取已选择的日期
*/
export const getSelectedDay = () => {
return currentPage && currentPage.data.calendar.selectedDay;
};
/**
* 跳转至指定日期
*/
export const jump = (year, month, day) => {
const self = currentPage;
const { selectedDay } = self.data.calendar;
if (
selectedDay && selectedDay[0]
+selectedDay[0].year === +year &&
+selectedDay[0].month === +month &&
+selectedDay[0].day === +day
) {
return;
}
if (year && month) {
if (typeof +year !== 'number' || typeof +month !== 'number') {
return warn('jump 函数年月日参数必须为数字');
}
let tmp = {
'calendar.curYear': year,
'calendar.curMonth': month
};
self.setData(tmp, () => {
if (typeof +day === 'number') {
return conf.renderCalendar.call(self, year, month, day);
}
conf.renderCalendar.call(self, year, month);
});
return;
}
conf.jumpToToday.call(self);
};
/**
* 设置代办事项日期标记
* @param {object} todos 待办事项配置
* @param {string} [todos.pos] 标记显示位置,默认值'bottom' ['bottom', 'top']
* @param {string} [todos.dotColor] 标记点颜色,backgroundColor 支持的值都行
* @param {object[]} todos.days 需要标记的所有日期,如:[{year: 2015, month: 5, day: 12}],其中年月日字段必填
*/
export const setTodoLabels = todos => {
conf.setTodoLabels.call(currentPage, todos);
};
/**
* 删除指定日期待办标记
* @param {array} todos 需要删除的待办日期数组
*/
export const deleteTodoLabels = todos => {
conf.deleteTodoLabels.call(currentPage, todos);
};
/**
* 清空所有待办标记
*/
export const clearTodoLabels = () => {
conf.clearTodoLabels.call(currentPage);
};
/**
* 切换周月视图
* @param {string} view 视图模式[week, month]
*/
export const switchView = view => {
conf.switchWeek.call(currentPage, view);
};
/**
* 禁用指定日期
* @param {array} days 日期
* @param {number} [days.year]
* @param {number} [days.month]
* @param {number} [days.day]
*/
export const disableDay = (days = []) => {
conf.disableDays.call(currentPage, days);
};
/**
* 指定可选日期及可选日期数组去重
* @param {array} enableDays 特定可选日期数组
* @param {array} enableArea 可选日期区域数组
*/
function delRepeatedEnableDay(enableDays = [], enableArea = []) {
let _startTimestamp;
let _endTimestamp;
if (enableArea.length === 2) {
const { startTimestamp, endTimestamp } = convertEnableAreaToTimestamp(
enableArea
);
_startTimestamp = startTimestamp;
_endTimestamp = endTimestamp;
}
const enableDaysTimestamp = converEnableDaysToTimestamp(enableDays);
const tmp = enableDaysTimestamp.filter(
item => item < _startTimestamp || item > _endTimestamp
);
return tmp;
}
/**
* 指定日期区域转时间戳
* @param {array} timearea 时间区域
*/
function convertEnableAreaToTimestamp(timearea) {
if (!timearea || !timearea.length) return;
const start = timearea[0].split('-');
const end = timearea[1].split('-');
const startTimestamp = newDate(start[0], start[1], start[2]).getTime();
const endTimestamp = newDate(end[0], end[1], end[2]).getTime();
return {
start,
end,
startTimestamp,
endTimestamp
};
}
/**
* 指定特定日期数组转时间戳
* @param {array} enableDays 指定时间数组
*/
function converEnableDaysToTimestamp(enableDays = []) {
const enableDaysTimestamp = [];
enableDays.forEach(item => {
if (typeof item !== 'string') return warn('enableDays()入参日期格式错误');
const tmp = item.split('-');
if (tmp.length !== 3) return warn('enableDays()入参日期格式错误');
const timestamp = newDate(tmp[0], tmp[1], tmp[2]).getTime();
enableDaysTimestamp.push(timestamp);
});
return enableDaysTimestamp;
}
/**
* 指定可选日期范围
* @param {array} area 日期访问数组
*/
export const enableArea = (area = []) => {
const self = currentPage;
const { enableDays = [] } = self.data.calendar;
let expectEnableDaysTimestamp = [];
if (enableDays.length) {
expectEnableDaysTimestamp = delRepeatedEnableDay(enableDays, area);
}
if (area.length === 2) {
const {
start,
end,
startTimestamp,
endTimestamp
} = convertEnableAreaToTimestamp(area);
const startMonthDays = conf.getThisMonthDays(start[0], start[1]);
const endMonthDays = conf.getThisMonthDays(end[0], end[1]);
if (start[2] > startMonthDays || start[2] < 1) {
return warn('enableArea() 开始日期错误,指定日期不在当前月份天数范围内');
}
if (start[1] > 12 || start[1] < 1) {
return warn('enableArea() 开始日期错误,月份超出1-12月份');
}
if (end[2] > endMonthDays || end[2] < 1) {
return warn('enableArea() 截止日期错误,指定日期不在当前月份天数范围内');
}
if (end[1] > 12 || end[1] < 1) {
return warn('enableArea() 截止日期错误,月份超出1-12月份');
}
if (startTimestamp > endTimestamp) {
warn('enableArea()参数最小日期大于了最大日期');
} else {
let { days = [], selectedDay = [] } = self.data.calendar;
const daysCopy = days.slice();
daysCopy.forEach(item => {
const timestamp = newDate(item.year, item.month, item.day).getTime();
if (
(+startTimestamp > +timestamp || +timestamp > +endTimestamp) &&
!expectEnableDaysTimestamp.includes(+timestamp)
) {
item.disable = true;
if (item.choosed) {
item.choosed = false;
selectedDay = selectedDay.filter(
d =>
`${item.year}-${item.month}-${item.day}` !==
`${d.year}-${d.month}-${d.day}`
);
}
} else if (item.disable) {
item.disable = false;
}
});
self.setData({
'calendar.days': daysCopy,
'calendar.selectedDay': selectedDay,
'calendar.enableArea': area,
'calendar.enableAreaTimestamp': [startTimestamp, endTimestamp]
});
}
} else {
warn('enableArea()参数需为时间范围数组,形如:["2018-8-4" , "2018-8-24"]');
}
};
/**
* 指定特定日期可选
* @param {array} days 指定日期数组
*/
export function enableDays(days = []) {
const self = currentPage;
const { enableArea = [], enableAreaTimestamp = [] } = self.data.calendar;
let expectEnableDaysTimestamp = [];
if (enableArea.length) {
expectEnableDaysTimestamp = delRepeatedEnableDay(days, enableArea);
} else {
expectEnableDaysTimestamp = converEnableDaysToTimestamp(days);
}
let { days: allDays = [], selectedDay = [] } = self.data.calendar;
const daysCopy = allDays.slice();
daysCopy.forEach(item => {
const timestamp = newDate(item.year, item.month, item.day).getTime();
let setDisable = false;
if (enableAreaTimestamp.length) {
if (
(+enableAreaTimestamp[0] > +timestamp ||
+timestamp > +enableAreaTimestamp[1]) &&
!expectEnableDaysTimestamp.includes(+timestamp)
) {
setDisable = true;
}
} else if (!expectEnableDaysTimestamp.includes(+timestamp)) {
setDisable = true;
}
if (setDisable) {
item.disable = true;
if (item.choosed) {
item.choosed = false;
selectedDay = selectedDay.filter(
d =>
`${item.year}-${item.month}-${item.day}` !==
`${d.year}-${d.month}-${d.day}`
);
}
} else {
item.disable = false;
}
});
self.setData({
'calendar.days': daysCopy,
'calendar.selectedDay': selectedDay,
'calendar.enableDays': days,
'calendar.enableDaysTimestamp': expectEnableDaysTimestamp
});
}
export default (config = {}) => {
tips(
'使用中若遇问题请反馈至 https://github.com/treadpit/wx_calendar/issues ✍️'
);
const weeksCh = ['日', '一', '二', '三', '四', '五', '六'];
currentPage = getCurrentPage();
currentPage.config = config;
currentPage.setData({
'calendar.weeksCh': weeksCh
});
if (config.defaultDay && typeof config.defaultDay === 'string') {
const day = config.defaultDay.split('-');
if (day.length < 3) {
return warn('配置 jumpTo 格式应为: 2018-4-2 或 2018-04-02');
}
jump(+day[0], +day[1], +day[2]);
} else {
conf.jumpToToday.call(currentPage);
}
};
2. 在需要使用的界面
wxml
<calendar calendar="{{calendar}}" gesture="{{gesture}}"></calendar>
js
import initCalendar from '../../component/calendar/main.js';
const conf = {
onShow: function() {
initCalendar();
}
};
Page(conf);
效果图