- Add QR code login flow with scanner and confirmation screens - Implement swipe-to-reply in chat with SwipeableMessageBubble - Replace FlatList with FlashList for chat performance optimization - Add Schedule stack navigator with CourseDetail screen support - Configure deep linking for QR code login (carrotbbs://qrcode/login/:sessionId) - Update branding from 胡萝卜 to 萝卜社区 - Display dynamic app version in settings
288 lines
7.5 KiB
TypeScript
288 lines
7.5 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Image,
|
|
ActivityIndicator,
|
|
Alert,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { RootStackParamList } from '../../navigation/types';
|
|
import { qrcodeApi } from '../../services/authService';
|
|
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
|
|
|
type QRCodeConfirmRouteProp = RouteProp<RootStackParamList, 'QRCodeConfirm'>;
|
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
|
|
|
export const QRCodeConfirmScreen: React.FC = () => {
|
|
const route = useRoute<QRCodeConfirmRouteProp>();
|
|
const navigation = useNavigation<NavigationProp>();
|
|
const { sessionId } = route.params;
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [userInfo, setUserInfo] = useState<{ id: string; nickname: string; avatar: string } | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
// 扫码
|
|
handleScan();
|
|
}, []);
|
|
|
|
const handleScan = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await qrcodeApi.scan(sessionId);
|
|
setUserInfo(response.user);
|
|
} catch (err: any) {
|
|
const errorMsg = err.response?.data?.message || '扫码失败';
|
|
setError(errorMsg);
|
|
Alert.alert('扫码失败', errorMsg, [
|
|
{ text: '确定', onPress: () => navigation.goBack() }
|
|
]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleConfirm = async () => {
|
|
try {
|
|
setLoading(true);
|
|
await qrcodeApi.confirm(sessionId);
|
|
Alert.alert('登录成功', '网页端已登录', [
|
|
{ text: '确定', onPress: () => navigation.goBack() }
|
|
]);
|
|
} catch (err: any) {
|
|
const errorMsg = err.response?.data?.message || '确认登录失败';
|
|
Alert.alert('登录失败', errorMsg);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async () => {
|
|
try {
|
|
await qrcodeApi.cancel(sessionId);
|
|
} catch (err) {
|
|
// 忽略取消错误
|
|
}
|
|
navigation.goBack();
|
|
};
|
|
|
|
if (loading && !userInfo) {
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
<Text style={styles.loadingText}>正在扫码...</Text>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.errorContainer}>
|
|
<MaterialCommunityIcons name="alert-circle" size={64} color={colors.error.main} />
|
|
<Text style={styles.errorText}>{error}</Text>
|
|
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
|
|
<Text style={styles.backButtonText}>返回</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<View style={styles.header}>
|
|
<TouchableOpacity onPress={handleCancel} style={styles.closeButton}>
|
|
<MaterialCommunityIcons name="close" size={24} color="#666" />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>确认登录</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
|
|
<View style={styles.content}>
|
|
<View style={styles.infoCard}>
|
|
<Text style={styles.infoText}>您正在登录网页端</Text>
|
|
|
|
{userInfo && (
|
|
<View style={styles.userInfo}>
|
|
{userInfo.avatar ? (
|
|
<Image source={{ uri: userInfo.avatar }} style={styles.avatar} />
|
|
) : (
|
|
<View style={styles.avatarPlaceholder}>
|
|
<MaterialCommunityIcons name="account" size={40} color="#999" />
|
|
</View>
|
|
)}
|
|
<Text style={styles.nickname}>{userInfo.nickname}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.buttonContainer}>
|
|
<TouchableOpacity
|
|
style={[styles.button, styles.confirmButton]}
|
|
onPress={handleConfirm}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<ActivityIndicator color="#fff" />
|
|
) : (
|
|
<Text style={styles.confirmButtonText}>确认登录</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.button, styles.cancelButton]}
|
|
onPress={handleCancel}
|
|
disabled={loading}
|
|
>
|
|
<Text style={styles.cancelButtonText}>取消</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#f5f5f5',
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
backgroundColor: '#fff',
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#eee',
|
|
},
|
|
closeButton: {
|
|
padding: spacing.sm,
|
|
},
|
|
headerTitle: {
|
|
fontSize: fontSizes.lg,
|
|
fontWeight: '600',
|
|
color: '#333',
|
|
},
|
|
placeholder: {
|
|
width: 40,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
padding: spacing.lg,
|
|
justifyContent: 'center',
|
|
},
|
|
infoCard: {
|
|
backgroundColor: '#fff',
|
|
borderRadius: borderRadius.lg,
|
|
padding: spacing.xl,
|
|
alignItems: 'center',
|
|
marginBottom: spacing.xl,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
infoText: {
|
|
fontSize: fontSizes.md,
|
|
color: '#666',
|
|
marginBottom: spacing.lg,
|
|
},
|
|
userInfo: {
|
|
alignItems: 'center',
|
|
},
|
|
avatar: {
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: 40,
|
|
marginBottom: spacing.md,
|
|
},
|
|
avatarPlaceholder: {
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: 40,
|
|
backgroundColor: '#f0f0f0',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: spacing.md,
|
|
},
|
|
nickname: {
|
|
fontSize: fontSizes.lg,
|
|
fontWeight: '600',
|
|
color: '#333',
|
|
},
|
|
buttonContainer: {
|
|
gap: spacing.md,
|
|
},
|
|
button: {
|
|
paddingVertical: spacing.md,
|
|
paddingHorizontal: spacing.xl,
|
|
borderRadius: borderRadius.md,
|
|
alignItems: 'center',
|
|
},
|
|
confirmButton: {
|
|
backgroundColor: colors.primary.main,
|
|
},
|
|
confirmButtonText: {
|
|
color: '#fff',
|
|
fontSize: fontSizes.md,
|
|
fontWeight: '600',
|
|
},
|
|
cancelButton: {
|
|
backgroundColor: '#fff',
|
|
borderWidth: 1,
|
|
borderColor: '#ddd',
|
|
},
|
|
cancelButtonText: {
|
|
color: '#666',
|
|
fontSize: fontSizes.md,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
loadingText: {
|
|
marginTop: spacing.md,
|
|
fontSize: fontSizes.md,
|
|
color: '#666',
|
|
},
|
|
errorContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: spacing.xl,
|
|
},
|
|
errorText: {
|
|
marginTop: spacing.md,
|
|
fontSize: fontSizes.md,
|
|
color: '#666',
|
|
textAlign: 'center',
|
|
},
|
|
backButton: {
|
|
marginTop: spacing.xl,
|
|
paddingVertical: spacing.md,
|
|
paddingHorizontal: spacing.xl,
|
|
backgroundColor: colors.primary.main,
|
|
borderRadius: borderRadius.md,
|
|
},
|
|
backButtonText: {
|
|
color: '#fff',
|
|
fontSize: fontSizes.md,
|
|
fontWeight: '600',
|
|
},
|
|
});
|
|
|
|
export default QRCodeConfirmScreen; |