///
/**
* 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