feat: add QR code login and enhance chat experience
- 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
This commit is contained in:
@@ -188,7 +188,7 @@ export const LoginScreen: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* 品牌名称 - 优化大屏排版 */}
|
||||
<Text style={[styles.appName, isSplitLayout && styles.splitAppName]}>胡萝卜</Text>
|
||||
<Text style={[styles.appName, isSplitLayout && styles.splitAppName]}>萝卜社区</Text>
|
||||
<Text style={[styles.subtitle, isSplitLayout && styles.splitSubtitle]}>发现有趣的内容,结识志同道合的朋友</Text>
|
||||
|
||||
{/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */}
|
||||
@@ -431,7 +431,7 @@ export const LoginScreen: React.FC = () => {
|
||||
color="#FFF"
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.appName, { fontSize: appNameSize }]}>胡萝卜</Text>
|
||||
<Text style={[styles.appName, { fontSize: appNameSize }]}>萝卜社区</Text>
|
||||
<Text style={styles.subtitle}>发现有趣的内容,结识志同道合的朋友</Text>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
288
src/screens/auth/QRCodeConfirmScreen.tsx
Normal file
288
src/screens/auth/QRCodeConfirmScreen.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
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;
|
||||
@@ -268,8 +268,8 @@ export const RegisterScreen: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* 品牌名称 - 优化大屏排版 */}
|
||||
<Text style={[styles.title, isSplitLayout && styles.splitTitle]}>创建账号</Text>
|
||||
<Text style={[styles.subtitle, isSplitLayout && styles.splitSubtitle]}>加入胡萝卜,开始你的旅程</Text>
|
||||
<Text style={[styles.title, isSplitLayout && styles.splitTitle]}>加入萝卜社区</Text>
|
||||
<Text style={[styles.subtitle, isSplitLayout && styles.splitSubtitle]}>加入萝卜社区,发现有趣的内容</Text>
|
||||
|
||||
{/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */}
|
||||
<View style={[styles.leftPanelDecorBottom, isSplitLayout && styles.splitLeftPanelDecorBottom]}>
|
||||
@@ -720,8 +720,8 @@ export const RegisterScreen: React.FC = () => {
|
||||
color="#FFF"
|
||||
/>
|
||||
</View>
|
||||
<Text style={[styles.title, { fontSize: titleSize }]}>创建账号</Text>
|
||||
<Text style={styles.subtitle}>加入胡萝卜,开始你的旅程</Text>
|
||||
<Text style={[styles.title, { fontSize: titleSize }]}>加入萝卜社区</Text>
|
||||
<Text style={styles.subtitle}>加入萝卜社区,发现有趣的内容</Text>
|
||||
</Animated.View>
|
||||
|
||||
{/* 表单 */}
|
||||
|
||||
Reference in New Issue
Block a user