File size: 2,883 Bytes
793e953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import React, { useState, useRef, useEffect } from 'react'

/**
 * SegmentedControl 多选项水平切换控件
 *
 * 用法:
 *   <SegmentedControl
 *     options={[
 *       { label: '全部', value: 'all' },
 *       { label: '草稿', value: 'draft' },
 *       { label: '终稿', value: 'final', disabled: true },
 *     ]}
 *     value={selected}
 *     onChange={(val) => setSelected(val)}
 *   />
 *
 * 选中项背景滑动动画(transform: translateX)
 * 支持 disabled 选项
 */

export interface SegmentOption {
  label: string
  value: string
  disabled?: boolean
}

interface SegmentedControlProps {
  options: SegmentOption[]
  value: string
  onChange: (value: string) => void
  className?: string
  size?: 'sm' | 'md'
}

export default function SegmentedControl({
  options,
  value,
  onChange,
  className = '',
  size = 'md',
}: SegmentedControlProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 })

  const sizeClasses = size === 'sm'
    ? 'px-2.5 py-1 text-xs'
    : 'px-3.5 py-2 text-sm'

  // 计算选中指示器位置
  useEffect(() => {
    if (!containerRef.current) return
    const activeIndex = options.findIndex((o) => o.value === value)
    const buttons = containerRef.current.querySelectorAll<HTMLButtonElement>('[data-segment]')
    if (activeIndex < 0 || !buttons[activeIndex]) return

    const containerRect = containerRef.current.getBoundingClientRect()
    const activeRect = buttons[activeIndex].getBoundingClientRect()

    setIndicatorStyle({
      left: activeRect.left - containerRect.left,
      width: activeRect.width,
    })
  }, [value, options])

  return (
    <div
      ref={containerRef}
      className={`relative inline-flex items-center bg-gray-100 rounded-lg p-0.5 ${className}`}
      role="radiogroup"
    >
      {/* 滑动背景指示器 */}
      <div
        className="absolute top-0.5 bottom-0.5 bg-white rounded-md shadow-sm transition-all duration-300 ease-out"
        style={{
          left: `${indicatorStyle.left}px`,
          width: `${indicatorStyle.width}px`,
        }}
      />
      {options.map((opt) => (
        <button
          key={opt.value}
          data-segment
          type="button"
          role="radio"
          aria-checked={value === opt.value}
          disabled={opt.disabled}
          onClick={() => !opt.disabled && onChange(opt.value)}
          className={`relative z-10 ${sizeClasses} font-medium rounded-md transition-colors duration-200 min-h-[32px] whitespace-nowrap ${
            opt.disabled
              ? 'text-gray-300 cursor-not-allowed'
              : value === opt.value
                ? 'text-gray-800'
                : 'text-gray-500 hover:text-gray-700 cursor-pointer'
          }`}
        >
          {opt.label}
        </button>
      ))}
    </div>
  )
}