feat: 添加用户登录和法律协议页面
- 新增登录页面,支持 Apple 登录和游客登录功能 - 添加用户协议和隐私政策页面,用户需同意后才能登录 - 更新首页逻辑,首次进入时自动跳转到登录页面 - 修改个人信息页面,移除单位选择功能,统一使用 kg 和 cm - 更新依赖,添加 expo-apple-authentication 库以支持 Apple 登录 - 更新布局以适应新功能的展示和交互
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user