feat(auth): implement multi-step registration process and enhance background services
- Updated RegisterScreen to support a multi-step registration flow with components for email input, verification, and profile setup. - Added new components: VerificationCodeInput, StepIndicator, and corresponding types for step management. - Enhanced background service to check authentication status before executing tasks, improving app reliability. - Updated TypeScript configuration to include module resolution and JSON module support. This update significantly improves the user registration experience and ensures background tasks are only executed for authenticated users.
This commit is contained in:
200
src/components/common/StepIndicator.tsx
Normal file
200
src/components/common/StepIndicator.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 步骤指示器组件 - 极简扁平设计
|
||||
* 与下方表单风格统一
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { useAppColors, type AppColors } from '../../theme';
|
||||
|
||||
export interface Step {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
steps: Step[];
|
||||
currentStep: number;
|
||||
containerStyle?: ViewStyle;
|
||||
}
|
||||
|
||||
export const StepIndicator: React.FC<StepIndicatorProps> = ({
|
||||
steps,
|
||||
currentStep,
|
||||
containerStyle,
|
||||
}) => {
|
||||
const colors = useAppColors();
|
||||
const styles = createStyles(colors);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep;
|
||||
const isCurrent = index === currentStep;
|
||||
const isPending = index > currentStep;
|
||||
const isLast = index === steps.length - 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={step.id}>
|
||||
<View style={styles.stepWrapper}>
|
||||
{/* 步骤编号/状态 */}
|
||||
<View
|
||||
style={[
|
||||
styles.stepBadge,
|
||||
isCompleted && styles.stepBadgeCompleted,
|
||||
isCurrent && styles.stepBadgeCurrent,
|
||||
isPending && styles.stepBadgePending,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.stepBadgeText,
|
||||
isCompleted && styles.stepBadgeTextCompleted,
|
||||
isCurrent && styles.stepBadgeTextCurrent,
|
||||
isPending && styles.stepBadgeTextPending,
|
||||
]}
|
||||
>
|
||||
{isCompleted ? '✓' : index + 1}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 步骤标题 */}
|
||||
<Text
|
||||
style={[
|
||||
styles.stepTitle,
|
||||
isCompleted && styles.stepTitleCompleted,
|
||||
isCurrent && styles.stepTitleCurrent,
|
||||
isPending && styles.stepTitlePending,
|
||||
]}
|
||||
>
|
||||
{step.title}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 连接线 */}
|
||||
{!isLast && (
|
||||
<View style={styles.connector}>
|
||||
<View
|
||||
style={[
|
||||
styles.connectorLine,
|
||||
isCompleted && styles.connectorLineCompleted,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 注册流程专用步骤指示器
|
||||
export const RegisterStepIndicator: React.FC<{
|
||||
currentStep: number;
|
||||
containerStyle?: ViewStyle;
|
||||
}> = ({ currentStep, containerStyle }) => {
|
||||
const steps: Step[] = [
|
||||
{ id: 0, title: '输入邮箱' },
|
||||
{ id: 1, title: '验证码' },
|
||||
{ id: 2, title: '设置信息' },
|
||||
];
|
||||
|
||||
return (
|
||||
<StepIndicator
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
containerStyle={containerStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function createStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
stepWrapper: {
|
||||
alignItems: 'center',
|
||||
minWidth: 80,
|
||||
},
|
||||
// 步骤徽章
|
||||
stepBadge: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#F5F5F7',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 0,
|
||||
},
|
||||
stepBadgeCompleted: {
|
||||
backgroundColor: '#FF6B35',
|
||||
},
|
||||
stepBadgeCurrent: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FF6B35',
|
||||
},
|
||||
stepBadgePending: {
|
||||
backgroundColor: '#F5F5F7',
|
||||
},
|
||||
stepBadgeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#999',
|
||||
},
|
||||
stepBadgeTextCompleted: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
},
|
||||
stepBadgeTextCurrent: {
|
||||
color: '#FF6B35',
|
||||
},
|
||||
stepBadgeTextPending: {
|
||||
color: '#BBB',
|
||||
},
|
||||
// 步骤标题
|
||||
stepTitle: {
|
||||
marginTop: 8,
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
},
|
||||
stepTitleCompleted: {
|
||||
color: '#FF6B35',
|
||||
fontWeight: '600',
|
||||
},
|
||||
stepTitleCurrent: {
|
||||
color: '#333',
|
||||
fontWeight: '600',
|
||||
},
|
||||
stepTitlePending: {
|
||||
color: '#BBB',
|
||||
},
|
||||
// 连接线
|
||||
connector: {
|
||||
width: 60,
|
||||
height: 28,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
connectorLine: {
|
||||
width: '100%',
|
||||
height: 2,
|
||||
backgroundColor: '#E5E5E5',
|
||||
borderRadius: 1,
|
||||
},
|
||||
connectorLineCompleted: {
|
||||
backgroundColor: '#FF6B35',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default StepIndicator;
|
||||
256
src/components/common/VerificationCodeInput.tsx
Normal file
256
src/components/common/VerificationCodeInput.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* 验证码输入组件
|
||||
* 参考设计: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 {
|
||||
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`, // 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;
|
||||
@@ -32,3 +32,8 @@ export type { ImageGalleryProps, GalleryImageItem } from './ImageGallery';
|
||||
export type { ResponsiveGridProps } from './ResponsiveGrid';
|
||||
export type { ResponsiveStackProps, StackDirection, StackAlignment, StackJustify } from './ResponsiveStack';
|
||||
export type { AdaptiveLayoutProps, SidebarLayoutProps } from './AdaptiveLayout';
|
||||
|
||||
// 注册流程相关组件
|
||||
export { default as VerificationCodeInput } from './VerificationCodeInput';
|
||||
export { default as StepIndicator, RegisterStepIndicator } from './StepIndicator';
|
||||
export type { Step, StepIndicatorProps } from './StepIndicator';
|
||||
|
||||
Reference in New Issue
Block a user