【山竹记账前端-react】18.自制日期选择器(下)


大纲链接 §

[toc]


1. 一列变三列 ⇧

提取单个日期栏组件到 DateTimeColumn.tsx

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
interface ColumnProps {
  className?: string
  start?: Date
  end?: Date
  value?: Date
  dateItemHeight?: number
}

const DateTimeColumn: React.FC<ColumnProps> = ({
  className,
  start,
  end,
  value,
  dateItemHeight = 36,
}) => {
  const startTime = start ? createDateTime(start) : createDateTime().add(-3, 'year')
  const endTime = end ? createDateTime(end) : createDateTime().add(3, 'year')
  if (startTime.getTimestamp() >= endTime.getTimestamp()) {
    throw new Error('startTime must be greater than endTime')
  }

  const valueTime = value ? createDateTime(value) : createDateTime()
  const yearList = Array.from({ length: endTime.year - startTime.year + 1 }).map(
    (_, i) => startTime.year + i
  )
  const halfDateItemHeight = dateItemHeight / 2
  const itemHeightVarStyle = {
    '--date-item-height': `${dateItemHeight}px`,
    '--half-date-item-height': `${halfDateItemHeight}px`,
    '--total-height': `${yearList.length * dateItemHeight}px`,
  }
  const currentYearIndex = yearList.indexOf(valueTime.year)

  const [isTouching, setIsTouching] = useState(false)
  const [lastY, setLastY] = useState(-1)
  const [translateY, setTranslateYState] = useState(currentYearIndex * -dateItemHeight)

  const springStyle = useSpring({
    transform: `translate3d(0, ${translateY}px, 0)`,
    immediate: isTouching,
    config: {
      tension: 300,
      friction: 30,
    },
  })

  const setTranslateY = (y: number) => {
    y = Math.min(0, y)
    y = Math.max(y, (yearList.length - 1) * -dateItemHeight)
    setTranslateYState(y)
  }

  const onTouchStart: TouchEventHandler<HTMLDivElement> = (e) => {
    setIsTouching(true)
    setLastY(e.touches[0].clientY)
  }

  const onTouchMove: TouchEventHandler<HTMLDivElement> = (e) => {
    if (!isTouching) {
      return
    }
    const currentY = e.touches[0].clientY
    const deltaY = currentY - lastY
    setTranslateY(translateY + deltaY)
    setLastY(currentY)
  }

  const onTouchEnd: TouchEventHandler<HTMLDivElement> = (/* e */) => {
    const remainder = translateY % dateItemHeight
    let resultY = translateY - remainder
    if (Math.abs(remainder) > halfDateItemHeight) {
      resultY += dateItemHeight * (remainder > 0 ? 1 : -1)
    }
    setTranslateY(resultY)
    setIsTouching(false)
  }
  return (
    <div
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      style={itemHeightVarStyle}
      className={cn(
        'date-picker-wrapper h-[var(--total-height)] overflow-clip relative',
        className
      )}>
      {/*
      <div
        className={cn(
          'highlight-select-box h-[var(--date-item-height)] w-full',
          'absolute top-[calc(50%-var(--half-date-item-height))] left-0',
          'b-t-1.5 b-b-1.5 b-solid b-[#ccc]/50'
        )}></div>
         */}
      <div className="picker-positioner w-full absolute top-[calc(50%-var(--half-date-item-height))]">
        <animated.ol style={springStyle} className="year-picker text-center">
          {yearList.map((year) => (
            <li
              key={year}
              className="h-[var(--date-item-height)] leading-[var(--date-item-height)]">
              {year}
            </li>
          ))}
        </animated.ol>
      </div>
    </div>
  )
}

