Files
mini-programs/src/components/Picker/CalendarUI/CalendarUI.tsx

290 lines
9.2 KiB
TypeScript

import React, { useState, useEffect, useRef, useImperativeHandle } from "react";
import { CalendarCard } from "@nutui/nutui-react-taro";
import { View, Text, Image } from "@tarojs/components";
import images from "@/config/images";
import styles from "./index.module.scss";
import {
getMonth,
getWeekend,
getWeekendOfCurrentWeek,
} from "@/utils/timeUtils";
import { PopupPicker } from "@/components/Picker/index";
import dayjs from "dayjs";
interface NutUICalendarProps {
type?: "single" | "range" | "multiple";
value?: string | Date | String[] | Date[];
defaultValue?: string | string[];
onChange?: (value: Date | Date[]) => void;
isBorder?: boolean;
showQuickActions?: boolean;
onHeaderClick?: (date: Date) => void;
}
export interface CalendarUIRef {
jumpTo: (year: number, month: number) => void;
gotoMonth: (delta: number) => void;
}
const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(
(
{
type = "single",
value,
onChange,
isBorder = false,
showQuickActions = true,
onHeaderClick,
},
ref
) => {
// 根据类型初始化选中值
// const getInitialValue = (): Date | Date[] => {
// console.log(value,defaultValue,'today')
// if (typeof value === 'string' && value) {
// return new Date(value)
// }
// if (Array.isArray(value) && value.length > 0) {
// return value.map(item => new Date(item))
// }
// if (typeof defaultValue === 'string' && defaultValue) {
// return new Date(defaultValue)
// }
// if (Array.isArray(defaultValue) && defaultValue.length > 0) {
// return defaultValue.map(item => new Date(item))
// }
// const today = new Date();
// if (type === 'multiple') {
// return [today]
// }
// return today
// }
const startOfMonth = (date: Date) =>
new Date(date.getFullYear(), date.getMonth(), 1);
const [selectedValue, setSelectedValue] = useState<Date | Date[]>();
const [current, setCurrent] = useState<Date>(startOfMonth(new Date()));
const calendarRef = useRef<any>(null);
const [visible, setvisible] = useState(false);
console.log("current", current);
// 当外部 value 变化时更新内部状态
useEffect(() => {
if (Array.isArray(value) && value.length > 0) {
setSelectedValue(value.map((item) => new Date(item)));
setCurrent(new Date(value[0] as Date));
}
if ((typeof value === "string" || value instanceof Date) && value) {
setSelectedValue(new Date(value));
setCurrent(new Date(value));
}
}, [value]);
useImperativeHandle(ref, () => ({
jumpTo: (year: number, month: number) => {
calendarRef.current?.jumpTo(year, month);
},
gotoMonth,
}));
const handleDateChange = (newValue: any) => {
if (type === "range") return;
setSelectedValue(newValue);
onChange?.(newValue as any);
};
const formatHeader = (date: Date) => `${getMonth(date)}`;
const handlePageChange = (data: { year: number; month: number }) => {
// 月份切换时的处理逻辑,如果需要的话
console.log("月份切换:", data);
};
const handleDayClick = (day: any) => {
const { type, year, month, date } = day;
if (type === "next") return;
onChange?.([new Date(year, month - 1, date)]);
};
const gotoMonth = (delta: number) => {
const base = current instanceof Date ? new Date(current) : new Date();
base.setMonth(base.getMonth() + delta);
const next = startOfMonth(base);
setCurrent(next);
// 同步底部 CalendarCard 的月份
try {
calendarRef.current?.jump?.(delta);
} catch (e) {
console.warn("CalendarCardRef jump 调用失败", e);
}
handlePageChange({
year: next.getFullYear(),
month: next.getMonth() + 1,
});
};
const handleHeaderClick = () => {
onHeaderClick && onHeaderClick(current);
setvisible(true);
};
const syncMonthTo = (anchor: Date) => {
// 计算从 current 到目标 anchor 所在月份的偏移,调用 jump(delta)
const monthsDelta =
(anchor.getFullYear() - current.getFullYear()) * 12 +
(anchor.getMonth() - current.getMonth());
if (monthsDelta !== 0) {
gotoMonth(monthsDelta);
}
};
const renderDay = (day: any) => {
const { date, month, year } = day;
const today = new Date();
if (
date === today.getDate() &&
month === today.getMonth() + 1 &&
year === today.getFullYear()
) {
return <View className="day-container">{date}</View>;
}
return date;
};
const selectWeekend = () => {
const [start, end] = getWeekend();
setSelectedValue([start, end]);
syncMonthTo(start);
onChange?.([start, end]);
};
const selectWeek = () => {
const dayList = getWeekendOfCurrentWeek(7);
setSelectedValue(dayList);
syncMonthTo(dayList[0]);
onChange?.(dayList);
};
const selectMonth = () => {
const dayList = getWeekendOfCurrentWeek(30);
setSelectedValue(dayList);
syncMonthTo(dayList[0]);
onChange?.(dayList);
};
const handleMonthChange = (value: any) => {
const [year, month] = value;
const newDate = new Date(year, month - 1, 1);
setCurrent(newDate);
calendarRef.current?.jumpTo(year, month);
};
return (
<View>
{/* 快速操作行 */}
{showQuickActions && (
<View className={styles["quick-actions"]}>
<View className={styles["quick-action"]} onClick={selectWeekend}>
</View>
<View className={styles["quick-action"]} onClick={selectWeek}>
</View>
<View className={styles["quick-action"]} onClick={selectMonth}>
</View>
</View>
)}
<View
className={`${styles["calendar-card"]} ${
isBorder ? styles["border"] : ""
}`}
>
{type === "range" && (
<View className={styles["date-range-container"]}>
<Text
className={`${styles["date-text-placeholder"]} ${
(value as Date[]).length === 2 ? styles["date-text"] : ""
}`}
>
{(value as Date[]).length === 2
? dayjs(value?.[0] as Date).format("YYYY-MM-DD")
: "起始时间"}
</Text>
<Text></Text>
<Text
className={`${styles["date-text-placeholder"]} ${
(value as Date[]).length === 2 ? styles["date-text"] : ""
}`}
>
{(value as Date[]).length === 2
? dayjs(value?.[1] as Date).format("YYYY-MM-DD")
: "结束时间"}
</Text>
</View>
)}
{/* 自定义头部显示周一到周日 */}
<View className={styles["header"]}>
<View className={styles["header-left"]} onClick={handleHeaderClick}>
<Text className={styles["header-text"]}>
{formatHeader(current as Date)}
</Text>
<Image
src={images.ICON_RIGHT_MAX}
className={`${styles["month-arrow"]}`}
onClick={() => gotoMonth(1)}
/>
</View>
<View className={styles["header-actions"]}>
<View
className={styles["arrow-left-container"]}
onClick={() => gotoMonth(-1)}
>
<Image
src={images.ICON_RIGHT_MAX}
className={`${styles["arrow"]} ${styles["left"]}`}
/>
</View>
<View
className={styles["arrow-right-container"]}
onClick={() => gotoMonth(1)}
>
<Image
src={images.ICON_RIGHT_MAX}
className={`${styles["arrow"]}`}
/>
</View>
</View>
</View>
<View className={styles["week-header"]}>
{["周日", "周一", "周二", "周三", "周四", "周五", "周六"].map(
(day) => (
<Text key={day} className={styles["week-day"]}>
{day}
</Text>
)
)}
</View>
{/* NutUI CalendarCard 组件 */}
<CalendarCard
ref={calendarRef}
type={type}
value={selectedValue}
renderDay={renderDay}
onChange={handleDateChange}
onPageChange={handlePageChange}
onDayClick={handleDayClick}
/>
</View>
{visible && (
<PopupPicker
visible={visible}
setvisible={setvisible}
value={[current.getFullYear(), current.getMonth() + 1]}
type="month"
onChange={(value) => handleMonthChange(value)}
/>
)}
</View>
);
}
);
export default NutUICalendar;