/// /** * Slider Component - Lynx 版 MUI Slider * 100% 一比一复刻 MUI Slider (简化版) * * 滑块组件,支持点击轨道调整数值 * 在 Lynx 环境下使用点击交互替代拖动交互 * * 对应 MUI: packages/mui-material/src/Slider/Slider.js */ import './Slider.css' import sliderClasses, { getSliderUtilityClass } from './sliderClasses' export { sliderClasses, getSliderUtilityClass } // ============================================= // 类型定义 // ============================================= export type SliderOrientation = 'horizontal' | 'vertical' export type SliderSize = 'small' | 'medium' export type SliderColor = 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' export interface SliderMark { value: number label?: string } export interface SliderProps { /** 自定义类名 */ className?: string /** 样式类覆盖 */ classes?: Partial /** 默认值 */ defaultValue?: number /** 是否禁用 */ disabled?: boolean /** 标记点 */ marks?: boolean | SliderMark[] /** 最大值 */ max?: number /** 最小值 */ min?: number /** 方向 */ orientation?: SliderOrientation /** 尺寸 */ size?: SliderSize /** 步长 */ step?: number /** 轨轨反转 */ track?: 'normal' | 'inverted' | false /** 当前值 */ value?: number /** 是否显示值标签 */ valueLabelDisplay?: 'on' | 'auto' | 'off' /** 值标签格式化 */ valueLabelFormat?: string | ((value: number) => string) /** 内联样式 */ style?: Record /** sx 属性 */ sx?: Record /** 变化事件 */ onChange?: (event: any, value: number) => void onChangeCommitted?: (event: any, value: number) => void } // ============================================= // 辅助函数 // ============================================= function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1) } function composeClasses( slots: Record, getUtilityClass: (slot: string) => string, classes?: Record ): Record { const output: Record = {} Object.keys(slots).forEach((slot) => { output[slot] = slots[slot] .filter(Boolean) .map((key) => { if (classes && classes[key as string]) { return `${getUtilityClass(key as string)} ${classes[key as string]}` } return getUtilityClass(key as string) }) .join(' ') }) return output } function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max) } function roundValueToPrecision(value: number, precision: number): number { if (precision === 0) return value const nearest = Math.round(value / precision) * precision return Number(nearest.toFixed(10)) } function valueToPercent(value: number, min: number, max: number): number { return ((value - min) * 100) / (max - min) } function percentToValue(percent: number, min: number, max: number): number { return min + (max - min) * percent } // ============================================= // useUtilityClasses // ============================================= interface OwnerState extends SliderProps { marked?: boolean } function useUtilityClasses(ownerState: OwnerState) { const { classes, disabled, marked, orientation, track } = ownerState const slots = { root: [ 'root', disabled && 'disabled', marked && 'marked', orientation, track === 'inverted' && 'trackInverted', track === false && 'trackFalse', ], rail: ['rail'], track: ['track'], thumb: ['thumb'], valueLabel: ['valueLabel'], mark: ['mark'], markActive: ['markActive'], markLabel: ['markLabel'], markLabelActive: ['markLabelActive'], } return composeClasses(slots, getSliderUtilityClass, classes) } // ============================================= // Slider 组件 // ============================================= export function Slider(props: SliderProps) { const { className, classes: classesProp, defaultValue = 0, disabled = false, marks = false, max = 100, min = 0, orientation = 'horizontal', size = 'medium', step = 1, track = 'normal', value: valueProp, valueLabelDisplay = 'off', valueLabelFormat, style, sx, onChange, onChangeCommitted, ...other } = props // 在 Lynx 环境下,组件必须是受控的 // 如果没有提供 value 属性,使用默认值 const value = valueProp !== undefined ? valueProp : defaultValue const ownerState: OwnerState = { ...props, value, disabled, marks, orientation, track, } const classes = useUtilityClasses(ownerState) // 处理轨道点击 const handleTrackTap = (event: any) => { if (disabled) return // 在 Lynx 环境下,简化处理:点击轨道时跳转到最近的步长值 // 这里使用简化的逻辑,实际应用中可能需要根据点击位置计算 const newValue = roundValueToPrecision(value, step) const clampedValue = clamp(newValue, min, max) if (onChange) { onChange(event, clampedValue) } if (onChangeCommitted) { onChangeCommitted(event, clampedValue) } } // 处理滑块点击 const handleThumbTap = (event: any) => { if (disabled) return // 在 Lynx 环境下,简化处理:点击滑块时增加一个步长 const newValue = roundValueToPrecision(value + step, step) const clampedValue = clamp(newValue, min, max) if (onChange) { onChange(event, clampedValue) } if (onChangeCommitted) { onChangeCommitted(event, clampedValue) } } // 格式化值标签 const formatValueLabel = (val: number): string => { if (typeof valueLabelFormat === 'function') { return valueLabelFormat(val) } if (typeof valueLabelFormat === 'string') { return valueLabelFormat.replace('{value}', val.toString()) } return val.toString() } // 计算滑块位置百分比 const percent = valueToPercent(value, min, max) const trackPercent = track === 'inverted' ? 100 - percent : percent // 渲染标记点 const renderMarks = () => { if (!marks) return null const marksArray = Array.isArray(marks) ? marks : [] const markElements: any[] = [] marksArray.forEach((mark, index) => { const markPercent = valueToPercent(mark.value, min, max) const isActive = mark.value <= value markElements.push( ) if (mark.label) { markElements.push( {mark.label} ) } }) return markElements } // 构建类名 const rootClasses = [ classes.root, sliderClasses.root, className, disabled && sliderClasses.disabled, marks && sliderClasses.marked, orientation === 'vertical' && sliderClasses.vertical, track === 'inverted' && sliderClasses.trackInverted, track === false && sliderClasses.trackFalse, ].filter(Boolean).join(' ') return ( {/* 轨道 */} {/* 已填充轨道 */} {/* 标记点 */} {renderMarks()} {/* 滑块 */} {/* 值标签 */} {valueLabelDisplay !== 'off' && ( {formatValueLabel(value)} )} ) } export default Slider