@/components/form/DatePickerPopup.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export const DatePickerPopup: React.FC<Props> = ({
  className,
  start,
  end,
  value,
  dateItemHeight = 36,
}) => {
  return (
    <div className={cn(className, 'flex relative')}>
      <DateTimeColumn
        className="grow-1"
        start={start}
        end={end}
        value={value}
        dateItemHeight={dateItemHeight}
      />
      <DateTimeColumn
        className="grow-1"
        start={start}
        end={end}
        value={value}
        dateItemHeight={dateItemHeight}
      />
      <DateTimeColumn
        className="grow-1"
        start={start}
        end={end}
        value={value}
        dateItemHeight={dateItemHeight}
      />
      <div
        className={cn(
          'picker-mask z-1 pointer-events-none w-full h-full',
          'absolute top-0 left-1em right-1em transform-gpu',
          'bg-[image:var(--picker-mask-color)] bg-no-repeat',
          'bg-[position:center_top,center_bottom] bg-[size:100%_110px]'
        )}></div>
      <div
        className={cn(
          'pricker-frame z-2 h-[36px] pointer-events-none',
          'absolute top-[50%] left-[1em] right-[1em] translate-y-[-50%]',
          'animation-rainbow after:(b-color-[var(--vp-c-brand-next)])',
          'after:(content-[""] b-y-2.5)',
          'after:(absolute top-[-50%] bottom-[-50%] left-[-50%] right-[-50%] scale-[.5])'
        )}></div>
    </div>
  )
}

2. useRef 配合 forceUpdate ⇧

添加月日列重,构组件 ⇧

重构 @/components/form/DateTimeColumn.tsx 解耦年份数据,只需要 dateTimeValue 和 dateTimeList

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { animated, useSpring } from '@react-spring/web'
import { cn } from 'cn'
import { TouchEventHandler, useState } from 'react'

interface ColumnProps {
  className?: string
  dateItemHeight?: number
  dateTimeValue: string
  dateTimeList: string[]
}

export const DateTimeColumn: React.FC<ColumnProps> = ({
  className,
  dateItemHeight = 36,
  dateTimeValue,
  dateTimeList,
}) => {
  const halfDateItemHeight = dateItemHeight / 2
  const itemHeightVarStyle = {
    '--date-item-height': `${dateItemHeight}px`,
    '--half-date-item-height': `${halfDateItemHeight}px`,
  }

  const currentDateIndex = dateTimeList.indexOf(dateTimeValue)
  if (currentDateIndex === -1) {
    throw new Error('dateTimeValue is not in dateTimeList')
  }

  const [isTouching, setIsTouching] = useState(false)
  const [lastY, setLastY] = useState(-1)
  const [translateY, setTranslateYState] = useState(currentDateIndex * -dateItemHeight)

  const springStyle = useSpring({
    transform: `translate3d(0, ${translateY}px, 0)`,
    immediate: isTouching,
    config: {
      tension: 300,
      friction: 30,
    },
  })

  const setTranslateY = (y: number) => {
    y = Math.min(0, y)
    y = Math.max(y, (dateTimeList.length - 1) * -dateItemHeight)
    setTranslateYState(y)
  }

  const onTouchStart: TouchEventHandler<HTMLDivElement> = (e) => {
    setIsTouching(true)
    setLastY(e.touches[0].clientY)
  }

  const onTouchMove: TouchEventHandler<HTMLDivElement> = (e) => {
    if (!isTouching) {
      return
    }
    const currentY = e.touches[0].clientY
    const deltaY = currentY - lastY
    setTranslateY(translateY + deltaY)
    setLastY(currentY)
  }

  const onTouchEnd: TouchEventHandler<HTMLDivElement> = (/* e */) => {
    const remainder = translateY % dateItemHeight
    let resultY = translateY - remainder
    if (Math.abs(remainder) > halfDateItemHeight) {
      resultY += dateItemHeight * (remainder > 0 ? 1 : -1)
    }
    setTranslateY(resultY)
    setIsTouching(false)
  }

  return (
    <div
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      style={itemHeightVarStyle}
      className={cn(
        'date-picker-column h-[var(--popup-height)] overflow-clip relative',
        className
      )}>
      <div className="picker-positioner w-full absolute top-[calc(50%-var(--half-date-item-height))]">
        <animated.ol style={springStyle} className="date-time-picker text-center">
          {dateTimeList.map((dateTimeStr) => (
            <li
              key={dateTimeStr}
              className="h-[var(--date-item-height)] leading-[var(--date-item-height)]">
              {dateTimeStr}
            </li>
          ))}
        </animated.ol>
      </div>
    </div>
  )
}
  • 容器总高度需要写死 h-[var(--popup-height)]

