/// /** * InputBase Component - Lynx 版 MUI InputBase * 100% 一比一复刻 MUI InputBase * * InputBase 是所有输入组件的基础,包含最少的样式 * 用于构建 Input, OutlinedInput, FilledInput 等 * * 对应 MUI: packages/mui-material/src/InputBase/InputBase.js */ import './InputBase.css' import inputBaseClasses, { getInputBaseUtilityClass } from './inputBaseClasses' export { inputBaseClasses, getInputBaseUtilityClass } // ============================================= // 类型定义 - 对应 MUI InputBase.d.ts // ============================================= export type InputBaseColor = 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' export type InputBaseSize = 'small' | 'medium' export type InputBaseMargin = 'dense' | 'none' export interface InputBaseProps { /** aria-describedby */ 'aria-describedby'?: string /** 自动完成 */ autoComplete?: string /** 自动聚焦 */ autoFocus?: boolean /** 自定义类名 */ className?: string /** 样式类覆盖 */ classes?: Partial /** 颜色 */ color?: InputBaseColor /** 默认值 */ defaultValue?: any /** 是否禁用 */ disabled?: boolean /** 结束装饰器 */ endAdornment?: any /** 是否错误状态 */ error?: boolean /** 是否全宽 */ fullWidth?: boolean /** ID */ id?: string /** 输入组件 */ inputComponent?: any /** 输入属性 */ inputProps?: Record /** 输入引用 */ inputRef?: any /** 边距 */ margin?: InputBaseMargin /** 最大行数 (多行时) */ maxRows?: number /** 最小行数 (多行时) */ minRows?: number /** 是否多行 */ multiline?: boolean /** 名称 */ name?: string /** 失焦事件 */ onBlur?: (event?: any) => void /** 值变化事件 */ onChange?: (event?: any) => void /** 点击事件 */ onClick?: (event?: any) => void /** 聚焦事件 */ onFocus?: (event?: any) => void /** 键盘按下事件 */ onKeyDown?: (event?: any) => void /** 键盘抬起事件 */ onKeyUp?: (event?: any) => void /** 占位符 */ placeholder?: string /** 是否只读 */ readOnly?: boolean /** 是否必填 */ required?: boolean /** 行数 (多行时) */ rows?: number /** 尺寸 */ size?: InputBaseSize /** 开始装饰器 */ startAdornment?: any /** 内联样式 */ style?: Record /** sx 属性 */ sx?: Record /** 输入类型 */ type?: string /** 值 */ value?: any /** 子元素 */ children?: any } // ============================================= // 辅助函数 // ============================================= 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 } /** * 检查输入是否有值 */ export function isFilled(obj: any, SSR = false): boolean { return ( obj && ((hasValue(obj.value) && obj.value !== '') || (SSR && hasValue(obj.defaultValue) && obj.defaultValue !== '')) ) } function hasValue(value: any): boolean { return value != null && !(Array.isArray(value) && value.length === 0) } // ============================================= // useUtilityClasses // ============================================= interface OwnerState extends InputBaseProps { focused?: boolean formControl?: boolean hiddenLabel?: boolean } function useUtilityClasses(ownerState: OwnerState) { const { classes, color = 'primary', disabled, error, endAdornment, focused, formControl, fullWidth, hiddenLabel, multiline, readOnly, size, startAdornment, type, } = ownerState const slots = { root: [ 'root', `color${capitalize(color)}`, disabled && 'disabled', error && 'error', fullWidth && 'fullWidth', focused && 'focused', formControl && 'formControl', size && size !== 'medium' && `size${capitalize(size)}`, multiline && 'multiline', startAdornment && 'adornedStart', endAdornment && 'adornedEnd', hiddenLabel && 'hiddenLabel', readOnly && 'readOnly', ], input: [ 'input', disabled && 'disabled', type === 'search' && 'inputTypeSearch', multiline && 'inputMultiline', size === 'small' && 'inputSizeSmall', hiddenLabel && 'inputHiddenLabel', startAdornment && 'inputAdornedStart', endAdornment && 'inputAdornedEnd', readOnly && 'readOnly', ], } return composeClasses(slots, getInputBaseUtilityClass, classes) } // ============================================= // InputBase 组件 - 完整实现 // ============================================= export function InputBase(props: InputBaseProps) { const { 'aria-describedby': ariaDescribedby, autoComplete, autoFocus, className, classes: classesProp, color = 'primary', defaultValue, disabled = false, endAdornment, error = false, fullWidth = false, id, inputComponent = 'input', inputProps = {}, inputRef, margin, maxRows, minRows, multiline = false, name, onBlur, onChange, onClick, onFocus, onKeyDown, onKeyUp, placeholder, readOnly, required, rows, size, startAdornment, style, sx, type = 'text', value, ...other } = props // 状态 const focused = false // 在实际使用中通过 useState 管理 const ownerState: OwnerState = { ...props, color, disabled, error, focused, formControl: false, fullWidth, hiddenLabel: false, multiline, size, type, } const classes = useUtilityClasses(ownerState) // 处理事件 const handleClick = (event: any) => { if (onClick) { onClick(event) } } const handleFocus = (event: any) => { if (onFocus) { onFocus(event) } } const handleBlur = (event: any) => { if (onBlur) { onBlur(event) } } const handleChange = (event: any) => { if (onChange) { onChange(event) } } // 构建根类名 const rootClasses = [ classes.root, className, disabled && inputBaseClasses.disabled, error && inputBaseClasses.error, focused && inputBaseClasses.focused, readOnly && 'MuiInputBase-readOnly', ].filter(Boolean).join(' ') // 构建输入类名 const inputClasses = [ classes.input, readOnly && 'MuiInputBase-readOnly', ].filter(Boolean).join(' ') // 输入属性 - 注意:Lynx 原生 input 不支持 value 属性(非受控组件) // 使用 defaultValue 作为初始值,通过 bindinput 事件跟踪变化 const inputElementProps = { 'aria-invalid': error, 'aria-describedby': ariaDescribedby, autoComplete, autoFocus, defaultValue: value !== undefined ? value : defaultValue, // 将 value 作为 defaultValue disabled, id, name, placeholder, readOnly, required, type: multiline ? undefined : type, // 注意:不传递 value 属性,因为 Lynx input 是非受控组件 ...inputProps, } return ( {startAdornment} {multiline ? (