Next.js 13 App Dir实现流畅滑动时间选择器的问题求助
自定义滑动时间选择器问题解决方案
问题分析
你当前的自定义时间选择器存在两个核心问题:
- 滑动时只能单次切换一个数值:原逻辑基于触摸起始时的初始值计算新值,且仅当滑动距离达到阈值时才更新,无法连续响应滑动操作
- 无实时滑动跟随效果:容器位置仅依赖选中值的状态更新,滑动过程中没有实时偏移反馈,导致交互感缺失
现有代码修复方案
以下是修改后的完整代码,解决上述两个问题并优化交互体验:
1. 子组件 TimeSection.tsx(核心滑动逻辑)
import React, { useState, useRef } from "react"; interface TimeSectionProps { values: number[]; currentValue: number; onValueChange: (value: number) => void; } export default function TimeSection({ values, currentValue, onValueChange, }: TimeSectionProps) { const [tempOffset, setTempOffset] = useState(0); const itemHeight = 24; // 每个数值项的固定高度 const startInfo = useRef<{ startY: number; initialTop: number } | null>(null); const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => { if (e.touches.length === 0) return; const startY = e.touches[0].clientY; // 计算初始top位置:容器中心对齐选中项 const initialTop = window.innerHeight / 2 - currentValue * itemHeight; startInfo.current = { startY, initialTop }; setTempOffset(0); }; const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => { if (!startInfo.current) return; const currentY = e.touches[0].clientY; const diff = currentY - startInfo.current.startY; // 实时更新临时偏移,实现滑动跟随效果 setTempOffset(diff); }; const handleTouchEnd = () => { if (!startInfo.current) return; // 计算滑动结束后的最终选中值 const totalOffset = startInfo.current.initialTop + tempOffset; const centerY = window.innerHeight / 2; const relativeY = centerY - totalOffset; let newValue = Math.round(relativeY / itemHeight); // 限制数值在合法范围内 newValue = Math.max(0, Math.min(values.length - 1, newValue)); onValueChange(newValue); setTempOffset(0); startInfo.current = null; }; // 容器最终位置:初始位置 + 临时偏移 const containerTop = `calc(50% - ${currentValue * itemHeight}px + ${tempOffset}px)`; return ( <div style={{ height: "250px", width: "50px", overflow: "hidden", position: "relative", border: "1px solid #eee", }} > {/* 中心选中线,增强视觉引导 */} <div style={{ position: "absolute", top: "50%", left: 0, right: 0, height: "1px", backgroundColor: "#333", transform: "translateY(-50%)", zIndex: 10, }} /> <div onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd} onTouchCancel={handleTouchEnd} // 处理触摸中断场景 style={{ position: "absolute", top: containerTop, width: "100%", textAlign: "center", // 滑动结束后平滑归位 transition: tempOffset === 0 ? "top 0.2s ease" : "none", }} > {values.map((value, index) => { const distanceFromCurrent = Math.abs(index - currentValue); // 显示相邻3项,增强滑动连续感 const isVisible = distanceFromCurrent <= 3; // 动态调整透明度和字体大小,突出选中项 const opacity = isVisible ? (index === currentValue ? 1 : 0.6 - distanceFromCurrent * 0.1) : 0; const fontSize = index === currentValue ? "24px" : "18px"; return ( <div key={index} style={{ color: "black", height: `${itemHeight}px`, lineHeight: `${itemHeight}px`, visibility: isVisible ? "visible" : "hidden", opacity, fontSize, transition: "opacity 0.2s ease, fontSize 0.2s ease", }} > {String(value).padStart(2, "0")} </div> ); })} </div> </div> ); }
2. 父组件 TimePickerModal.tsx
"use client"; import React, { useState } from "react"; import TimeSection from "./TimeSection"; export default function TimePickerModal() { const [isOpen, setIsOpen] = useState(false); const [selectedHour, setSelectedHour] = useState(12); const [selectedMinute, setSelectedMinute] = useState(30); const hours = Array.from({ length: 24 }, (_, i) => i); const minutes = Array.from({ length: 60 }, (_, i) => i); return ( <div> <button onClick={() => setIsOpen(true)}>打开时间选择器</button> {isOpen && ( <div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0,0,0,0.5)", display: "flex", justifyContent: "center", alignItems: "center", }} > <div style={{ backgroundColor: "white", padding: "20px", borderRadius: "8px", display: "flex", gap: "16px", alignItems: "center", color: "black", }} > <TimeSection values={hours} currentValue={selectedHour} onValueChange={setSelectedHour} /> <span style={{ fontSize: "24px", fontWeight: "bold" }}>:</span> <TimeSection values={minutes} currentValue={selectedMinute} onValueChange={setSelectedMinute} /> <button onClick={() => { console.log( `选中时间: ${String(selectedHour).padStart(2, "0")}:${String(selectedMinute).padStart(2, "0")}` ); setIsOpen(false); }} style={{ padding: "8px 16px", backgroundColor: "#007bff", color: "white", border: "none", borderRadius: "4px", cursor: "pointer", }} > 确认 </button> </div> </div> )} </div> ); }
修复说明
- 实时滑动跟随:通过
tempOffset状态跟踪触摸偏移,实时更新容器位置,滑动过程中数字会跟随手指移动 - 连续数值切换:触摸结束时基于总偏移计算最终选中值,支持一次滑动切换多个数值
- 交互优化:增加中心选中线、平滑过渡动画、动态样式调整,提升用户体验
替代方案:适配的npm包
如果不想自行维护滑动逻辑,可选择以下轻量级组件:
- rc-picker:蚂蚁金服出品,支持单独选择时分,滑动交互流畅,自定义样式能力强
- react-swipeable-picker:专为Web端设计的垂直滑动选择组件,配置简单,适合时间选择场景
- react-native-picker-select:虽标注React Native,但Web端可适配,支持自定义模板和滑动操作
内容的提问来源于stack exchange,提问作者Inzen
相关产品推荐
相关产品推荐