重构 @/components/form/DatePickerPopup.tsx 添加月份、日列表数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { cn } from 'cn'
import { DateTimeColumn } from '@/components/form/DateTimeColumn'
import { createDateTime } from '@/lib/helpers/DateTime'

interface Props {
  className?: string
  start?: Date
  end?: Date
  value?: Date
  dateItemHeight?: number
}

export const DatePickerPopup: React.FC<Props> = ({
  className,
  start,
  end,
  value,
  dateItemHeight = 36,
}) => {
  const startTime = start ? createDateTime(start) : createDateTime().add(-3, 'year')
  const endTime = end ? createDateTime(end) : createDateTime().add(3, 'year')
  if (startTime.getTimestamp() >= endTime.getTimestamp()) {
    throw new Error('startTime must be greater than endTime')
  }

  const valueTime = value ? createDateTime(value) : createDateTime()
  const yearList = Array.from({ length: endTime.year - startTime.year + 1 })
    .map((_, i) => startTime.year + i)
    .map((item) => item.toString())
  const monthList = Array.from({ length: 12 })
    .map((_, i) => i + 1)
    .map((item) => item.toString())
  const dayList = Array.from({ length: valueTime.lastDayOfMonth })
    .map((_, i) => i + 1)
    .map((item) => item.toString())

  return (
    <div className={cn(className, 'flex relative')}>
      <DateTimeColumn
        className="grow-1"
        dateTimeValue={valueTime.year.toString()}
        dateTimeList={yearList}
        dateItemHeight={dateItemHeight}
      />
      <DateTimeColumn
        className="grow-1"
        dateTimeValue={valueTime.month.toString()}
        dateTimeList={monthList}
        dateItemHeight={dateItemHeight}
      />
      <DateTimeColumn
        className="grow-1"
        dateTimeValue={valueTime.day.toString()}
        dateTimeList={dayList}
        dateItemHeight={dateItemHeight}
      />
      <div
        className={cn(
          'picker-mask z-1 pointer-events-none w-full h-full',
          'absolute top-0 left-1em right-1em transform-gpu',
          'bg-[image:var(--picker-mask-color)] bg-no-repeat',
          'bg-[position:center_top,center_bottom] bg-[size:100%_110px]'
        )}></div>
      <div
        className={cn(
          'pricker-frame z-2 h-[36px] pointer-events-none',
          'absolute top-[50%] left-[1em] right-[1em] translate-y-[-50%]',
          'animation-rainbow after:(b-color-[var(--vp-c-brand-next)])',
          'after:(content-[""] b-y-2.5)',
          'after:(absolute top-[-50%] bottom-[-50%] left-[-50%] right-[-50%] scale-[.5])'
        )}></div>
    </div>
  )
}

日期栏联动,onChange 触发日期栏切换 ⇧

@/components/form/DateTimeColumn.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// import ...

interface ColumnProps {
  className?: string
  dateItemHeight?: number
  dateTimeValue: string
  dateTimeList: string[]
  onChange: (value: string) => void
}

export const DateTimeColumn: React.FC<ColumnProps> = ({
  className,
  dateItemHeight = 36,
  dateTimeValue,
  dateTimeList,
  onChange,
}) => {
  const halfDateItemHeight = dateItemHeight / 2
  const itemHeightVarStyle = {/* ... */}
  const currentDateIndex = dateTimeList.indexOf(dateTimeValue)
  if (currentDateIndex === -1) {/* ... */}
  const [isTouching, setIsTouching] = useState(false)
  const [lastY, setLastY] = useState(-1)
  const [translateY, setTranslateYState] = useState(currentDateIndex * -dateItemHeight)
  const springStyle = useSpring({/* ... */})
  const setTranslateY = (y: number) => {/* ... */}
  const onTouchStart: TouchEventHandler<HTMLDivElement> = (e) => {/* ... */}
  const onTouchMove: TouchEventHandler<HTMLDivElement> = (e) => {/* ... */}
  const onTouchEnd: TouchEventHandler<HTMLDivElement> = (/* e */) => {
    const remainder = translateY % dateItemHeight
    let resultY = translateY - remainder
    if (Math.abs(remainder) > halfDateItemHeight) {
      resultY += dateItemHeight * (remainder > 0 ? 1 : -1)
    }
    setTranslateY(resultY)
    setIsTouching(false)
    onChange(dateTimeList[Math.abs(resultY / dateItemHeight)])
  }

  return (
    <div
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      style={itemHeightVarStyle}
      className={cn(/* ... */)}>
      <div className="picker-positioner w-full absolute top-[calc(50%-var(--half-date-item-height))]">
        <animated.ol style={springStyle} className="date-time-picker text-center">
          {dateTimeList.map((dateTimeStr) => (
            <li
              key={dateTimeStr}
              className="h-[var(--date-item-height)] leading-[var(--date-item-height)]">
              {dateTimeStr}
            </li>
          ))}
        </animated.ol>
      </div>
    </div>
  )
}
  • 组件接受父组件传参方法onChange
  • 在 onTouchEnd 时触发 onChange
  • onChange 时将当前时间选项值,通过计算 dateTimeList 的索引,传递给父组件
    • dateTimeList[Math.abs(resultY / dateItemHeight)]

使用 useRef + forceUpdate 改造 @/components/form/DatePickerPopup.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// import ...

interface Props {
  className?: string
  start?: Date
  end?: Date
  value?: Date
  dateItemHeight?: number
  onChange?: (value: Date) => void
}

export const DatePickerPopup: React.FC<Props> = ({
  className,
  start,
  end,
  value,
  dateItemHeight = 36,
  onChange,
}) => {
  const startTime = start ? createDateTime(start) : createDateTime().add(-3, 'year')
  const endTime = end ? createDateTime(end) : createDateTime().add(3, 'year')
  if (startTime.getTimestamp() >= endTime.getTimestamp()) {
    throw new Error('startTime must be greater than endTime')
  }

  const valueTime = useRef(value ? createDateTime(value) : createDateTime())
  const [_, forceUpdate] = useState({})

  const yearList = Array.from({ length: endTime.year - startTime.year + 1 })
    .map((_, i) => startTime.year + i)
    .map((item) => item.toString())
  const monthList = Array.from({ length: 12 })
    .map((_, i) => i + 1)
    .map((item) => item.toString())
  const dayList = Array.from({ length: valueTime.current.lastDayOfMonth })
    .map((_, i) => i + 1)
    .map((item) => item.toString())

  const onChangeYear = (value: string) => {
    valueTime.current.year = parseInt(value)
    forceUpdate({})
    onChange?.(valueTime.current.rawDate)
  }
  const onChangeMonth = (value: string) => {
    valueTime.current.month = parseInt(value)
    forceUpdate({})
    onChange?.(valueTime.current.rawDate)
  }
  const onChangeDay = (value: string) => {
    valueTime.current.day = parseInt(value)
    forceUpdate({})
    onChange?.(valueTime.current.rawDate)
  }

  return (
    <div className={cn(className, 'flex relative')}>
      {valueTime.current.format()}
      <DateTimeColumn
        className="grow-1"
        dateTimeValue={valueTime.current.year.toString()}
        dateTimeList={yearList}
        dateItemHeight={dateItemHeight}
        onChange={onChangeYear}
      />
      <DateTimeColumn
        className="grow-1"
        dateTimeValue={valueTime.current.month.toString()}
        dateTimeList={monthList}
        dateItemHeight={dateItemHeight}
        onChange={onChangeMonth}
      />
      <DateTimeColumn
        className="grow-1"
        dateTimeValue={valueTime.current.day.toString()}
        dateTimeList={dayList}
        dateItemHeight={dateItemHeight}
        onChange={onChangeDay}
      />
      <div className={cn( 'picker-mask', /* ... */ )}></div>
      <div className={cn('pricker-frame', /* ... */ )}></div>
    </div>
  )
}
  • 需要强行设置一个空对象的状态 const [_, forceUpdate] = useState({})
  • 每次更新组件只声明一次变量使用 useRef
    • const valueTime = useRef(value ? createDateTime(value) : createDateTime())
  • 当切换日期栏,每次都需要重新计算 dayList
  • 使用 useRef + forceUpdate
    • 值放到一个固定的对象上 const valueTime = useRef(value ? createDateTime(value) : createDateTime())
    • 声明强行更行更新方法 const [_, forceUpdate] = useState({})
    • 状态变更时添加 forceUpdate({})
    • const onChangeYear = (value) => {valueTime.current.year = ...; forceUpdate({})}

