最近写小程序时,使用@nutui/nutui-react-taro里面的时间选择器,总是很不流畅,用户体验感不好
鞭策AI,利用Taro中的PickerView 和 PickerViewColumn组件,手写了个时间选择器
滑动比较流畅
下面是源码:
import { useState, useEffect } from 'react'
import { View, Text, PickerView, PickerViewColumn } from '@tarojs/components'
import { Image } from '@nutui/nutui-react-taro'
import './index.scss'
import ic_more from '@/public/images/ic_more@2x.png'
// 类型定义
interface DateTimePickerProps {
defaultValue?: string // 默认值 YYYY-MM-DD HH:mm
defaultColor?: string
defaultSize?: string
valueColor?: string
valueSize?: string
minDate?: string // 最小可选日期 YYYY-MM-DD
maxDate?: string // 最大可选日期 YYYY-MM-DD
format?: 'YYYY-MM-DD HH:mm' | 'YYYY-MM-DD' // 显示格式
onChange: (value: string) => void // 选择回调
placeholder?: string // 占位符
confirmText?: string
cancelText?: string
titleText?: string
onClick?: () => void
}
// 工具函数:格式化数字为两位数
const formatNum = (num: number): string => num.toString().padStart(2, '0')
// 工具函数:生成年份数组
const generateYears = (minYear: number, maxYear: number): string[] => {
const years: string[] = []
for (let i = minYear; i <= maxYear; i++) {
years.push(i.toString())
}
return years
}
// 工具函数:生成月份数组
const generateMonths = (): string[] => {
const months: string[] = []
for (let i = 1; i <= 12; i++) {
months.push(formatNum(i))
}
return months
}
// 工具函数:生成日期数组(根据年月)
const generateDays = (year: string, month: string): string[] => {
const daysInMonth = new Date(+year, +month, 0).getDate()
const days: string[] = []
for (let i = 1; i <= daysInMonth; i++) {
days.push(formatNum(i))
}
return days
}
// 工具函数:生成小时数组
const generateHours = (): string[] => {
const hours: string[] = []
for (let i = 0; i < 24; i++) {
hours.push(formatNum(i))
}
return hours
}
// 工具函数:生成分钟数组
const generateMinutes = (step = 5): string[] => {
const minutes: string[] = []
for (let i = 0; i < 60; i += step) {
minutes.push(formatNum(i))
}
return minutes
}
const DateTimePicker: React.FC<DateTimePickerProps> = ({
defaultValue = '',
defaultColor = '#666',
defaultSize = '15px',
valueColor = '#333',
valueSize = '16px',
minDate = '2000-01-01',
maxDate = '2099-12-31',
format = 'YYYY-MM-DD HH:mm',
onChange,
placeholder = '请选择日期时间',
cancelText = '取消',
confirmText = '保存',
titleText = '选择日期时间',
onClick = () => {},
}) => {
// 核心新增:控制选择器弹窗显隐
const [pickerVisible, setPickerVisible] = useState<boolean>(false)
// 原有状态
const [selectedValue, setSelectedValue] = useState<number[]>([])
const [years, setYears] = useState<string[]>([])
const [months, setMonths] = useState<string[]>([])
const [days, setDays] = useState<string[]>([])
const [hours, setHours] = useState<string[]>([])
const [minutes, setMinutes] = useState<string[]>([])
const [displayValue, setDisplayValue] = useState<string>(defaultValue || '')
// 查找值在数组中的索引
const findIndexByValue = (values: string[], targetValue: string): number => {
let index = values.indexOf(targetValue)
if (index !== -1) return index
const normalizedTarget = targetValue.startsWith('0') ? targetValue.slice(1) : targetValue
index = values.findIndex(v => v === normalizedTarget || v === targetValue)
return index !== -1 ? index : 0
}
// 解析日期字符串为索引数组
const parseDateTimeToIndexes = (
dateStr: string,
years: string[],
months: string[],
days: string[],
hours: string[],
minutes: string[],
): number[] => {
if (!dateStr || dateStr === '未设置') return []
const [datePart, timePart = '00:00'] = dateStr.split(' ')
const [year, month, day] = datePart.split('-')
const [hour, minute] = timePart.split(':')
return [
findIndexByValue(years, year),
findIndexByValue(months, month),
findIndexByValue(days, day),
findIndexByValue(hours, hour),
findIndexByValue(minutes, minute),
]
}
// 初始化时间选项
// 初始化时间选项
useEffect(() => {
const minYear = new Date(minDate).getFullYear()
const maxYear = new Date(maxDate).getFullYear()
const newYears = generateYears(minYear, maxYear)
const newMonths = generateMonths()
const newHours = generateHours()
const newMinutes = generateMinutes()
setYears(newYears)
setMonths(newMonths)
setHours(newHours)
setMinutes(newMinutes)
if (defaultValue && defaultValue !== '未设置') {
const [defaultYear] = defaultValue.split('-')
const defaultMonth = defaultValue.split('-')[1]
const newDays = generateDays(defaultYear, defaultMonth)
setDays(newDays)
setTimeout(() => {
const indexes = parseDateTimeToIndexes(
defaultValue,
newYears,
newMonths,
newDays,
newHours,
newMinutes,
)
setSelectedValue(indexes.filter(index => index >= 0))
}, 0)
} else {
// 如果没有默认值,设置默认选中为当前时间
const now = new Date()
const currentYear = now.getFullYear().toString()
const currentMonth = (now.getMonth() + 1).toString().padStart(2, '0')
const currentDay = now.getDate().toString().padStart(2, '0')
const currentHour = now.getHours().toString().padStart(2, '0')
const currentMinute = (Math.floor(now.getMinutes() / 5) * 5).toString().padStart(2, '0')
// 设置当前月份的天数
const newDays = generateDays(currentYear, currentMonth)
setDays(newDays)
setTimeout(() => {
const currentIndexes = [
findIndexByValue(newYears, currentYear),
findIndexByValue(newMonths, currentMonth),
findIndexByValue(newDays, currentDay),
]
// 如果是完整格式,加上时间
if (format === 'YYYY-MM-DD HH:mm') {
currentIndexes.push(
findIndexByValue(newHours, currentHour),
findIndexByValue(newMinutes, currentMinute),
)
}
setSelectedValue(currentIndexes)
}, 0)
}
}, [minDate, maxDate, defaultValue])
// 处理PickerView值变化(年份/月份变化时更新日期)
const handlePickerChange = (e: any) => {
const newSelectedIndexes = e.detail.value || []
setSelectedValue(newSelectedIndexes)
// 年份或月份变化时,重新计算日期
const [yearIndex, monthIndex] = newSelectedIndexes
if (yearIndex !== undefined && monthIndex !== undefined) {
const selectedYear = years[yearIndex] || years[0]
const selectedMonth = months[monthIndex] || months[0]
const newDays = generateDays(selectedYear, selectedMonth)
setDays(newDays)
// 如果当前日期超出范围,重置为最后一天
if (newSelectedIndexes[2] >= newDays.length) {
const updatedIndexes = [...newSelectedIndexes]
updatedIndexes[2] = newDays.length - 1
setSelectedValue(updatedIndexes)
}
}
}
// 确认选择
const handleConfirm = () => {
if (selectedValue.length === 0) return
// 映射索引到实际值
const getValueByIndex = (list: string[], index: number) => list[index] || list[0]
const year = getValueByIndex(years, selectedValue[0])
const month = getValueByIndex(months, selectedValue[1])
const day = getValueByIndex(days, selectedValue[2])
const hour = format === 'YYYY-MM-DD HH:mm' ? getValueByIndex(hours, selectedValue[3]) : '00'
const minute = format === 'YYYY-MM-DD HH:mm' ? getValueByIndex(minutes, selectedValue[4]) : '00'
// 验证日期范围
// const selectedDate = new Date(`${year}-${month}-${day} ${hour}:${minute}`)
// const minDateTime = new Date(minDate)
// const maxDateTime = new Date(maxDate)
// if (selectedDate < minDateTime || selectedDate > maxDateTime) {
// Toast({
// title: `请选择${minDate}至${maxDate}之间的时间`,
// icon: 'none',
// })
// return
// }
// 格式化结果
const result =
format === 'YYYY-MM-DD HH:mm'
? `${year}-${month}-${day} ${hour}:${minute}`
: `${year}-${month}-${day}`
console.log(result)
setDisplayValue(result)
onChange(result)
setPickerVisible(false) // 关闭弹窗
}
// 取消选择
const handleCancel = () => {
setPickerVisible(false)
onChange('')
}
// 构建PickerView的列数据
const getPickerColumns = () => {
const columns = [
<PickerViewColumn key="year" className="picker-column">
{years.map(item => (
<View key={`year-${item}`} className="picker-item">
{item}年
</View>
))}
</PickerViewColumn>,
<PickerViewColumn key="month" className="picker-column">
{months.map(item => (
<View key={`month-${item}`} className="picker-item">
{item}月
</View>
))}
</PickerViewColumn>,
<PickerViewColumn key="day" className="picker-column">
{days.map(item => (
<View key={`day-${item}`} className="picker-item">
{item}日
</View>
))}
</PickerViewColumn>,
]
// 根据格式添加时分列
if (format === 'YYYY-MM-DD HH:mm') {
columns.push(
<PickerViewColumn key="hour" className="picker-column">
{hours.map(item => (
<View key={`hour-${item}`} className="picker-item">
{item}时
</View>
))}
</PickerViewColumn>,
<PickerViewColumn key="minute" className="picker-column">
{minutes.map(item => (
<View key={`minute-${item}`} className="picker-item">
{item}分
</View>
))}
</PickerViewColumn>,
)
}
return columns
}
return (
<View className="date-time-picker">
{/* 触发区域 */}
<View
className="picker-display flex items-center justify-between"
onClick={() => {
onClick()
setPickerVisible(true)
}}
>
<Text
className="mr-[10px]"
style={{
color: displayValue ? valueColor : defaultColor,
fontSize: displayValue ? valueSize : defaultSize,
}}
>
{displayValue || placeholder}
</Text>
<Image src={ic_more} width={18} height={18} />
</View>
{/* 自定义PickerView弹窗 */}
{pickerVisible && (
<View className="picker-modal-mask" onClick={handleCancel}>
<View className="picker-modal-content" onClick={e => e.stopPropagation()}>
{/* 顶部操作栏 */}
<View className="picker-modal-header flex justify-between items-center">
<Text className="picker-modal-cancel" onClick={handleCancel}>
{cancelText}
</Text>
<Text className="picker-modal-title">{titleText}</Text>
<Text className="picker-modal-confirm" onClick={handleConfirm}>
{confirmText}
</Text>
</View>
{/* PickerView主体 */}
<PickerView
value={selectedValue}
onChange={handlePickerChange}
className="picker-view-container"
>
{getPickerColumns()}
</PickerView>
</View>
</View>
)}
</View>
)
}
export default DateTimePicker
Scss代码如下:
xml
.date-time-picker {
.picker-display {
width: 100%;
box-sizing: border-box;
cursor: pointer;
}
// 遮罩层
.picker-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
// 弹窗内容
.picker-modal-content {
width: 100%;
background-color: #fff;
border-radius: 12px 12px 0 0;
overflow: hidden;
}
// 弹窗头部
.picker-modal-header {
padding: 12px 16px;
border-bottom: 1px solid #eee;
.picker-modal-cancel,
.picker-modal-confirm {
font-size: 15px;
color: #666;
padding: 4px 8px;
}
.picker-modal-confirm {
color: #1677ff; // 确认按钮蓝色
font-weight: 500;
}
.picker-modal-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
}
// PickerView容器
.picker-view-container {
height: 300px;
width: 100%;
}
// 每一列样式
.picker-column {
.picker-item {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #333;
}
}
}
// 适配flex布局
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
样例图:

