You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 11:10:56