Files
mini-programs/src/components/TimePicker/TimePicker.tsx
2025-08-30 22:25:39 +08:00

234 lines
7.6 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react'
import { View, Text, ScrollView } from '@tarojs/components'
import { CommonPopup } from '../index'
import styles from './index.module.scss'
export interface TimePickerProps {
visible: boolean
onClose: () => void
onConfirm: (year: number, month: number) => void
defaultYear?: number
defaultMonth?: number
minYear?: number
maxYear?: number
}
const TimePicker: React.FC<TimePickerProps> = ({
visible,
onClose,
onConfirm,
defaultYear = new Date().getFullYear(),
defaultMonth = new Date().getMonth() + 1,
minYear = 2020,
maxYear = 2030
}) => {
const [selectedYear, setSelectedYear] = useState(defaultYear)
const [selectedMonth, setSelectedMonth] = useState(defaultMonth)
const [yearScrollTop, setYearScrollTop] = useState(0)
const [monthScrollTop, setMonthScrollTop] = useState(0)
const yearScrollRef = useRef<any>(null)
const monthScrollRef = useRef<any>(null)
// 计算当前选项在数组中的索引
const getYearIndex = (year: number) => year - minYear
const getMonthIndex = (month: number) => month - 1
// 生成选择器的选项数据
const yearOptions = Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({
text: `${minYear + index}`,
value: minYear + index
}))
const monthOptions = Array.from({ length: 12 }, (_, index) => ({
text: `${index + 1}`,
value: index + 1
}))
// 计算滚动位置 - 确保每次只显示一个选项
const calculateScrollTop = (index: number) => {
const itemHeight = 48 // 每个选项的高度
const containerHeight = 216 // 容器高度
const centerOffset = (containerHeight - itemHeight) / 2
return index * itemHeight - centerOffset
}
// 获取当前可见的选项数量
const getVisibleItemCount = () => {
const containerHeight = 216
const itemHeight = 48
return Math.floor(containerHeight / itemHeight)
}
useEffect(() => {
if (visible) {
setSelectedYear(defaultYear)
setSelectedMonth(defaultMonth)
// 设置初始滚动位置
const yearScrollTop = calculateScrollTop(getYearIndex(defaultYear))
const monthScrollTop = calculateScrollTop(getMonthIndex(defaultMonth))
setYearScrollTop(yearScrollTop)
setMonthScrollTop(monthScrollTop)
}
}, [visible, defaultYear, defaultMonth])
// 处理年份滚动
const handleYearScroll = (event: any) => {
const scrollTop = event.detail.scrollTop
const itemHeight = 48
const containerHeight = 216
const centerOffset = (containerHeight - itemHeight) / 2
// 计算当前选中的年份索引
const currentIndex = Math.round((scrollTop + centerOffset) / itemHeight)
const clampedIndex = Math.max(0, Math.min(currentIndex, yearOptions.length - 1))
const newYear = minYear + clampedIndex
if (newYear !== selectedYear) {
setSelectedYear(newYear)
}
}
// 处理年份滚动结束,自动对齐
const handleYearScrollEnd = () => {
const yearIndex = getYearIndex(selectedYear)
const alignedScrollTop = calculateScrollTop(yearIndex)
// 使用setTimeout确保滚动动画完成后再对齐
setTimeout(() => {
setYearScrollTop(alignedScrollTop)
}, 100)
}
// 处理月份滚动
const handleMonthScroll = (event: any) => {
const scrollTop = event.detail.scrollTop
const itemHeight = 48
const containerHeight = 216
const centerOffset = (containerHeight - itemHeight) / 2
// 计算当前选中的月份索引
const currentIndex = Math.round((scrollTop + centerOffset) / itemHeight)
const clampedIndex = Math.max(0, Math.min(currentIndex, monthOptions.length - 1))
const newMonth = clampedIndex + 1
if (newMonth !== selectedMonth) {
setSelectedMonth(newMonth)
}
}
// 处理月份滚动结束,自动对齐
const handleMonthScrollEnd = () => {
const monthIndex = getMonthIndex(selectedMonth)
const alignedScrollTop = calculateScrollTop(monthIndex)
// 使用setTimeout确保滚动动画完成后再对齐
setTimeout(() => {
setMonthScrollTop(alignedScrollTop)
}, 100)
}
const handleConfirm = () => {
onConfirm(selectedYear, selectedMonth)
onClose()
}
if (!visible) return null
return (
<CommonPopup
visible={visible}
onClose={onClose}
onConfirm={handleConfirm}
showHeader={false}
hideFooter={false}
cancelText="返回"
confirmText="完成"
position="bottom"
round={true}
className={styles['time-picker-popup']}
>
{/* 拖拽手柄 */}
<View className={styles['popup-handle']} />
{/* 时间选择器 */}
<View className={styles['picker-container']}>
{/* 自定义多列选择器 */}
<View className={styles['picker-wrapper']}>
<View className={styles['custom-picker']}>
{/* 选中项指示器 */}
<View className={styles['picker-indicator']} />
{/* 年份列 */}
<View className={styles['picker-column']}>
<ScrollView
ref={yearScrollRef}
scrollY
scrollTop={yearScrollTop}
onScroll={handleYearScroll}
onTouchEnd={handleYearScrollEnd}
onScrollToLower={handleYearScrollEnd}
onScrollToUpper={handleYearScrollEnd}
className={styles['picker-scroll']}
scrollWithAnimation={true}
enhanced={true}
showScrollbar={false}
bounces={false}
fastDeceleration={true}
>
<View className={styles['picker-padding']} />
{yearOptions.map((option, index) => (
<View
key={option.value}
className={`${styles['picker-item']} ${
option.value === selectedYear ? styles['picker-item-active'] : ''
}`}
data-value={option.value}
>
<Text className={styles['picker-item-text']}>{option.text}</Text>
</View>
))}
<View className={styles['picker-padding']} />
</ScrollView>
</View>
{/* 月份列 */}
<View className={styles['picker-column']}>
<ScrollView
ref={monthScrollRef}
scrollY
scrollTop={monthScrollTop}
onScroll={handleMonthScroll}
onTouchEnd={handleMonthScrollEnd}
onScrollToLower={handleMonthScrollEnd}
onScrollToUpper={handleMonthScrollEnd}
className={styles['picker-scroll']}
scrollWithAnimation={true}
enhanced={true}
showScrollbar={false}
bounces={false}
fastDeceleration={true}
>
<View className={styles['picker-padding']} />
{monthOptions.map((option, index) => (
<View
key={option.value}
className={`${styles['picker-item']} ${
option.value === selectedMonth ? styles['picker-item-active'] : ''
}`}
data-value={option.value}
>
<Text className={styles['picker-item-text']}>{option.text}</Text>
</View>
))}
<View className={styles['picker-padding']} />
</ScrollView>
</View>
</View>
</View>
</View>
</CommonPopup>
)
}
export default TimePicker