- 新增训练计划页面,允许用户制定个性化的训练计划 - 集成打卡功能,用户可以记录每日的训练情况 - 更新 Redux 状态管理,添加训练计划相关的 reducer - 在首页中添加训练计划卡片,支持用户点击跳转 - 更新样式和布局,以适应新功能的展示和交互 - 添加日期选择器和相关依赖,支持用户选择训练日期
254 lines
8.9 KiB
TypeScript
254 lines
8.9 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons';
|
|
import * as AppleAuthentication from 'expo-apple-authentication';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { Alert, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
import { ThemedText } from '@/components/ThemedText';
|
|
import { ThemedView } from '@/components/ThemedView';
|
|
import { Colors } from '@/constants/Colors';
|
|
import { useAppDispatch } from '@/hooks/redux';
|
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
import { login } from '@/store/userSlice';
|
|
|
|
export default function LoginScreen() {
|
|
const router = useRouter();
|
|
const searchParams = useLocalSearchParams<{ redirectTo?: string; redirectParams?: string }>();
|
|
const scheme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
|
const color = Colors[scheme];
|
|
const dispatch = useAppDispatch();
|
|
|
|
const [hasAgreed, setHasAgreed] = useState<boolean>(false);
|
|
const [appleAvailable, setAppleAvailable] = useState<boolean>(false);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
AppleAuthentication.isAvailableAsync().then(setAppleAvailable).catch(() => setAppleAvailable(false));
|
|
}, []);
|
|
|
|
const guardAgreement = useCallback((action: () => void) => {
|
|
if (!hasAgreed) {
|
|
Alert.alert('请先阅读并同意', '勾选“我已阅读并同意用户协议与隐私政策”后才可继续登录');
|
|
return;
|
|
}
|
|
action();
|
|
}, [hasAgreed]);
|
|
|
|
const onAppleLogin = useCallback(async () => {
|
|
if (!appleAvailable) return;
|
|
try {
|
|
setLoading(true);
|
|
const credential = await AppleAuthentication.signInAsync({
|
|
requestedScopes: [
|
|
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
|
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
|
],
|
|
});
|
|
const identityToken = (credential as any)?.identityToken;
|
|
await dispatch(login({ appleIdentityToken: identityToken })).unwrap();
|
|
// 登录成功后处理重定向
|
|
const to = searchParams?.redirectTo as string | undefined;
|
|
const paramsJson = searchParams?.redirectParams as string | undefined;
|
|
let parsedParams: Record<string, any> | undefined;
|
|
if (paramsJson) {
|
|
try { parsedParams = JSON.parse(paramsJson); } catch { }
|
|
}
|
|
if (to) {
|
|
router.replace({ pathname: to, params: parsedParams } as any);
|
|
} else {
|
|
router.back();
|
|
}
|
|
} catch (err: any) {
|
|
if (err?.code === 'ERR_CANCELED') return;
|
|
const message = err?.message || '登录失败,请稍后再试';
|
|
Alert.alert('登录失败', message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [appleAvailable, router, searchParams?.redirectParams, searchParams?.redirectTo]);
|
|
|
|
const onGuestLogin = useCallback(() => {
|
|
// 游客继续:若有 redirect 则前往,无则返回
|
|
const to = searchParams?.redirectTo as string | undefined;
|
|
const paramsJson = searchParams?.redirectParams as string | undefined;
|
|
let parsedParams: Record<string, any> | undefined;
|
|
if (paramsJson) {
|
|
try { parsedParams = JSON.parse(paramsJson); } catch { }
|
|
}
|
|
if (to) {
|
|
router.replace({ pathname: to, params: parsedParams } as any);
|
|
} else {
|
|
router.back();
|
|
}
|
|
}, [router, searchParams?.redirectParams, searchParams?.redirectTo]);
|
|
|
|
const disabledStyle = useMemo(() => ({ opacity: hasAgreed ? 1 : 0.5 }), [hasAgreed]);
|
|
|
|
return (
|
|
<SafeAreaView edges={['top']} style={[styles.safeArea, { backgroundColor: color.background }]}>
|
|
<ThemedView style={styles.container}>
|
|
{/* 自定义头部,与其它页面风格一致 */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity accessibilityRole="button" onPress={() => router.back()} style={styles.backButton}>
|
|
<Ionicons name="chevron-back" size={24} color={scheme === 'dark' ? '#ECEDEE' : '#192126'} />
|
|
</TouchableOpacity>
|
|
<Text style={[styles.headerTitle, { color: color.text }]}>登录</Text>
|
|
<View style={{ width: 32 }} />
|
|
</View>
|
|
|
|
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
|
<View style={styles.headerWrap}>
|
|
<ThemedText style={[styles.title, { color: color.text }]}>数字普拉提</ThemedText>
|
|
<ThemedText style={[styles.subtitle, { color: color.textMuted }]}>欢迎登录</ThemedText>
|
|
</View>
|
|
|
|
{/* Apple 登录 */}
|
|
{appleAvailable && (
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
onPress={() => guardAgreement(onAppleLogin)}
|
|
disabled={!hasAgreed || loading}
|
|
style={({ pressed }) => [
|
|
styles.appleButton,
|
|
{ backgroundColor: '#000000' },
|
|
disabledStyle,
|
|
pressed && { transform: [{ scale: 0.98 }] },
|
|
]}
|
|
>
|
|
<Ionicons name="logo-apple" size={22} color="#FFFFFF" style={{ marginRight: 10 }} />
|
|
<Text style={styles.appleText}>使用 Apple 登录</Text>
|
|
</Pressable>
|
|
)}
|
|
|
|
{/* 游客登录(弱化样式) */}
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
onPress={() => guardAgreement(onGuestLogin)}
|
|
disabled={!hasAgreed || loading}
|
|
style={({ pressed }) => [
|
|
styles.guestButton,
|
|
{ borderColor: color.border, backgroundColor: color.surface },
|
|
disabledStyle,
|
|
pressed && { transform: [{ scale: 0.98 }] },
|
|
]}
|
|
>
|
|
<Ionicons name="person-circle-outline" size={22} color={Colors.light.neutral200} style={{ marginRight: 8 }} />
|
|
<Text style={[styles.guestText, { color: Colors.light.neutral200 }]}>以游客身份继续</Text>
|
|
</Pressable>
|
|
|
|
{/* 协议勾选 */}
|
|
<View style={styles.agreementRow}>
|
|
<Pressable onPress={() => setHasAgreed((v) => !v)} style={styles.checkboxWrap} accessibilityRole="checkbox" accessibilityState={{ checked: hasAgreed }}>
|
|
<View
|
|
style={[styles.checkbox, {
|
|
backgroundColor: hasAgreed ? color.primary : 'transparent',
|
|
borderColor: hasAgreed ? color.primary : color.border,
|
|
}]}
|
|
>
|
|
{hasAgreed && <Ionicons name="checkmark" size={14} color={color.onPrimary} />}
|
|
</View>
|
|
</Pressable>
|
|
<Text style={[styles.agreementText, { color: color.textMuted }]}>我已阅读并同意</Text>
|
|
<Pressable onPress={() => router.push('/legal/privacy-policy')}>
|
|
<Text style={[styles.link, { color: color.primary }]}>《隐私政策》</Text>
|
|
</Pressable>
|
|
<Text style={[styles.agreementText, { color: color.textMuted }]}>和</Text>
|
|
<Pressable onPress={() => router.push('/legal/user-agreement')}>
|
|
<Text style={[styles.link, { color: color.primary }]}>《用户协议》</Text>
|
|
</Pressable>
|
|
</View>
|
|
|
|
{/* 占位底部间距 */}
|
|
<View style={{ height: 40 }} />
|
|
</ScrollView>
|
|
</ThemedView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safeArea: { flex: 1 },
|
|
container: { flex: 1 },
|
|
content: {
|
|
flexGrow: 1,
|
|
paddingHorizontal: 24,
|
|
justifyContent: 'center',
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
paddingTop: 4,
|
|
paddingBottom: 8,
|
|
},
|
|
backButton: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' },
|
|
headerTitle: { fontSize: 18, fontWeight: '700' },
|
|
headerWrap: {
|
|
marginBottom: 36,
|
|
},
|
|
title: {
|
|
fontSize: 32,
|
|
fontWeight: '500',
|
|
letterSpacing: 0.5,
|
|
},
|
|
subtitle: {
|
|
marginTop: 8,
|
|
fontSize: 14,
|
|
fontWeight: '500',
|
|
},
|
|
appleButton: {
|
|
height: 56,
|
|
borderRadius: 28,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexDirection: 'row',
|
|
marginBottom: 16,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 8 },
|
|
shadowOpacity: 0.15,
|
|
shadowRadius: 12,
|
|
elevation: 2,
|
|
},
|
|
appleText: {
|
|
fontSize: 16,
|
|
color: '#FFFFFF',
|
|
fontWeight: '600',
|
|
},
|
|
guestButton: {
|
|
height: 52,
|
|
borderRadius: 26,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexDirection: 'row',
|
|
borderWidth: 1,
|
|
marginTop: 6,
|
|
},
|
|
guestText: {
|
|
fontSize: 15,
|
|
fontWeight: '500',
|
|
},
|
|
agreementRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
flexWrap: 'wrap',
|
|
marginTop: 24,
|
|
},
|
|
checkboxWrap: { marginRight: 8 },
|
|
checkbox: {
|
|
width: 18,
|
|
height: 18,
|
|
borderRadius: 5,
|
|
borderWidth: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
agreementText: { fontSize: 12 },
|
|
link: { fontSize: 12, fontWeight: '600' },
|
|
footerHint: { marginTop: 24 },
|
|
hintText: { fontSize: 12 },
|
|
});
|
|
|
|
|