Download vetcopilot-app/src/components/SegmentedControl.tsx from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 2.88 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/SegmentedControl.tsx
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/components/SegmentedControl.tsx
-
curl -L -o SegmentedControl.tsx https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/SegmentedControl.tsx
2.88 kB
| 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> | |
| ) | |
| } | |