父组件 @/modules/items/input/DateInput.tsx 通过 onChange 拿到新的日期值,再设置到日期对象中 valueTime

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// import ...

export const DateInput: React.FC<Props> = ({ className }) => {
  const [currentDate, setCurrentDate] = useState<string>(genDateStr())
  const onChange = (value: Date) => {
    const dateStr = genDateStr(value)
    setCurrentDate(dateStr)
    console.log('DateInput onChange_______________________')
    console.log('value: ', value)
    console.log('dateStr: ', dateStr)
    console.log('_______________________DateInput onChange')
  }
  const { portalPopup, togglePopup } =
    usePopup(<DatePickerPopup onChange={onChange} />, true)

  return (
    <>
      {portalPopup}
      <span
        onClick={togglePopup}
        className={cn('flex justify-start items-center gap-x-[.5em] pr-1em', className)}>
        <SvgIcon name="salary-calendar" className="..." />
        <time
          dateTime={currentDate}
          className="grow-1 shrink-1 animation-rainbow text-[14px] font-mono color-[var(--vp-c-brand-next)]">
          {currentDate}
        </time>
      </span>
    </>
  )
}

目前还有 bug

  • 从闰年的 02-29 切换到平年时,实际日期到了 03-01
  • dateTimeValue 变化时,没有去更新 UI

date-picker-bug-1 date-picker-bug-2


3. value变化时更新偏移量y ⇧

date-picker-bug-3

