feat: 添加用户登录和法律协议页面

- 新增登录页面,支持 Apple 登录和游客登录功能
- 添加用户协议和隐私政策页面,用户需同意后才能登录
- 更新首页逻辑,首次进入时自动跳转到登录页面
- 修改个人信息页面,移除单位选择功能,统一使用 kg 和 cm
- 更新依赖,添加 expo-apple-authentication 库以支持 Apple 登录
- 更新布局以适应新功能的展示和交互
This commit is contained in:
richarjiang
2025-08-12 19:21:07 +08:00
parent 8ffebfb297
commit c3d4630801
13 changed files with 326 additions and 103 deletions

View File

@@ -4,7 +4,7 @@ import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as ImagePicker from 'expo-image-picker';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Alert,
Image,
@@ -32,10 +32,6 @@ interface UserProfile {
weightKg?: number; // kg
heightCm?: number; // cm
avatarUri?: string | null;
unitPref?: {
weight: WeightUnit;
height: HeightUnit;
};
}
const STORAGE_KEY = '@user_profile';
@@ -52,26 +48,12 @@ export default function EditProfileScreen() {
weightKg: undefined,
heightCm: undefined,
avatarUri: null,
unitPref: { weight: 'kg', height: 'cm' },
});
const [weightInput, setWeightInput] = useState<string>('');
const [heightInput, setHeightInput] = useState<string>('');
// 将存储的公制值转换为当前选择单位显示
const displayedWeight = useMemo(() => {
if (profile.weightKg == null || isNaN(profile.weightKg)) return '';
return profile.unitPref?.weight === 'kg'
? String(round(profile.weightKg, 1))
: String(round(kgToLb(profile.weightKg), 1));
}, [profile.weightKg, profile.unitPref?.weight]);
const displayedHeight = useMemo(() => {
if (profile.heightCm == null || isNaN(profile.heightCm)) return '';
return profile.unitPref?.height === 'cm'
? String(Math.round(profile.heightCm))
: String(round(cmToFt(profile.heightCm), 1));
}, [profile.heightCm, profile.unitPref?.height]);
// 输入框字符串
useEffect(() => {
(async () => {
@@ -89,7 +71,6 @@ export default function EditProfileScreen() {
weightKg: undefined,
heightCm: undefined,
avatarUri: null,
unitPref: { weight: 'kg', height: 'cm' },
};
if (fromOnboarding) {
@@ -109,8 +90,8 @@ export default function EditProfileScreen() {
} catch { }
}
setProfile(next);
setWeightInput(next.weightKg != null ? (next.unitPref?.weight === 'kg' ? String(round(next.weightKg, 1)) : String(round(kgToLb(next.weightKg), 1))) : '');
setHeightInput(next.heightCm != null ? (next.unitPref?.height === 'cm' ? String(Math.round(next.heightCm)) : String(round(cmToFt(next.heightCm), 1))) : '');
setWeightInput(next.weightKg != null ? String(round(next.weightKg, 1)) : '');
setHeightInput(next.heightCm != null ? String(Math.round(next.heightCm)) : '');
} catch (e) {
console.warn('读取资料失败', e);
}
@@ -124,17 +105,17 @@ export default function EditProfileScreen() {
try {
const next: UserProfile = { ...profile };
// 将当前输入反向同步为公制
// 将当前输入同步为公制(固定 kg/cm
const w = parseFloat(weightInput);
if (!isNaN(w)) {
next.weightKg = profile.unitPref?.weight === 'kg' ? w : lbToKg(w);
next.weightKg = w;
} else {
next.weightKg = undefined;
}
const h = parseFloat(heightInput);
if (!isNaN(h)) {
next.heightCm = profile.unitPref?.height === 'cm' ? h : ftToCm(h);
next.heightCm = h;
} else {
next.heightCm = undefined;
}
@@ -147,27 +128,7 @@ export default function EditProfileScreen() {
}
};
const toggleWeightUnit = (unit: WeightUnit) => {
if (unit === profile.unitPref?.weight) return;
const current = parseFloat(weightInput);
let nextValueStr = weightInput;
if (!isNaN(current)) {
nextValueStr = unit === 'kg' ? String(round(lbToKg(current), 1)) : String(round(kgToLb(current), 1));
}
setProfile((p) => ({ ...p, unitPref: { ...(p.unitPref || { weight: 'kg', height: 'cm' }), weight: unit } }));
setWeightInput(nextValueStr);
};
const toggleHeightUnit = (unit: HeightUnit) => {
if (unit === profile.unitPref?.height) return;
const current = parseFloat(heightInput);
let nextValueStr = heightInput;
if (!isNaN(current)) {
nextValueStr = unit === 'cm' ? String(Math.round(ftToCm(current))) : String(round(cmToFt(current), 1));
}
setProfile((p) => ({ ...p, unitPref: { ...(p.unitPref || { weight: 'kg', height: 'cm' }), height: unit } }));
setHeightInput(nextValueStr);
};
// 不再需要单位切换
const pickAvatarFromLibrary = async () => {
try {
@@ -235,44 +196,36 @@ export default function EditProfileScreen() {
{!!profile.fullName && <Text style={{ color: '#C4C4C4' }}></Text>}
</View>
{/* 体重 */}
{/* 体重kg */}
<FieldLabel text="体重" />
<View style={styles.row}>
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
<TextInput
style={[styles.textInput, { color: textColor }]}
placeholder={profile.unitPref?.weight === 'kg' ? '输入体重' : '输入体重'}
placeholder={'输入体重'}
placeholderTextColor={placeholderColor}
keyboardType="numeric"
value={weightInput}
onChangeText={setWeightInput}
/>
<Text style={{ color: '#5E6468', marginLeft: 6 }}>kg</Text>
</View>
<SegmentedTwo
options={[{ key: 'lb', label: 'LBS' }, { key: 'kg', label: 'KG' }]}
activeKey={profile.unitPref?.weight || 'kg'}
onChange={(key) => toggleWeightUnit(key as WeightUnit)}
/>
</View>
{/* 身高 */}
{/* 身高cm */}
<FieldLabel text="身高" />
<View style={styles.row}>
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
<TextInput
style={[styles.textInput, { color: textColor }]}
placeholder={profile.unitPref?.height === 'cm' ? '输入身高' : '输入身高'}
placeholder={'输入身高'}
placeholderTextColor={placeholderColor}
keyboardType="numeric"
value={heightInput}
onChangeText={setHeightInput}
/>
<Text style={{ color: '#5E6468', marginLeft: 6 }}>cm</Text>
</View>
<SegmentedTwo
options={[{ key: 'ft', label: 'FEET' }, { key: 'cm', label: 'CM' }]}
activeKey={profile.unitPref?.height || 'cm'}
onChange={(key) => toggleHeightUnit(key as HeightUnit)}
/>
</View>
{/* 性别 */}
@@ -323,29 +276,10 @@ function FieldLabel({ text }: { text: string }) {
);
}
function SegmentedTwo(props: {
options: { key: string; label: string }[];
activeKey: string;
onChange: (key: string) => void;
}) {
const { options, activeKey, onChange } = props;
return (
<View style={[styles.segmented, { backgroundColor: '#EFEFEF' }]}>
{options.map((opt) => (
<TouchableOpacity
key={opt.key}
style={[styles.segmentBtn, activeKey === opt.key && { backgroundColor: '#FFFFFF' }]}
onPress={() => onChange(opt.key)}
activeOpacity={0.8}
>
<Text style={{ fontSize: 14, fontWeight: '600', color: activeKey === opt.key ? '#000' : '#5E6468' }}>{opt.label}</Text>
</TouchableOpacity>
))}
</View>
);
}
// 单位切换组件已移除(固定 kg/cm
// 工具函数
// 转换函数不再使用,保留 round
function kgToLb(kg: number) { return kg * 2.2046226218; }
function lbToKg(lb: number) { return lb / 2.2046226218; }
function cmToFt(cm: number) { return cm / 30.48; }