基于Taro-UI封装的日历组件
【代码】基于Taro-UI封装的日历组件
·
默认显示当前这周的日历
点击左上角日期后,进行周-月视图的切换
当有日期需要标记时,右上角显示,选择的日期显示蓝色,默认当日日期显示绿色
组件结构目录:
// index.jsx
import {Picker, Swiper, SwiperItem, View} from "@tarojs/components";
import {Component} from "react";
import {formatDate, getWeekDayList} from "./utils";
import Days from "./days/index";
import "./index.scss";
class Calendar extends Component {
state = {
current: formatDate(new Date(this.props.currentView)),
selectedDate: this.props.selectedDate,
currentCarouselIndex: 1,
selectedRange: { start: "", end: "" },
};
componentWillMount() {
if (this.props.bindRef) {
this.props.bindRef(this);
}
}
componentWillReceiveProps(nextProps) {
if (
nextProps.selectedDate &&
nextProps.selectedDate !== this.props.selectedDate
) {
this.setState({
selectedDate: nextProps.selectedDate,
current: nextProps.selectedDate,
});
}
if (
nextProps.currentView &&
nextProps.currentView !== this.props.currentView
) {
this.setState({ current: nextProps.currentView });
}
}
getPickerText = () => {
const { current } = this.state;
const currentDateObj = new Date(current);
return formatDate(currentDateObj, "month")
};
onClickDate = (value) => {
console.log(value)
const { onDayClick, onSelectDate } = this.props;
let { current, currentCarouselIndex, selectedRange } = this.state;
if (!selectedRange.start || selectedRange.end) {
selectedRange = { start: value.fullDateStr, end: "" };
} else {
if (new Date(selectedRange.start) > new Date(value.fullDateStr)) {
selectedRange = {
start: value.fullDateStr,
end: selectedRange.start,
};
} else {
selectedRange.end = value.fullDateStr;
}
}
this.setState({
selectedDate: value.fullDateStr,
selectedRange,
currentCarouselIndex,
current,
});
if (onDayClick) {
onDayClick({ value: value.fullDateStr });
}
if (onSelectDate) {
onSelectDate(selectedRange);
}
};
goNext = () => {
console.log("下个月");
const { view } = this.props;
const { currentCarouselIndex } = this.state;
let dateObj = new Date(this.state.current);
const { onClickNext, onCurrentViewChange } = this.props;
let current = "";
if (view === "month") {
dateObj.setMonth(dateObj.getMonth() + 1);
const nextMonth = formatDate(dateObj);
current = nextMonth;
}
if (view === "week") {
dateObj.setDate(dateObj.getDate() + 7);
const nextWeek = formatDate(dateObj, "day");
current = nextWeek;
}
if (onClickNext) onClickNext();
if (onCurrentViewChange) onCurrentViewChange(current);
this.setState({
currentCarouselIndex: (currentCarouselIndex + 1) % 3,
current,
});
};
goPre = () => {
console.log("上个月");
const { view } = this.props;
const { currentCarouselIndex } = this.state;
let dateObj = new Date(this.state.current);
let current = "";
if (view === "month") {
dateObj.setMonth(dateObj.getMonth() - 1);
const preMonth = formatDate(dateObj);
current = preMonth;
}
if (view === "week") {
dateObj.setDate(dateObj.getDate() - 7);
const preWeek = formatDate(dateObj, "day");
current = preWeek;
}
const { onClickPre, onCurrentViewChange } = this.props;
if (onClickPre) onClickPre();
if (onCurrentViewChange) onCurrentViewChange(current);
this.setState({
currentCarouselIndex: (currentCarouselIndex + 2) % 3,
current,
});
};
render() {
const {
current,
selectedDate,
currentCarouselIndex,
selectedRange,
} = this.state;
const {
marks,
isVertical,
selectedDateColor,
isSwiper,
minDate,
maxDate,
onDayLongPress,
showDivider,
isMultiSelect,
customStyleGenerator,
headStyle,
headCellStyle,
bodyStyle,
view,
startDay,
extraInfo,
} = this.props;
// 配合Swiper组件实现无限滚动
// 原理:永远保持当前屏幕显示月份的左边是前一个月,右边是后一个月
// current即当前月份,currentCarouselIndex即当前显示页面的index。一共3个页面,index分别为0 1 2 。
// Swiper的无限循环就是类似0 1 2 0 1 2 这样。如果currentCarouselIndex是2 那么我只要保证 1显示的是前面一个月,0显示的是后面一个月 就完成了循环。
let currentDate = new Date(current);
let preDate = new Date(current);
let nextDate = new Date(current);
if (view === "month") {
preDate.setMonth(currentDate.getMonth() - 1);
nextDate.setMonth(currentDate.getMonth() + 1);
}
if (view === "week") {
preDate.setDate(currentDate.getDate() - 7);
nextDate.setDate(currentDate.getDate() + 7);
}
const preIndex = (currentCarouselIndex + 2) % 3;
const nextIndex = (currentCarouselIndex + 1) % 3;
let monthObj = [];
monthObj[currentCarouselIndex] = currentDate;
monthObj[preIndex] = preDate;
monthObj[nextIndex] = nextDate;
// 所有Days组件的公共Props
const publicDaysProp = {
marks: marks || [],
onClick: this.onClickDate,
selectedDate,
minDate: minDate,
maxDate,
selectedDateColor,
onDayLongPress,
showDivider: showDivider,
isMultiSelect: isMultiSelect,
selectedRange: selectedRange,
customStyleGenerator,
view: view,
startDay: startDay,
extraInfo: extraInfo || [],
};
console.log(this.getPickerText())
return (
<View className='calendar'>
<View className='calendar-head' style={headStyle}>
{getWeekDayList(startDay).map((value) => (
<View style={headCellStyle} key={value}>
{value}
</View>
))}
</View>
{isSwiper ? (
<Swiper
style={{
height: view === "month" ? "14rem" : "3rem",
...bodyStyle,
}}
vertical={isVertical}
circular
current={currentCarouselIndex}
onChange={(e) => {
if (e.detail.source === "touch") {
const currentIndex = e.detail.current;
if ((currentCarouselIndex + 1) % 3 === currentIndex) {
// 当前月份+1
this.goNext();
} else {
// 当前月份-1
this.goPre();
}
}
}}
className='calendar-swiper'
>
<SwiperItem style='position: absolute; width: 100%; height: 100%;'>
<Days date={monthObj[0]} {...publicDaysProp} />
</SwiperItem>
<SwiperItem style='position: absolute; width: 100%; height: 100%;'>
<Days date={monthObj[1]} {...publicDaysProp} />
</SwiperItem>
<SwiperItem style='position: absolute; width: 100%; height: 100%;'>
<Days date={monthObj[2]} {...publicDaysProp} />
</SwiperItem>
</Swiper>
) : (
<Days bodyStyle={bodyStyle} date={currentDate} {...publicDaysProp} />
)}
</View>
);
}
}
Calendar.defaultProps = {
isVertical: false,
marks: [],
selectedDate: formatDate(new Date(), "day"),
selectedDateColor: "#90b1ef",
hideArrow: false,
isSwiper: true,
minDate: "1970-01-01",
maxDate: "2100-12-31",
showDivider: false,
isMultiSelect: false,
view: "month",
currentView: formatDate(new Date()),
startDay: 0,
extraInfo: [],
};
export default Calendar;
// index.scss
.calendar {
width:100%;
.calendar-head > view {
height: 2rem;
display: inline-block;
width: 14.28%;
text-align: center;
line-height: 2rem;
color: #bfbfd1;
}
.calendar-head {
font-size: 1rem;
}
.calendar-picker {
position: relative;
left: 50%;
transform: translateX(-50%);
width: fit-content;
}
.calendar-arrow-right,
.calendar-arrow-left {
height: 1rem;
width: 1rem;
display: inline-block;
background-repeat: no-repeat;
background-size: 100% 100%;
position: relative;
top: 0.2rem;
opacity: 0.3;
}
.calendar-arrow-left {
right: 0.5rem;
}
.calendar-arrow-right {
left: 0.5rem;
}
.calendar-disabled-arrow {
display: none;
}
}
// utils.js
/** 填充0 */
export const fillWithZero = (target, length) => {
return (Array(length).join('0') + target).slice(-length);
};
/**
* 默认将日期格式化为 YYYY-MM-DD
* @param date Date类型的时间
* @param field 时间显示粒度
*/
export const formatDate = (date, field = 'day') => {
const yearStr = date.getFullYear();
const month = date.getMonth() + 1;
const monthStr = fillWithZero(month, 2);
const day = date.getDate();
const dayStr = fillWithZero(day, 2);
switch (field) {
case 'year':
return `${yearStr}`;
case 'month':
return `${yearStr}/${monthStr}`;
case 'day':
return `${yearStr}-${monthStr}-${dayStr}`;
}
};
/**
* 计算current增加add天后是周几
* @param current 当前是第几天
* @param add 要加多少天
*/
export const calcWeekDay = (current, add) => {
return (current + add) % 7;
};
/**
* 获取当月的date列表
* @param date 属于目标月份的Date对象
* @param startDay 一行的起点 比如以周一为起点 此时startDay = 1,以周日为起点,此时startDay = 0
*/
export const getDateListByMonth = (date, startDay) => {
const month = date.getMonth();
const year = date.getFullYear();
/** 一周的最后一天 */
const weekEndDay = calcWeekDay(startDay, 6);
let result = [];
/** 先获取该月份的起点 */
date.setDate(1);
let dateObj = new Date(date);
dateObj.setDate(1);
/** 前面一部分非当前月的日期 */
for (let day = startDay; day != date.getDay(); day = calcWeekDay(day, 1)) {
dateObj.setFullYear(year);
dateObj.setMonth(month);
dateObj.setDate(date.getDate() - (date.getDay() - day));
const preDate={
date: dateObj.getDate(),
currentMonth: false,
fullDateStr: formatDate(dateObj, 'day'),
}
result.push(preDate);
}
/** 当前月的日期 */
console.log(date);
while (date.getMonth() === month) {
result.push({
date: date.getDate(),
currentMonth: true,
fullDateStr: formatDate(date, 'day'),
});
date.setDate(date.getDate() + 1);
}
/** 后面一部分非当前月的日期 */
for (let day = date.getDay(); day != weekEndDay; day = calcWeekDay(day, 1)) {
result.push({
date: date.getDate(),
currentMonth: false,
fullDateStr: formatDate(date, 'day'),
});
date.setDate(date.getDate() + 1);
}
result.push({
date: date.getDate(),
currentMonth: false,
fullDateStr: formatDate(date, 'day'),
});
// 保证每个月的数据都是 42 个
if (result.length === 35) {
date.setDate(date.getDate() + 1);
for (
let day = date.getDay();
day != weekEndDay;
day = calcWeekDay(day, 1)
) {
result.push({
date: date.getDate(),
currentMonth: false,
fullDateStr: formatDate(date, 'day'),
});
date.setDate(date.getDate() + 1);
}
result.push({
date: date.getDate(),
currentMonth: false,
fullDateStr: formatDate(date, 'day'),
});
}
return result;
};
/** 获取指定日期所在周的所有天
* @param date 属于目标星期的Date对象
* @param startDay 一行的起点 比如以周一为起点 此时startDay = 1,以周日为起点,此时startDay = 0
*/
export const getDateListByWeek = (date, startDay) => {
date.setDate(date.getDate() - ((date.getDay() - startDay + 7) % 7));
/** 一周的最后一天 */
const weekEndDay = calcWeekDay(startDay, 6);
let result = [];
while (date.getDay() !== weekEndDay) {
result.push({
date: date.getDate(),
currentMonth: true,
fullDateStr: formatDate(date, 'day'),
});
date.setDate(date.getDate() + 1);
}
result.push({
date: date.getDate(),
currentMonth: true,
fullDateStr: formatDate(date, 'day'),
});
return result;
};
export const getWeekDayList = (startDay) => {
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
let result = [];
for (let i = startDay; i < 7; i++) {
result.push(weekDays[i]);
}
for (let i = 0; i < startDay; i++) {
result.push(weekDays[i]);
}
return result;
};
// day/index.jsx
import React, { useEffect, useState } from 'react';
import { View } from '@tarojs/components';
import { formatDate } from '../utils';
const Day = (props) => {
const {
selected,
onDayLongPress,
onClick,
value,
markIndex,
extraInfoIndex,
customStyleGenerator,
disable,
isInRange,
rangeStart,
rangeEnd,
isMultiSelectAndFinish,
selectedDateColor,
markColor,
markSize,
extraInfoColor,
extraInfoSize,
extraInfoText,
showDivider,
} = props;
const [className, setClassName] = useState(['calendar-day']);
const [customStyles, setCustomStyles] = useState({});
useEffect(() => {
let set = ['calendar-day'];
const today = formatDate(new Date(), 'day');
if (!value.currentMonth || disable) {
// 非本月
set.push('not-this-month');
}
if (selected && !isMultiSelectAndFinish) {
// 选中
// 范围选择模式显示已选范围时,不显示selected
set.push('calendar-selected');
}
if (markIndex !== -1) {
// 标记
set.push('calendar-marked');
}
if (extraInfoIndex !== -1) {
// 额外信息
set.push('calendar-extra-info');
}
if (value.fullDateStr === today) {
// 当天
set.push('calendar-today');
}
if (showDivider) {
// 分割线
set.push('calendar-line-divider');
}
if (isInRange) {
set.push('calendar-range');
}
if (rangeStart) {
set.push('calendar-range-start');
}
if (rangeEnd) {
set.push('calendar-range-end');
}
setClassName(set);
}, [
disable,
extraInfoIndex,
isMultiSelectAndFinish,
markIndex,
selected,
showDivider,
value.currentMonth,
value.fullDateStr,
isInRange,
rangeStart,
rangeEnd,
]);
useEffect(() => {
if (customStyleGenerator) {
// 用户定制样式
const generatorParams = {
...value,
selected: selected,
multiSelect: {
multiSelected: isInRange,
multiSelectedStar: rangeStart,
multiSelectedEnd: rangeEnd,
},
marked: markIndex !== -1,
hasExtraInfo: extraInfoIndex !== -1,
};
setCustomStyles(customStyleGenerator(generatorParams));
}
}, [
selected,
value,
markIndex,
extraInfoIndex,
customStyleGenerator,
isInRange,
rangeStart,
rangeEnd
]);
return (
<View
onLongPress={
onDayLongPress
? () => onDayLongPress({ value: value.fullDateStr })
: undefined
}
className={className.join(' ')}
onClick={() => {
if (!disable) {
onClick(value);
}
}}
style={customStyles.containerStyle}
>
<View
className='calendar-date'
style={
customStyles.dateStyle || customStyles.dateStyle === {}
? customStyles.dateStyle
: {
backgroundColor: selected || isInRange ? selectedDateColor : '',
}
}
>
{/* 日期 */}
{value.date}
</View>
{/* 标记 */}
<View
className='calendar-mark'
style={{
backgroundColor: markIndex === -1 ? '' : markColor,
height: markIndex === -1 ? '' : markSize,
width: markIndex === -1 ? '' : markSize,
...customStyles.markStyle,
}}
/>
{extraInfoIndex !== -1 && (
<View
className='calendar-extra-info'
style={{
color: extraInfoIndex === -1 ? '' : extraInfoColor,
fontSize: extraInfoIndex === -1 ? '' : extraInfoSize,
...customStyles.extraInfoStyle,
}}
>
{/* 额外信息 */}
{extraInfoText}
</View>
)}
</View>
);
};
export default React.memo(Day);
// days/index.jsx
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { View } from '@tarojs/components';
import {
formatDate,
getDateListByMonth,
getDateListByWeek,
} from '../utils';
import Day from '../day';
import './index.scss';
const Days = ({
date,
onClick,
marks,
selectedDate,
selectedDateColor,
minDate,
maxDate,
onDayLongPress,
showDivider,
isMultiSelect,
selectedRange,
customStyleGenerator,
bodyStyle,
view,
startDay,
extraInfo,
}) => {
const [days, setDays] = useState([]);
const prevDateRef = useRef(null);
const prevViewRef = useRef(null);
const _onDayClick = useCallback(
(value) => {
console.log(value);
onClick && onClick(value);
},
[onClick]
);
const _onDayLongPress = useCallback(
(args) => {
onDayLongPress && onDayLongPress(args);
},
[onDayLongPress]
);
useEffect(() => {
//view和startDay基本不会变,就date会经常变化
//由于传递的是date对象,需要判断date对象的值是否变化,防止因为days变化导致的重复刷新
if (
!prevDateRef.current ||
formatDate(prevDateRef.current) !== formatDate(date) ||
prevViewRef.current !== view
) {
const dateObj = date ? new Date(date) : new Date();
let tempDays = [];
if (view === 'month') {
tempDays = getDateListByMonth(dateObj, startDay);
}
if (view === 'week') {
tempDays = getDateListByWeek(dateObj, startDay);
}
console.log('更新 days');
setDays(tempDays);
}
prevDateRef.current = date;
prevViewRef.current = view;
// console.log(formatDate(prevDateRef.current));
}, [view, date, startDay]);
const maxDateObj = new Date(maxDate);
const markDateList = marks.map((value) => value.value);
const extraInfoDateList = extraInfo.map((value) => value.value);
// console.log(extraInfo);
let endDateStr = selectedRange ? selectedRange.end : '';
const startDateObj = new Date(selectedRange ? selectedRange.start : '');
const endDateObj = new Date(endDateStr);
const minDateObj = new Date(minDate);
// console.log(days);
return (
<View className='calendar-body' style={bodyStyle}>
{days.map((value) => {
const markIndex = markDateList.indexOf(value.fullDateStr);
const extraInfoIndex = extraInfoDateList.indexOf(value.fullDateStr);
if (extraInfoIndex !== -1) {
console.log(extraInfoIndex);
}
let isInRange = false;
let rangeStart = false;
let rangeEnd = false;
if (isMultiSelect && endDateStr) {
// 范围选择模式
const valueDateTimestamp = new Date(value.fullDateStr).getTime();
if (
valueDateTimestamp >= startDateObj.getTime() &&
valueDateTimestamp <= endDateObj.getTime()
) {
// 被选择(范围选择)
isInRange = true;
if (valueDateTimestamp === startDateObj.getTime()) {
// 范围起点
rangeStart = true;
}
if (valueDateTimestamp === endDateObj.getTime()) {
// 范围终点
rangeEnd = true;
}
}
}
let disable =
new Date(value.fullDateStr).getTime() < minDateObj.getTime() ||
(maxDate &&
new Date(value.fullDateStr).getTime() > maxDateObj.getTime()) ||
false;
return (
<Day
key={value.fullDateStr}
onDayLongPress={_onDayLongPress}
selected={selectedDate === value.fullDateStr}
isMultiSelectAndFinish={
isMultiSelect && (selectedRange.end || '') != ''
}
markIndex={markIndex}
extraInfoIndex={extraInfoIndex}
showDivider={showDivider}
minDate={minDate}
value={value}
onClick={_onDayClick}
selectedDateColor={selectedDateColor}
markColor={markIndex === -1 ? '' : marks[markIndex].color}
markSize={markIndex === -1 ? '' : marks[markIndex].markSize}
extraInfoColor={
extraInfoIndex === -1 ? '' : extraInfo[extraInfoIndex].color
}
extraInfoSize={
extraInfoIndex === -1 ? '' : extraInfo[extraInfoIndex].fontSize
}
extraInfoText={
extraInfoIndex === -1 ? '' : extraInfo[extraInfoIndex].text
}
customStyleGenerator={customStyleGenerator}
isInRange={isInRange}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
disable={disable}
/>
);
})}
</View>
);
};
export default React.memo(Days);
// days/index.scss
.calendar-body {
display: flex;
flex-wrap: wrap;
padding-bottom: 1rem;
// 分割线
.calendar-line-divider {
border-top: 1px dashed rgba(0, 0, 0, 0.1);
}
// 基础样式
.calendar-date {
width: 2rem;
line-height: 2rem;
margin-left: 0.5rem;
border-radius: 50%;
}
// 选中样式
.calendar-selected > .calendar-date {
background-color: #90b1ef;
color: white;
}
// 范围选择
.calendar-range > .calendar-date {
background-color: #90b1ef;
color: white;
}
// 范围选择起点
.calendar-range-end > .calendar-date {
border-top-right-radius: 1rem;
border-bottom-right-radius: 1rem;
}
// 范围选择终点
.calendar-range-start > .calendar-date {
border-top-left-radius: 1rem;
border-bottom-left-radius: 1rem;
}
// 当天
.calendar-today>view:first-child {
border-radius: 50%;
background-color: #57C27E;
color: white;
}
// 非本月
.not-this-month {
color: #e2e5e8 !important;
}
// 农历
.lunar-day {
display: inline-block;
font-size: 2.5vw;
position: absolute;
top: 1.2rem;
left: 50%;
transform: translateX(-50%);
opacity: 0.7;
width: 2rem;
}
// 农历初一
.lunar-month {
text-decoration-color: red;
text-decoration-line: underline;
}
&>.calendar-day {
position: relative;
height: 3rem;
width: 14.28%;
text-align: center;
color: #828ba6;
// 标记
.calendar-mark {
position: absolute;
top: 0;
right: 0;
height: 0.5rem;
width: 0.5rem;
border-radius: 50%;
}
// 额外信息
.calendar-extra-info {
margin-top: -0.5rem;
color: #ff9800;
}
}
}
组件调用:
<Calendar
marks={marks}
extraInfo={[]}
view={view}
selectedDateColor="#346fc2"
onDayClick={(item) => this.onDayClick(item)}
/>
当点击日期后,以下内容中的value就是所点击的日期,格式为'2024-05-13'。
onDayClick =(item)=>{
const {value} =item
console.log(value)
}
参数说明:
参数 |
说明 |
类型 |
默认值 |
view |
视图模式 |
"week"|"month" |
"month" |
marks |
需要标记的时间 |
Array<{value:string,color:string,markSize:string}> |
[] |
extraInfo |
额外信息 |
Array<{value:string,text:string,color:string,fontSize:string}> |
[] |
onDayClick |
点击日期时候触发 |
(item:{value:string}) => any |
|
selectedDateColor |
选中日期的颜色 |
string |
参考文档:
更多推荐
所有评论(0)