import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; import dayjs from 'dayjs'; import React, { useEffect, useRef, useState } from 'react'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; export interface DateSelectorProps { /** 当前选中的日期索引 */ selectedIndex?: number; /** 日期选择回调 */ onDateSelect?: (index: number, date: Date) => void; /** 是否显示月份标题 */ showMonthTitle?: boolean; /** 自定义月份标题 */ monthTitle?: string; /** 是否禁用未来日期 */ disableFutureDates?: boolean; /** 自定义样式 */ style?: any; /** 容器样式 */ containerStyle?: any; /** 日期项样式 */ dayItemStyle?: any; /** 是否自动滚动到选中项 */ autoScrollToSelected?: boolean; } export const DateSelector: React.FC = ({ selectedIndex: externalSelectedIndex, onDateSelect, showMonthTitle = true, monthTitle: externalMonthTitle, disableFutureDates = true, style, containerStyle, dayItemStyle, autoScrollToSelected = true, }) => { // 内部状态管理 const [internalSelectedIndex, setInternalSelectedIndex] = useState(getTodayIndexInMonth()); const selectedIndex = externalSelectedIndex ?? internalSelectedIndex; // 获取日期数据 const days = getMonthDaysZh(); const monthTitle = externalMonthTitle ?? getMonthTitleZh(); // 滚动相关 const daysScrollRef = useRef(null); const [scrollWidth, setScrollWidth] = useState(0); const DAY_PILL_WIDTH = 48; const DAY_PILL_SPACING = 8; // 滚动到指定索引 const scrollToIndex = (index: number, animated = true) => { if (!daysScrollRef.current || scrollWidth === 0) return; const itemWidth = DAY_PILL_WIDTH + DAY_PILL_SPACING; const baseOffset = index * itemWidth; const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2)); // 确保不会滚动超出边界 const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth); const finalOffset = Math.min(centerOffset, maxScrollOffset); if (animated) { // 使用原生动画API实现更平滑的滚动 requestAnimationFrame(() => { daysScrollRef.current?.scrollTo({ x: finalOffset, animated: true }); }); } else { // 非动画情况直接跳转 daysScrollRef.current?.scrollTo({ x: finalOffset, animated: false }); } }; // 初始化时滚动到选中项 useEffect(() => { if (scrollWidth > 0 && autoScrollToSelected) { scrollToIndex(selectedIndex, true); } }, [scrollWidth, selectedIndex, autoScrollToSelected]); // 当选中索引变化时,滚动到对应位置 useEffect(() => { if (scrollWidth > 0 && autoScrollToSelected) { // 添加微小延迟以确保动画效果更明显 const timer = setTimeout(() => { scrollToIndex(selectedIndex, true); }, 50); return () => clearTimeout(timer); } }, [selectedIndex, autoScrollToSelected]); // 处理日期选择 const handleDateSelect = (index: number) => { const targetDate = days[index]?.date?.toDate(); if (!targetDate) return; // 检查是否为未来日期 if (disableFutureDates && days[index].date.isAfter(dayjs(), 'day')) { return; } // 先滚动到目标位置,再更新状态 if (autoScrollToSelected) { scrollToIndex(index, true); } // 更新内部状态(如果使用外部控制则不更新) if (externalSelectedIndex === undefined) { setInternalSelectedIndex(index); } // 调用回调 onDateSelect?.(index, targetDate); }; return ( {showMonthTitle && ( {monthTitle} )} setScrollWidth(e.nativeEvent.layout.width)} style={style} > {days.map((d, i) => { const selected = i === selectedIndex; const isFutureDate = disableFutureDates && d.date.isAfter(dayjs(), 'day'); return ( !isFutureDate && handleDateSelect(i)} activeOpacity={isFutureDate ? 1 : 0.8} disabled={isFutureDate} > {d.weekdayZh} {d.dayOfMonth} ); })} ); }; const styles = StyleSheet.create({ container: { paddingVertical: 8, }, monthTitle: { fontSize: 18, fontWeight: '800', color: '#192126', marginBottom: 14, }, daysContainer: { paddingBottom: 8, }, dayItemWrapper: { alignItems: 'center', width: 48, marginRight: 8, }, dayPill: { width: 40, height: 60, borderRadius: 24, alignItems: 'center', justifyContent: 'center', }, dayPillNormal: { backgroundColor: 'transparent', }, dayPillSelected: { backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, dayPillDisabled: { backgroundColor: 'transparent', opacity: 0.4, }, dayLabel: { fontSize: 11, fontWeight: '700', color: 'gray', marginBottom: 2, }, dayLabelSelected: { color: '#192126', }, dayLabelDisabled: { color: 'gray', }, dayDate: { fontSize: 12, fontWeight: '600', color: 'gray', }, dayDateSelected: { color: '#192126', }, dayDateDisabled: { color: 'gray', }, });