2026-03-25 05:16:54 +08:00
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
2026-03-20 19:28:42 +08:00
|
|
|
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';
|
2026-03-24 14:21:31 +08:00
|
|
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
2026-04-13 01:30:37 +08:00
|
|
|
import { qrcodeApi } from '@/services/auth';
|
2026-03-25 05:16:54 +08:00
|
|
|
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
|
2026-03-24 14:21:31 +08:00
|
|
|
import { AppBackButton } from '../../components/common';
|
2026-03-20 19:28:42 +08:00
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
function createQrCodeConfirmStyles(colors: AppColors) {
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
container: {
|
|
|
|
|
flex: 1,
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
},
|
|
|
|
|
header: {
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
paddingVertical: spacing.sm,
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
borderBottomColor: colors.divider,
|
|
|
|
|
},
|
|
|
|
|
closeButton: {
|
|
|
|
|
padding: spacing.sm,
|
|
|
|
|
},
|
|
|
|
|
headerTitle: {
|
|
|
|
|
fontSize: fontSizes.lg,
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
},
|
|
|
|
|
placeholder: {
|
|
|
|
|
width: 40,
|
|
|
|
|
},
|
|
|
|
|
content: {
|
|
|
|
|
flex: 1,
|
|
|
|
|
padding: spacing.lg,
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
},
|
|
|
|
|
infoCard: {
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
padding: spacing.xl,
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
marginBottom: spacing.xl,
|
|
|
|
|
shadowColor: colors.chat.shadow,
|
|
|
|
|
shadowOffset: { width: 0, height: 2 },
|
|
|
|
|
shadowOpacity: 0.1,
|
|
|
|
|
shadowRadius: 4,
|
|
|
|
|
elevation: 3,
|
|
|
|
|
},
|
|
|
|
|
infoText: {
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
marginBottom: spacing.lg,
|
|
|
|
|
},
|
|
|
|
|
userInfo: {
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
},
|
|
|
|
|
avatar: {
|
|
|
|
|
width: 80,
|
|
|
|
|
height: 80,
|
|
|
|
|
borderRadius: 40,
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
},
|
|
|
|
|
avatarPlaceholder: {
|
|
|
|
|
width: 80,
|
|
|
|
|
height: 80,
|
|
|
|
|
borderRadius: 40,
|
|
|
|
|
backgroundColor: colors.background.disabled,
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
},
|
|
|
|
|
nickname: {
|
|
|
|
|
fontSize: fontSizes.lg,
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
},
|
|
|
|
|
buttonContainer: {
|
|
|
|
|
gap: spacing.md,
|
|
|
|
|
},
|
|
|
|
|
button: {
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
paddingHorizontal: spacing.xl,
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
},
|
|
|
|
|
confirmButton: {
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
},
|
|
|
|
|
confirmButtonText: {
|
|
|
|
|
color: colors.text.inverse,
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
},
|
|
|
|
|
cancelButton: {
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
borderColor: colors.divider,
|
|
|
|
|
},
|
|
|
|
|
cancelButtonText: {
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
},
|
|
|
|
|
loadingContainer: {
|
|
|
|
|
flex: 1,
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
},
|
|
|
|
|
loadingText: {
|
|
|
|
|
marginTop: spacing.md,
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
},
|
|
|
|
|
errorContainer: {
|
|
|
|
|
flex: 1,
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
padding: spacing.xl,
|
|
|
|
|
},
|
|
|
|
|
errorText: {
|
|
|
|
|
marginTop: spacing.md,
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
},
|
|
|
|
|
backButton: {
|
|
|
|
|
marginTop: spacing.xl,
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
paddingHorizontal: spacing.xl,
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
},
|
|
|
|
|
backButtonText: {
|
|
|
|
|
color: colors.text.inverse,
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:28:42 +08:00
|
|
|
export const QRCodeConfirmScreen: React.FC = () => {
|
2026-03-24 14:21:31 +08:00
|
|
|
const router = useRouter();
|
2026-03-25 05:16:54 +08:00
|
|
|
const colors = useAppColors();
|
|
|
|
|
const styles = useMemo(() => createQrCodeConfirmStyles(colors), [colors]);
|
2026-03-24 14:21:31 +08:00
|
|
|
const { sessionId: sessionIdParam } = useLocalSearchParams<{ sessionId?: string }>();
|
|
|
|
|
const sessionId = sessionIdParam || '';
|
2026-03-20 19:28:42 +08:00
|
|
|
|
|
|
|
|
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(() => {
|
2026-03-24 14:21:31 +08:00
|
|
|
if (!sessionId) return;
|
|
|
|
|
void handleScan();
|
|
|
|
|
}, [sessionId]);
|
2026-03-20 19:28:42 +08:00
|
|
|
|
|
|
|
|
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);
|
2026-03-25 05:16:54 +08:00
|
|
|
Alert.alert('扫码失败', errorMsg, [{ text: '确定', onPress: () => router.back() }]);
|
2026-03-20 19:28:42 +08:00
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleConfirm = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
await qrcodeApi.confirm(sessionId);
|
2026-03-25 05:16:54 +08:00
|
|
|
Alert.alert('登录成功', '网页端已登录', [{ text: '确定', onPress: () => router.back() }]);
|
2026-03-20 19:28:42 +08:00
|
|
|
} catch (err: any) {
|
|
|
|
|
const errorMsg = err.response?.data?.message || '确认登录失败';
|
|
|
|
|
Alert.alert('登录失败', errorMsg);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancel = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await qrcodeApi.cancel(sessionId);
|
2026-03-25 05:16:54 +08:00
|
|
|
} catch {
|
2026-03-20 19:28:42 +08:00
|
|
|
// 忽略取消错误
|
|
|
|
|
}
|
2026-03-24 14:21:31 +08:00
|
|
|
router.back();
|
2026-03-20 19:28:42 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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>
|
2026-03-24 14:21:31 +08:00
|
|
|
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
2026-03-20 19:28:42 +08:00
|
|
|
<Text style={styles.backButtonText}>返回</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</View>
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SafeAreaView style={styles.container}>
|
|
|
|
|
<View style={styles.header}>
|
2026-03-25 05:16:54 +08:00
|
|
|
<AppBackButton
|
|
|
|
|
onPress={handleCancel}
|
|
|
|
|
icon="close"
|
|
|
|
|
style={styles.closeButton}
|
|
|
|
|
iconColor={colors.text.secondary}
|
|
|
|
|
/>
|
2026-03-20 19:28:42 +08:00
|
|
|
<Text style={styles.headerTitle}>确认登录</Text>
|
|
|
|
|
<View style={styles.placeholder} />
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
<View style={styles.content}>
|
|
|
|
|
<View style={styles.infoCard}>
|
|
|
|
|
<Text style={styles.infoText}>您正在登录网页端</Text>
|
2026-03-25 05:16:54 +08:00
|
|
|
|
2026-03-20 19:28:42 +08:00
|
|
|
{userInfo && (
|
|
|
|
|
<View style={styles.userInfo}>
|
|
|
|
|
{userInfo.avatar ? (
|
|
|
|
|
<Image source={{ uri: userInfo.avatar }} style={styles.avatar} />
|
|
|
|
|
) : (
|
|
|
|
|
<View style={styles.avatarPlaceholder}>
|
2026-03-25 05:16:54 +08:00
|
|
|
<MaterialCommunityIcons name="account" size={40} color={colors.text.disabled} />
|
2026-03-20 19:28:42 +08:00
|
|
|
</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 ? (
|
2026-03-25 05:16:54 +08:00
|
|
|
<ActivityIndicator color={colors.text.inverse} />
|
2026-03-20 19:28:42 +08:00
|
|
|
) : (
|
|
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
export default QRCodeConfirmScreen;
|