Files
frontend/src/components/common/VerificationCodeInput.tsx

257 lines
7.0 KiB
TypeScript
Raw Normal View History

/**
*
* 4-6
* 退
*/
import React, { useRef, useEffect, useCallback } from 'react';
import {
View,
TextInput,
StyleSheet,
Keyboard,
Platform,
Text,
ViewStyle,
} from 'react-native';
import { useAppColors, type AppColors } from '../../theme';
interface VerificationCodeInputProps {
value: string;
onChangeValue: (value: string) => void;
length?: number; // 验证码位数默认4位
autoFocus?: boolean;
onComplete?: (value: string) => void; // 输入完成回调
containerStyle?: ViewStyle;
disabled?: boolean;
}
const CELL_WIDTH = 56;
const CELL_HEIGHT = 64;
const CELL_MARGIN = 8;
export const VerificationCodeInput: React.FC<VerificationCodeInputProps> = ({
value,
onChangeValue,
length = 4,
autoFocus = true,
onComplete,
containerStyle,
disabled = false,
}) => {
const colors = useAppColors();
const styles = createStyles(colors);
// 为每个数字创建一个 ref
const inputRefs = useRef<(TextInput | null)[]>(Array(length).fill(null));
// 将 value 分割为单个字符数组
const chars = value.split('').slice(0, length);
// 填充空位
while (chars.length < length) {
chars.push('');
}
// 获取当前聚焦的位置
const getFocusedIndex = useCallback(() => {
return Math.min(value.length, length - 1);
}, [value.length, length]);
// 聚焦到指定位置
const focusInput = useCallback((index: number) => {
if (index >= 0 && index < length) {
inputRefs.current[index]?.focus();
}
}, [length]);
// 自动聚焦第一个输入框
useEffect(() => {
if (autoFocus && !disabled) {
const timer = setTimeout(() => {
focusInput(0);
}, 100);
return () => clearTimeout(timer);
}
}, [autoFocus, disabled, focusInput]);
// 当值改变时,自动聚焦到下一个输入框
useEffect(() => {
if (value.length < length) {
focusInput(value.length);
}
// 当输入完成时触发回调
if (value.length === length && onComplete) {
onComplete(value);
}
}, [value, length, onComplete, focusInput]);
// 处理文本变化
const handleChangeText = (text: string, index: number) => {
if (disabled) return;
// 只允许数字
const numericText = text.replace(/[^0-9]/g, '');
if (numericText.length === 0) {
// 删除操作
if (value.length > index) {
// 删除当前位置的字符
const newValue = value.slice(0, index) + value.slice(index + 1);
onChangeValue(newValue);
// 聚焦到前一个输入框
if (index > 0) {
focusInput(index - 1);
}
}
} else if (numericText.length === 1) {
// 单个字符输入
let newValue: string;
if (index < value.length) {
// 替换已有字符
newValue = value.slice(0, index) + numericText + value.slice(index + 1);
} else {
// 追加新字符
newValue = value + numericText;
}
onChangeValue(newValue.slice(0, length));
// 自动聚焦到下一个
if (index < length - 1) {
focusInput(index + 1);
}
} else if (numericText.length > 1) {
// 粘贴操作:尝试将粘贴的内容作为完整验证码
const newValue = numericText.slice(0, length);
onChangeValue(newValue);
// 聚焦到最后一个或下一个
const focusIndex = Math.min(newValue.length, length - 1);
focusInput(focusIndex);
}
};
// 处理按键事件(用于删除键)
const handleKeyPress = (e: any, index: number) => {
if (disabled) return;
if (e.nativeEvent.key === 'Backspace') {
if (value.length > index) {
// 删除当前位置的字符
const newValue = value.slice(0, index) + value.slice(index + 1);
onChangeValue(newValue);
} else if (index > 0 && value.length === index) {
// 当前位置没有字符,删除前一个
const newValue = value.slice(0, -1);
onChangeValue(newValue);
focusInput(index - 1);
}
}
};
// 聚焦时全选文本
const handleFocus = (index: number) => {
if (disabled) return;
inputRefs.current[index]?.setNativeProps({
selection: { start: 0, end: 1 },
});
};
return (
<View style={[styles.container, containerStyle]}>
<View style={styles.cellsContainer}>
{chars.map((char, index) => {
const isFocused = value.length === index;
const isFilled = char !== '';
const isLastFilled = index === value.length - 1 && isFilled;
return (
<View
key={index}
style={[
styles.cell,
isFocused && styles.cellFocused,
isFilled && styles.cellFilled,
disabled && styles.cellDisabled,
]}
>
<TextInput
ref={(ref) => {
inputRefs.current[index] = ref;
}}
style={styles.cellInput}
value={char}
onChangeText={(text) => handleChangeText(text, index)}
onKeyPress={(e) => handleKeyPress(e, index)}
onFocus={() => handleFocus(index)}
keyboardType="number-pad"
maxLength={1}
selectTextOnFocus
editable={!disabled}
caretHidden={true}
textContentType="oneTimeCode" // iOS 自动填充支持
/>
{/* 光标指示器 */}
{isFocused && !isFilled && !disabled && (
<View style={styles.cursor} />
)}
</View>
);
})}
</View>
</View>
);
};
function createStyles(colors: AppColors) {
return StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
cellsContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
cell: {
width: CELL_WIDTH,
height: CELL_HEIGHT,
borderRadius: 12,
borderWidth: 1.5,
borderColor: colors.divider,
backgroundColor: colors.background.paper,
marginHorizontal: CELL_MARGIN / 2,
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
},
cellFocused: {
borderColor: colors.primary.main,
backgroundColor: `${colors.primary.main}10`,
},
cellFilled: {
borderColor: colors.primary.main,
},
cellDisabled: {
opacity: 0.5,
backgroundColor: colors.background.disabled,
},
cellInput: {
width: '100%',
height: '100%',
textAlign: 'center',
fontSize: 28,
fontWeight: '600',
color: colors.text.primary,
padding: 0,
},
cursor: {
position: 'absolute',
width: 2,
height: 28,
backgroundColor: colors.primary.main,
borderRadius: 1,
},
});
}
export default VerificationCodeInput;