VetNet-LCPS / vetcopilot-app /src /components /SegmentedControl.tsx
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
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>
)
}