Debug 调试精髓 @/components/form/DateTimeColumn.tsx

  • 未更新 UI 中的 currentDateIndex

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    
    import { animated, useSpring } from '@react-spring/web'
    import { cn } from 'cn'
    import { TouchEventHandler, useEffect, useState } from 'react'
    
    interface ColumnProps {
    className?: string
    dateItemHeight?: number
    dateTimeValue: string
    dateTimeList: string[]
    onChange: (value: string) => void
    }
    
    export const DateTimeColumn: React.FC<ColumnProps> = ({
    className,
    dateItemHeight = 36,
    dateTimeValue,
    dateTimeList,
    onChange,
    }) => {
    useEffect(() => {
    console.log('DateTimeColumn_______________________')
    console.log('%c⧭', 'color: #00ff88', dateTimeValue)
    console.log('_______________________DateTimeColumn')
    }, [dateTimeValue])
    
    const halfDateItemHeight = dateItemHeight / 2
    const itemHeightVarStyle = {/* ... */}
    
    const currentDateIndex = dateTimeList.indexOf(dateTimeValue)
    if (currentDateIndex === -1) {/* ... */}
    
    const [isTouching, setIsTouching] = useState(false)
    const [lastY, setLastY] = useState(-1)
    const [translateY, setTranslateYState] = useState(currentDateIndex * -dateItemHeight)
    const springStyle = useSpring({/* ... */})
    const setTranslateY = (y: number) => {/* ... */}
    const onTouchStart: TouchEventHandler<HTMLDivElement> = (e) => {/* ... */}
    const onTouchMove: TouchEventHandler<HTMLDivElement> = (e) => {/* ... */}
    const onTouchEnd: TouchEventHandler<HTMLDivElement> = (/* e */) => {/* ... */}
    
    return (
    <div
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      style={itemHeightVarStyle}
      className={cn(
        'date-picker-column h-[var(--popup-height)] overflow-clip relative',
        className
      )}>
      <div className="picker-positioner w-full absolute top-[calc(50%-var(--half-date-item-height))]">
        <animated.ol style={springStyle} className="date-time-picker text-center">
          {dateTimeList.map((dateTimeStr) => (
            <li
              key={dateTimeStr}
              className="h-[var(--date-item-height)] leading-[var(--date-item-height)]">
              {dateTimeStr}
            </li>
          ))}
        </animated.ol>
      </div>
    </div>
    )
    }
    
  • 需要使用 useEffect 监听 dateTimeValue 变化,重新计算 currentDateIndex,然后重新 setTranslateY

  • 同步 dateTimeValue 变化时的滚动偏移量位置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    export const DateTimeColumn: React.FC<ColumnProps> = ({
    className,
    dateItemHeight = 36,
    dateTimeValue,
    dateTimeList,
    onChange,
    }) => {
    const halfDateItemHeight = dateItemHeight / 2
    const itemHeightVarStyle = {/* ... */}
    const currentDateIndex = dateTimeList.indexOf(dateTimeValue)
    if (currentDateIndex === -1) {/* ... */}
    const [isTouching, setIsTouching] = useState(false)
    const [lastY, setLastY] = useState(-1)
    const [translateY, setTranslateYState] = useState(currentDateIndex * -dateItemHeight)
    const springStyle = useSpring({/* ... */})
    const setTranslateY = (y: number) => {
    y = Math.min(0, y)
    y = Math.max(y, (dateTimeList.length - 1) * -dateItemHeight)
    setTranslateYState(y)
    }
    
    useEffect(() => {
    console.log('DateTimeColumn_______________________')
    console.log('%c⧭', 'color: #00ff88', dateTimeValue)
    console.log('_______________________DateTimeColumn')
    
    const currentDateIndex = dateTimeList.indexOf(dateTimeValue)
    setTranslateY(currentDateIndex * -dateItemHeight)
    }, [dateTimeValue])
    
    // ...
    
    return (
    <div
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      style={itemHeightVarStyle}
      className={cn(/* ... */)}>
      {/* ... */}
    </div>
    )
    }
    

4. 确定和取消按钮 ⇧

@/components/form/DatePickerPopup.tsx

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { cn } from 'cn'
import { DateTimeColumn } from '@/components/form/DateTimeColumn'
import { createDateTime } from '@/lib/helpers/DateTime'
import { useRef, useState } from 'react'

interface Props {
  className?: string
  start?: Date
  end?: Date
  value?: Date
  dateItemHeight?: number
  onChange?: (value: Date) => void
  onCancel?: () => void
  onConfirm?: (dateTimeStr: string) => void
}

export const DatePickerPopup: React.FC<Props> = ({
  className,
  start,
  end,
  value,
  dateItemHeight = 36,
  onCancel,
  onConfirm,
}) => {
  const startTime = start ? createDateTime(start) : createDateTime().add(-3, 'year')
  const endTime = end ? createDateTime(end) : createDateTime().add(3, 'year')
  if (startTime.getTimestamp() >= endTime.getTimestamp()) {
    throw new Error('startTime must be greater than endTime')
  }

  const valueTime = useRef(value ? createDateTime(value) : createDateTime())
  const [_, forceUpdate] = useState({})

  const yearList = Array.from({ length: endTime.year - startTime.year + 1 })
    .map((_, i) => startTime.year + i)
    .map((item) => item.toString())
  const monthList = Array.from({ length: 12 })
    .map((_, i) => i + 1)
    .map((item) => item.toString())
  const dayList = Array.from({ length: valueTime.current.lastDayOfMonth })
    .map((_, i) => i + 1)
    .map((item) => item.toString())

  const onChangeYear = (value: string) => {
    valueTime.current.year = parseInt(value)
    forceUpdate({})
  }
  const onChangeMonth = (value: string) => {
    valueTime.current.month = parseInt(value)
    forceUpdate({})
  }
  const onChangeDay = (value: string) => {
    valueTime.current.day = parseInt(value)
    forceUpdate({})
  }

  return (
    <section className={className}>
      {/* 顶部按钮 */}
      <div
        className={cn(
          'flex justify-between p-[.5em]',
          'bg-white b-b-1 b-b-[#f3f3f3]',
          'children:(p-[.5em])'
        )}>
        <span onClick={onCancel}>取消</span>
        <span>日期选择</span>
        <span onClick={() => onConfirm?.(valueTime.current.format('YYYY-MM-DD'))}>确认</span>
      </div>

      {/* 日期选择 */}
      <div className={cn('flex relative')}>
        <DateTimeColumn
          className="grow-1"
          dateTimeValue={valueTime.current.year.toString()}
          dateTimeList={yearList}
          dateItemHeight={dateItemHeight}
          onChange={onChangeYear}
        />
        <DateTimeColumn
          className="grow-1"
          dateTimeValue={valueTime.current.month.toString()}
          dateTimeList={monthList}
          dateItemHeight={dateItemHeight}
          onChange={onChangeMonth}
        />
        <DateTimeColumn
          className="grow-1"
          dateTimeValue={valueTime.current.day.toString()}
          dateTimeList={dayList}
          dateItemHeight={dateItemHeight}
          onChange={onChangeDay}
        />
        <div
          className={cn(
            'picker-mask z-1 pointer-events-none w-full h-full',
            'absolute top-0 left-1em right-1em transform-gpu',
            'bg-[image:var(--picker-mask-color)] bg-no-repeat',
            'bg-[position:center_top,center_bottom] bg-[size:100%_110px]'
          )}></div>
        <div
          className={cn(
            'pricker-frame z-2 h-[36px] pointer-events-none',
            'absolute top-[50%] left-[1em] right-[1em] translate-y-[-50%]',
            'animation-rainbow after:(b-color-[var(--vp-c-brand-next)])',
            'after:(content-[""] b-y-2.5)',
            'after:(absolute top-[-50%] bottom-[-50%] left-[-50%] right-[-50%] scale-[.5])'
          )}></div>
      </div>
    </section>
  )
}
  • 将 onChange 改为确认/取消交互

@/modules/items/input/DateInput.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// import ...'

export const DateInput: React.FC<Props> = ({ className }) => {
  const [currentDate, setCurrentDate] = useState<string>(genDateStr())
  const onConfirm = (value: string) => {
    setCurrentDate(value)
    hidePopup?.()
  }

  const { portalPopup, togglePopup, hidePopup } = usePopup(
    <DatePickerPopup onConfirm={onConfirm} onCancel={() => hidePopup()} />,
    false
  )

  return (
    <>
      {portalPopup}
      <span
        onClick={togglePopup}
        className={cn('flex justify-start items-center gap-x-[.5em] pr-1em', className)}>
        <SvgIcon
          name="salary-calendar"
          className="grow-0 shrink-0 size-[2.5em] mx-1 pl-.5em" />
        <time
          dateTime={currentDate}
          className="grow-1 shrink-1 animation-rainbow text-[14px] font-mono color-[var(--vp-c-brand-next)]">
          {currentDate}
        </time>
      </span>
    </>
  )
}

5. 修复数据共享引起的bug ⇧

``

1



·未完待续·

参考文章

  • 无

相关文章

  • 无

  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名