Compare commits
2 Commits
2fac3f899c
...
c3d4630801
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3d4630801 | ||
|
|
8ffebfb297 |
@@ -1,8 +1,10 @@
|
|||||||
|
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||||
|
import { CircularRing } from '@/components/CircularRing';
|
||||||
import { ProgressBar } from '@/components/ProgressBar';
|
import { ProgressBar } from '@/components/ProgressBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
import { ensureHealthPermissions, fetchTodayHealthData } from '@/utils/health';
|
import { ensureHealthPermissions, fetchHealthDataForDate, fetchTodayHealthData } from '@/utils/health';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
@@ -52,8 +54,11 @@ export default function ExploreScreen() {
|
|||||||
const [stepCount, setStepCount] = useState<number | null>(null);
|
const [stepCount, setStepCount] = useState<number | null>(null);
|
||||||
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
||||||
|
const [animToken, setAnimToken] = useState(0);
|
||||||
|
const [trainingProgress, setTrainingProgress] = useState(0.8); // 暂定静态80%
|
||||||
|
|
||||||
const loadHealthData = async () => {
|
const loadHealthData = async (targetDate?: Date) => {
|
||||||
try {
|
try {
|
||||||
console.log('=== 开始HealthKit初始化流程 ===');
|
console.log('=== 开始HealthKit初始化流程 ===');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -66,11 +71,12 @@ export default function ExploreScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('权限获取成功,开始获取健康数据...');
|
console.log('权限获取成功,开始获取健康数据...');
|
||||||
const data = await fetchTodayHealthData();
|
const data = targetDate ? await fetchHealthDataForDate(targetDate) : await fetchTodayHealthData();
|
||||||
|
|
||||||
console.log('设置UI状态:', data);
|
console.log('设置UI状态:', data);
|
||||||
setStepCount(data.steps);
|
setStepCount(data.steps);
|
||||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||||
|
setAnimToken((t) => t + 1);
|
||||||
console.log('=== HealthKit数据获取完成 ===');
|
console.log('=== HealthKit数据获取完成 ===');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,6 +92,16 @@ export default function ExploreScreen() {
|
|||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 日期点击时,加载对应日期数据
|
||||||
|
const onSelectDate = (index: number) => {
|
||||||
|
setSelectedIndex(index);
|
||||||
|
scrollToIndex(index);
|
||||||
|
const target = days[index]?.date?.toDate();
|
||||||
|
if (target) {
|
||||||
|
loadHealthData(target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -110,10 +126,7 @@ export default function ExploreScreen() {
|
|||||||
<View key={`${d.dayOfMonth}`} style={styles.dayItemWrapper}>
|
<View key={`${d.dayOfMonth}`} style={styles.dayItemWrapper}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.dayPill, selected ? styles.dayPillSelected : styles.dayPillNormal]}
|
style={[styles.dayPill, selected ? styles.dayPillSelected : styles.dayPillNormal]}
|
||||||
onPress={() => {
|
onPress={() => onSelectDate(i)}
|
||||||
setSelectedIndex(i);
|
|
||||||
scrollToIndex(i);
|
|
||||||
}}
|
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={[styles.dayLabel, selected && styles.dayLabelSelected]}> {d.weekdayZh} </Text>
|
<Text style={[styles.dayLabel, selected && styles.dayLabelSelected]}> {d.weekdayZh} </Text>
|
||||||
@@ -128,48 +141,59 @@ export default function ExploreScreen() {
|
|||||||
{/* 今日报告 标题 */}
|
{/* 今日报告 标题 */}
|
||||||
<Text style={styles.sectionTitle}>今日报告</Text>
|
<Text style={styles.sectionTitle}>今日报告</Text>
|
||||||
|
|
||||||
{/* 健康数据错误提示 */}
|
{/* 取消卡片内 loading,保持静默刷新提升体验 */}
|
||||||
{isLoading && (
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<Ionicons name="warning-outline" size={20} color="#E54D4D" />
|
|
||||||
<Text style={styles.errorText}>加载中...</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.retryButton}
|
|
||||||
onPress={loadHealthData} disabled={isLoading}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name="refresh-outline"
|
|
||||||
size={16}
|
|
||||||
color={isLoading ? '#9AA3AE' : '#E54D4D'}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
|
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
|
||||||
<View style={styles.metricsRow}>
|
<View style={styles.metricsRow}>
|
||||||
<View style={[styles.trainingCard, styles.metricsLeft]}>
|
<View style={[styles.trainingCard, styles.metricsLeft]}>
|
||||||
<Text style={styles.cardTitleSecondary}>训练时间</Text>
|
<Text style={styles.cardTitleSecondary}>训练时间</Text>
|
||||||
<View style={styles.trainingContent}>
|
<View style={styles.trainingContent}>
|
||||||
<View style={styles.trainingRingTrack} />
|
<CircularRing
|
||||||
<View style={styles.trainingRingProgress} />
|
size={120}
|
||||||
<Text style={styles.trainingPercent}>80%</Text>
|
strokeWidth={12}
|
||||||
|
trackColor="#E2D9FD"
|
||||||
|
progressColor="#8B74F3"
|
||||||
|
progress={trainingProgress}
|
||||||
|
resetToken={animToken}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.metricsRight}>
|
<View style={styles.metricsRight}>
|
||||||
<View style={[styles.metricsRightCard, styles.caloriesCard, { minHeight: 88 }]}>
|
<View style={[styles.metricsRightCard, styles.caloriesCard, { minHeight: 88 }]}>
|
||||||
<Text style={styles.cardTitleSecondary}>消耗卡路里</Text>
|
<Text style={styles.cardTitleSecondary}>消耗卡路里</Text>
|
||||||
<Text style={styles.caloriesValue}>
|
{activeCalories != null ? (
|
||||||
{isLoading ? '加载中...' : activeCalories != null ? `${activeCalories} 千卡` : '——'}
|
<AnimatedNumber
|
||||||
</Text>
|
value={activeCalories}
|
||||||
|
resetToken={animToken}
|
||||||
|
style={styles.caloriesValue}
|
||||||
|
format={(v) => `${Math.round(v)} 千卡`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.caloriesValue}>——</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.metricsRightCard, styles.stepsCard, { minHeight: 88 }]}>
|
<View style={[styles.metricsRightCard, styles.stepsCard, { minHeight: 88 }]}>
|
||||||
<View style={styles.cardHeaderRow}>
|
<View style={styles.cardHeaderRow}>
|
||||||
<View style={styles.iconSquare}><Ionicons name="footsteps-outline" size={18} color="#192126" /></View>
|
<View style={styles.iconSquare}><Ionicons name="footsteps-outline" size={18} color="#192126" /></View>
|
||||||
<Text style={styles.cardTitle}>步数</Text>
|
<Text style={styles.cardTitle}>步数</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.stepsValue}>{isLoading ? '加载中.../2000' : stepCount != null ? `${stepCount}/2000` : '——/2000'}</Text>
|
{stepCount != null ? (
|
||||||
<ProgressBar progress={Math.min(1, Math.max(0, (stepCount ?? 0) / 2000))} height={12} trackColor="#FFEBCB" fillColor="#FFC365" />
|
<AnimatedNumber
|
||||||
|
value={stepCount}
|
||||||
|
resetToken={animToken}
|
||||||
|
style={styles.stepsValue}
|
||||||
|
format={(v) => `${Math.round(v)}/2000`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.stepsValue}>——/2000</Text>
|
||||||
|
)}
|
||||||
|
<ProgressBar
|
||||||
|
progress={Math.min(1, Math.max(0, (stepCount ?? 0) / 2000))}
|
||||||
|
height={18}
|
||||||
|
trackColor="#FFEBCB"
|
||||||
|
fillColor="#FFC365"
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ThemedView } from '@/components/ThemedView';
|
|||||||
import { WorkoutCard } from '@/components/WorkoutCard';
|
import { WorkoutCard } from '@/components/WorkoutCard';
|
||||||
import { getChineseGreeting } from '@/utils/date';
|
import { getChineseGreeting } from '@/utils/date';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
|
import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
|
||||||
|
|
||||||
const workoutData = [
|
const workoutData = [
|
||||||
@@ -24,6 +24,15 @@ const workoutData = [
|
|||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const hasOpenedLoginRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 仅在本次会话首次进入首页时打开登录页,可返回关闭
|
||||||
|
if (!hasOpenedLoginRef.current) {
|
||||||
|
hasOpenedLoginRef.current = true;
|
||||||
|
router.push('/auth/login');
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import React, { useMemo, useState } from 'react';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
@@ -29,6 +31,65 @@ export default function PersonalScreen() {
|
|||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const colors = Colors[colorScheme ?? 'light'];
|
const colors = Colors[colorScheme ?? 'light'];
|
||||||
|
|
||||||
|
type UserProfile = {
|
||||||
|
fullName?: string;
|
||||||
|
email?: string;
|
||||||
|
gender?: 'male' | 'female' | '';
|
||||||
|
age?: string;
|
||||||
|
weightKg?: number;
|
||||||
|
heightCm?: number;
|
||||||
|
avatarUri?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [profile, setProfile] = useState<UserProfile>({});
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const [p, o] = await Promise.all([
|
||||||
|
AsyncStorage.getItem('@user_profile'),
|
||||||
|
AsyncStorage.getItem('@user_personal_info'),
|
||||||
|
]);
|
||||||
|
let next: UserProfile = {};
|
||||||
|
if (o) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(o);
|
||||||
|
next = {
|
||||||
|
...next,
|
||||||
|
age: parsed?.age ? String(parsed.age) : undefined,
|
||||||
|
gender: parsed?.gender || '',
|
||||||
|
heightCm: parsed?.height ? parseFloat(parsed.height) : undefined,
|
||||||
|
weightKg: parsed?.weight ? parseFloat(parsed.weight) : undefined,
|
||||||
|
};
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (p) {
|
||||||
|
try { next = { ...next, ...JSON.parse(p) }; } catch { }
|
||||||
|
}
|
||||||
|
setProfile(next);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('加载用户资料失败', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, []);
|
||||||
|
useFocusEffect(React.useCallback(() => { load(); return () => { }; }, []));
|
||||||
|
|
||||||
|
const formatHeight = () => {
|
||||||
|
if (profile.heightCm == null) return '--';
|
||||||
|
return `${Math.round(profile.heightCm)}cm`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatWeight = () => {
|
||||||
|
if (profile.weightKg == null) return '--';
|
||||||
|
return `${round(profile.weightKg, 1)}kg`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAge = () => (profile.age ? `${profile.age}岁` : '--');
|
||||||
|
|
||||||
|
const round = (n: number, d = 0) => {
|
||||||
|
const p = Math.pow(10, d); return Math.round(n * p) / p;
|
||||||
|
};
|
||||||
|
|
||||||
const handleResetOnboarding = () => {
|
const handleResetOnboarding = () => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'重置引导',
|
'重置引导',
|
||||||
@@ -74,13 +135,13 @@ export default function PersonalScreen() {
|
|||||||
|
|
||||||
{/* 用户信息 */}
|
{/* 用户信息 */}
|
||||||
<View style={styles.userDetails}>
|
<View style={styles.userDetails}>
|
||||||
<Text style={styles.userName}>Masi Ramezanzade</Text>
|
<Text style={styles.userName}>{profile.fullName || '未设置姓名'}</Text>
|
||||||
<Text style={styles.userProgram}>Lose a Fat Program</Text>
|
<Text style={styles.userProgram}>减脂计划</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 编辑按钮 */}
|
{/* 编辑按钮 */}
|
||||||
<TouchableOpacity style={dynamicStyles.editButton}>
|
<TouchableOpacity style={dynamicStyles.editButton} onPress={() => router.push('/profile/edit')}>
|
||||||
<Text style={dynamicStyles.editButtonText}>Edit</Text>
|
<Text style={dynamicStyles.editButtonText}>编辑</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -89,16 +150,16 @@ export default function PersonalScreen() {
|
|||||||
const StatsSection = () => (
|
const StatsSection = () => (
|
||||||
<View style={styles.statsContainer}>
|
<View style={styles.statsContainer}>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={dynamicStyles.statValue}>180cm</Text>
|
<Text style={dynamicStyles.statValue}>{formatHeight()}</Text>
|
||||||
<Text style={styles.statLabel}>Height</Text>
|
<Text style={styles.statLabel}>身高</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={dynamicStyles.statValue}>65kg</Text>
|
<Text style={dynamicStyles.statValue}>{formatWeight()}</Text>
|
||||||
<Text style={styles.statLabel}>Weight</Text>
|
<Text style={styles.statLabel}>体重</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.statItem}>
|
||||||
<Text style={dynamicStyles.statValue}>22yo</Text>
|
<Text style={dynamicStyles.statValue}>{formatAge()}</Text>
|
||||||
<Text style={styles.statLabel}>Age</Text>
|
<Text style={styles.statLabel}>年龄</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -176,25 +237,26 @@ export default function PersonalScreen() {
|
|||||||
icon: 'person-outline',
|
icon: 'person-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Personal Data',
|
title: '个人资料',
|
||||||
|
onPress: () => router.push('/profile/edit'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'trophy-outline',
|
icon: 'trophy-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Achievement',
|
title: '成就',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'time-outline',
|
icon: 'time-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Activity History',
|
title: '活动历史',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'stats-chart-outline',
|
icon: 'stats-chart-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Workout Progress',
|
title: '训练进度',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -203,7 +265,7 @@ export default function PersonalScreen() {
|
|||||||
icon: 'notifications-outline',
|
icon: 'notifications-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Pop-up Notification',
|
title: '弹窗通知',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -213,19 +275,19 @@ export default function PersonalScreen() {
|
|||||||
icon: 'mail-outline',
|
icon: 'mail-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Contact Us',
|
title: '联系我们',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'shield-checkmark-outline',
|
icon: 'shield-checkmark-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Privacy Policy',
|
title: '隐私政策',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'settings-outline',
|
icon: 'settings-outline',
|
||||||
iconBg: '#E8F5E8',
|
iconBg: '#E8F5E8',
|
||||||
iconColor: '#4ADE80',
|
iconColor: '#4ADE80',
|
||||||
title: 'Settings',
|
title: '设置',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -250,10 +312,10 @@ export default function PersonalScreen() {
|
|||||||
>
|
>
|
||||||
<UserInfoSection />
|
<UserInfoSection />
|
||||||
<StatsSection />
|
<StatsSection />
|
||||||
<MenuSection title="Account" items={accountItems} />
|
<MenuSection title="账户" items={accountItems} />
|
||||||
<MenuSection title="Notification" items={notificationItems} />
|
<MenuSection title="通知" items={notificationItems} />
|
||||||
<MenuSection title="Other" items={otherItems} />
|
<MenuSection title="其他" items={otherItems} />
|
||||||
<MenuSection title="Developer" items={developerItems} />
|
<MenuSection title="开发者" items={developerItems} />
|
||||||
|
|
||||||
{/* 底部浮动按钮 */}
|
{/* 底部浮动按钮 */}
|
||||||
<View style={[styles.floatingButtonContainer, { bottom: Math.max(30, tabBarHeight / 2) + (insets?.bottom ?? 0) }]}>
|
<View style={[styles.floatingButtonContainer, { bottom: Math.max(30, tabBarHeight / 2) + (insets?.bottom ?? 0) }]}>
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ export default function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
<Stack>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
|
<Stack.Screen name="onboarding" />
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="(tabs)" />
|
||||||
<Stack.Screen name="ai-posture-assessment" options={{ headerShown: false }} />
|
<Stack.Screen name="profile/edit" />
|
||||||
|
<Stack.Screen name="ai-posture-assessment" />
|
||||||
|
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||||
|
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
|
|||||||
225
app/auth/login.tsx
Normal file
225
app/auth/login.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Alert, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
|
import { ThemedView } from '@/components/ThemedView';
|
||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
|
||||||
|
export default function LoginScreen() {
|
||||||
|
const router = useRouter();
|
||||||
|
const scheme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
|
const color = Colors[scheme];
|
||||||
|
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// TODO: 将 credential 发送到后端换取应用会话。这里先直接返回上一页。
|
||||||
|
router.back();
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.code === 'ERR_CANCELED') return;
|
||||||
|
Alert.alert('登录失败', '请稍后再试');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [appleAvailable, router]);
|
||||||
|
|
||||||
|
const onGuestLogin = useCallback(() => {
|
||||||
|
// TODO: 标记为游客身份,可在此写入本地状态/上报统计
|
||||||
|
router.back();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const disabledStyle = useMemo(() => ({ opacity: hasAgreed ? 1 : 0.5 }), [hasAgreed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView 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 }]}>Digital Pilates</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: '800',
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
15
app/legal/privacy-policy.tsx
Normal file
15
app/legal/privacy-policy.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ScrollView, Text } from 'react-native';
|
||||||
|
|
||||||
|
export default function PrivacyPolicy() {
|
||||||
|
return (
|
||||||
|
<ScrollView style={{ flex: 1, padding: 16 }}>
|
||||||
|
<Text style={{ fontSize: 20, fontWeight: '700', marginBottom: 12 }}>隐私政策(示例)</Text>
|
||||||
|
<Text style={{ lineHeight: 22, color: '#4A4A4A' }}>
|
||||||
|
这是占位文案,用于展示隐私政策内容。请替换为正式的隐私政策文本。
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
15
app/legal/user-agreement.tsx
Normal file
15
app/legal/user-agreement.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ScrollView, Text } from 'react-native';
|
||||||
|
|
||||||
|
export default function UserAgreement() {
|
||||||
|
return (
|
||||||
|
<ScrollView style={{ flex: 1, padding: 16 }}>
|
||||||
|
<Text style={{ fontSize: 20, fontWeight: '700', marginBottom: 12 }}>用户协议(示例)</Text>
|
||||||
|
<Text style={{ lineHeight: 22, color: '#4A4A4A' }}>
|
||||||
|
这是占位文案,用于说明用户协议。请在此替换为你们的正式协议内容。
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2,9 +2,9 @@ import { Stack } from 'expo-router';
|
|||||||
|
|
||||||
export default function OnboardingLayout() {
|
export default function OnboardingLayout() {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
<Stack.Screen name="index" />
|
||||||
<Stack.Screen name="personal-info" options={{ headerShown: false }} />
|
<Stack.Screen name="personal-info" />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
388
app/profile/edit.tsx
Normal file
388
app/profile/edit.tsx
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
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, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Image,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
SafeAreaView,
|
||||||
|
ScrollView,
|
||||||
|
StatusBar,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
type WeightUnit = 'kg' | 'lb';
|
||||||
|
type HeightUnit = 'cm' | 'ft';
|
||||||
|
|
||||||
|
interface UserProfile {
|
||||||
|
fullName?: string;
|
||||||
|
gender?: 'male' | 'female' | '';
|
||||||
|
age?: string; // 存储为字符串,方便非必填
|
||||||
|
// 以公制为基准存储
|
||||||
|
weightKg?: number; // kg
|
||||||
|
heightCm?: number; // cm
|
||||||
|
avatarUri?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = '@user_profile';
|
||||||
|
|
||||||
|
export default function EditProfileScreen() {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const colors = Colors[colorScheme ?? 'light'];
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const [profile, setProfile] = useState<UserProfile>({
|
||||||
|
fullName: '',
|
||||||
|
gender: '',
|
||||||
|
age: '',
|
||||||
|
weightKg: undefined,
|
||||||
|
heightCm: undefined,
|
||||||
|
avatarUri: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [weightInput, setWeightInput] = useState<string>('');
|
||||||
|
const [heightInput, setHeightInput] = useState<string>('');
|
||||||
|
|
||||||
|
// 输入框字符串
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// 读取已保存资料;兼容引导页的个人信息
|
||||||
|
const [p, fromOnboarding] = await Promise.all([
|
||||||
|
AsyncStorage.getItem(STORAGE_KEY),
|
||||||
|
AsyncStorage.getItem('@user_personal_info'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let next: UserProfile = {
|
||||||
|
fullName: '',
|
||||||
|
gender: '',
|
||||||
|
age: '',
|
||||||
|
weightKg: undefined,
|
||||||
|
heightCm: undefined,
|
||||||
|
avatarUri: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fromOnboarding) {
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(fromOnboarding);
|
||||||
|
if (o?.weight) next.weightKg = parseFloat(o.weight) || undefined;
|
||||||
|
if (o?.height) next.heightCm = parseFloat(o.height) || undefined;
|
||||||
|
if (o?.age) next.age = String(o.age);
|
||||||
|
if (o?.gender) next.gender = o.gender;
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p) {
|
||||||
|
try {
|
||||||
|
const parsed: UserProfile = JSON.parse(p);
|
||||||
|
next = { ...next, ...parsed };
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
setProfile(next);
|
||||||
|
setWeightInput(next.weightKg != null ? String(round(next.weightKg, 1)) : '');
|
||||||
|
setHeightInput(next.heightCm != null ? String(Math.round(next.heightCm)) : '');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('读取资料失败', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const textColor = colors.text;
|
||||||
|
const placeholderColor = colors.icon;
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
const next: UserProfile = { ...profile };
|
||||||
|
|
||||||
|
// 将当前输入同步为公制(固定 kg/cm)
|
||||||
|
const w = parseFloat(weightInput);
|
||||||
|
if (!isNaN(w)) {
|
||||||
|
next.weightKg = w;
|
||||||
|
} else {
|
||||||
|
next.weightKg = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const h = parseFloat(heightInput);
|
||||||
|
if (!isNaN(h)) {
|
||||||
|
next.heightCm = h;
|
||||||
|
} else {
|
||||||
|
next.heightCm = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||||
|
Alert.alert('已保存', '个人资料已更新。');
|
||||||
|
router.back();
|
||||||
|
} catch (e) {
|
||||||
|
Alert.alert('保存失败', '请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 不再需要单位切换
|
||||||
|
|
||||||
|
const pickAvatarFromLibrary = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
|
const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited';
|
||||||
|
if (!libGranted) {
|
||||||
|
Alert.alert('权限不足', '需要相册权限以选择头像');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
allowsEditing: true,
|
||||||
|
quality: 0.9,
|
||||||
|
aspect: [1, 1],
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
});
|
||||||
|
if (!result.canceled) {
|
||||||
|
setProfile((p) => ({ ...p, avatarUri: result.assets?.[0]?.uri ?? null }));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Alert.alert('发生错误', '选择头像失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={[styles.container, { backgroundColor: '#F5F5F5' }]}>
|
||||||
|
<StatusBar barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'} />
|
||||||
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
||||||
|
<ScrollView contentContainerStyle={{ paddingBottom: 40 }} style={{ paddingHorizontal: 20 }} showsVerticalScrollIndicator={false}>
|
||||||
|
{/* 统一头部 */}
|
||||||
|
<View style={[styles.header]}>
|
||||||
|
<TouchableOpacity accessibilityRole="button" onPress={() => router.back()} style={styles.backButton}>
|
||||||
|
<Ionicons name="chevron-back" size={24} color="#192126" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>编辑资料</Text>
|
||||||
|
<View style={{ width: 32 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 头像(带相机蒙层,点击从相册选择) */}
|
||||||
|
<View style={{ alignItems: 'center', marginTop: 4, marginBottom: 16 }}>
|
||||||
|
<TouchableOpacity activeOpacity={0.85} onPress={pickAvatarFromLibrary}>
|
||||||
|
<View style={styles.avatarCircle}>
|
||||||
|
{profile.avatarUri ? (
|
||||||
|
<Image source={{ uri: profile.avatarUri }} style={styles.avatarImage} />
|
||||||
|
) : (
|
||||||
|
<View style={{ width: 56, height: 56, borderRadius: 28, backgroundColor: '#D4A574' }} />
|
||||||
|
)}
|
||||||
|
<View style={styles.avatarOverlay}>
|
||||||
|
<Ionicons name="camera" size={22} color="#192126" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 姓名 */}
|
||||||
|
<FieldLabel text="姓名" />
|
||||||
|
<View style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.textInput, { color: textColor }]}
|
||||||
|
placeholder="填写姓名(可选)"
|
||||||
|
placeholderTextColor={placeholderColor}
|
||||||
|
value={profile.fullName}
|
||||||
|
onChangeText={(t) => setProfile((p) => ({ ...p, fullName: t }))}
|
||||||
|
/>
|
||||||
|
{/* 校验勾无需强制,仅装饰 */}
|
||||||
|
{!!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={'输入体重'}
|
||||||
|
placeholderTextColor={placeholderColor}
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={weightInput}
|
||||||
|
onChangeText={setWeightInput}
|
||||||
|
/>
|
||||||
|
<Text style={{ color: '#5E6468', marginLeft: 6 }}>kg</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 身高(cm) */}
|
||||||
|
<FieldLabel text="身高" />
|
||||||
|
<View style={styles.row}>
|
||||||
|
<View style={[styles.inputWrapper, styles.flex1, { borderColor: '#E0E0E0' }]}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.textInput, { color: textColor }]}
|
||||||
|
placeholder={'输入身高'}
|
||||||
|
placeholderTextColor={placeholderColor}
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={heightInput}
|
||||||
|
onChangeText={setHeightInput}
|
||||||
|
/>
|
||||||
|
<Text style={{ color: '#5E6468', marginLeft: 6 }}>cm</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 性别 */}
|
||||||
|
<FieldLabel text="性别" />
|
||||||
|
<View style={[styles.selector, { borderColor: '#E0E0E0' }]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.selectorItem, profile.gender === 'female' && { backgroundColor: colors.primary + '40' }]}
|
||||||
|
onPress={() => setProfile((p) => ({ ...p, gender: 'female' }))}
|
||||||
|
>
|
||||||
|
<Text style={styles.selectorEmoji}>♀</Text>
|
||||||
|
<Text style={styles.selectorText}>女性</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.selectorItem, profile.gender === 'male' && { backgroundColor: colors.primary + '40' }]}
|
||||||
|
onPress={() => setProfile((p) => ({ ...p, gender: 'male' }))}
|
||||||
|
>
|
||||||
|
<Text style={styles.selectorEmoji}>♂</Text>
|
||||||
|
<Text style={styles.selectorText}>男性</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 年龄 */}
|
||||||
|
<FieldLabel text="年龄" />
|
||||||
|
<View style={[styles.inputWrapper, { borderColor: '#E0E0E0' }]}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.textInput, { color: textColor }]}
|
||||||
|
placeholder="填写年龄(可选)"
|
||||||
|
placeholderTextColor={placeholderColor}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
value={profile.age ?? ''}
|
||||||
|
onChangeText={(t) => setProfile((p) => ({ ...p, age: t }))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 保存按钮 */}
|
||||||
|
<TouchableOpacity onPress={handleSave} activeOpacity={0.9} style={[styles.saveBtn, { backgroundColor: colors.primary }]}>
|
||||||
|
<Text style={{ color: colors.onPrimary, fontSize: 18, fontWeight: '700' }}>保存</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldLabel({ text }: { text: string }) {
|
||||||
|
return (
|
||||||
|
<Text style={{ fontSize: 14, color: '#5E6468', marginBottom: 8, marginTop: 10 }}>{text}</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单位切换组件已移除(固定 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; }
|
||||||
|
function ftToCm(ft: number) { return ft * 30.48; }
|
||||||
|
function round(n: number, d = 0) { const p = Math.pow(10, d); return Math.round(n * p) / p; }
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
avatarCircle: {
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
backgroundColor: '#E8D4F0',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
avatarImage: {
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
},
|
||||||
|
avatarOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.25)',
|
||||||
|
},
|
||||||
|
inputWrapper: {
|
||||||
|
height: 52,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
flex1: { flex: 1 },
|
||||||
|
segmented: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 4,
|
||||||
|
backgroundColor: '#EFEFEF',
|
||||||
|
},
|
||||||
|
segmentBtn: {
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
minWidth: 64,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
selector: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
selectorItem: {
|
||||||
|
flex: 1,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
selectorEmoji: { fontSize: 16, marginBottom: 2 },
|
||||||
|
selectorText: { fontSize: 15, fontWeight: '600' },
|
||||||
|
saveBtn: {
|
||||||
|
marginTop: 24,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
backButton: { padding: 4, width: 32 },
|
||||||
|
headerTitle: { fontSize: 18, fontWeight: '700', color: '#192126' },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
47
components/AnimatedNumber.tsx
Normal file
47
components/AnimatedNumber.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Animated, Easing, TextStyle } from 'react-native';
|
||||||
|
|
||||||
|
type AnimatedNumberProps = {
|
||||||
|
value: number; // 最终值
|
||||||
|
durationMs?: number;
|
||||||
|
format?: (v: number) => string;
|
||||||
|
style?: TextStyle;
|
||||||
|
/** 当该值变化时,从0重新动画 */
|
||||||
|
resetToken?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AnimatedNumber({
|
||||||
|
value,
|
||||||
|
durationMs = 800,
|
||||||
|
format,
|
||||||
|
style,
|
||||||
|
resetToken,
|
||||||
|
}: AnimatedNumberProps) {
|
||||||
|
const animated = useRef(new Animated.Value(0)).current;
|
||||||
|
const [display, setDisplay] = useState<string>('0');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
animated.stopAnimation(() => {
|
||||||
|
animated.setValue(0);
|
||||||
|
Animated.timing(animated, {
|
||||||
|
toValue: value,
|
||||||
|
duration: durationMs,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value, resetToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = animated.addListener(({ value: v }) => {
|
||||||
|
const num = Number(v) || 0;
|
||||||
|
setDisplay(format ? format(num) : `${Math.round(num)}`);
|
||||||
|
});
|
||||||
|
return () => animated.removeListener(id);
|
||||||
|
}, [animated, format]);
|
||||||
|
|
||||||
|
return <Animated.Text style={style}>{display}</Animated.Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
121
components/CircularRing.tsx
Normal file
121
components/CircularRing.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { Animated, Easing, StyleSheet, Text, View } from 'react-native';
|
||||||
|
import Svg, { Circle, G } from 'react-native-svg';
|
||||||
|
|
||||||
|
type CircularRingProps = {
|
||||||
|
size?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
trackColor?: string;
|
||||||
|
progressColor?: string;
|
||||||
|
progress: number; // 0..1
|
||||||
|
durationMs?: number;
|
||||||
|
showCenterText?: boolean;
|
||||||
|
/** 当该值变化时,会从0重新动画到 progress */
|
||||||
|
resetToken?: unknown;
|
||||||
|
/** 进度起始角度(度),默认 -90,使进度从正上方开始 */
|
||||||
|
startAngleDeg?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 纯 View 实现的圆环进度条(无依赖),通过两个半圆与旋转实现。
|
||||||
|
*/
|
||||||
|
export function CircularRing({
|
||||||
|
size = 120,
|
||||||
|
strokeWidth = 12,
|
||||||
|
trackColor = '#E2D9FD',
|
||||||
|
progressColor = '#8B74F3',
|
||||||
|
progress,
|
||||||
|
durationMs = 900,
|
||||||
|
showCenterText = true,
|
||||||
|
resetToken,
|
||||||
|
startAngleDeg = -90,
|
||||||
|
}: CircularRingProps) {
|
||||||
|
const clamped = useMemo(() => {
|
||||||
|
if (Number.isNaN(progress)) return 0;
|
||||||
|
return Math.min(1, Math.max(0, progress));
|
||||||
|
}, [progress]);
|
||||||
|
|
||||||
|
const animated = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 每次 resetToken 或目标进度变化时,从0动画到 clamped
|
||||||
|
animated.stopAnimation(() => {
|
||||||
|
animated.setValue(0);
|
||||||
|
Animated.timing(animated, {
|
||||||
|
toValue: clamped,
|
||||||
|
duration: durationMs,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [clamped, resetToken]);
|
||||||
|
const radius = useMemo(() => (size - strokeWidth) / 2, [size, strokeWidth]);
|
||||||
|
const circumference = useMemo(() => 2 * Math.PI * radius, [radius]);
|
||||||
|
const dashOffset = animated.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [circumference, 0],
|
||||||
|
});
|
||||||
|
|
||||||
|
const percentText = useMemo(() => `${Math.round(clamped * 100)}%`, [clamped]);
|
||||||
|
const containerStyle = useMemo(() => [styles.container, { width: size, height: size }], [size]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={containerStyle}>
|
||||||
|
<Svg width={size} height={size}>
|
||||||
|
<G rotation={startAngleDeg} originX={size / 2} originY={size / 2}>
|
||||||
|
{/* 轨道 */}
|
||||||
|
<Circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
stroke={trackColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
{/* 进度 */}
|
||||||
|
<AnimatedCircle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
stroke={progressColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={`${circumference} ${circumference}`}
|
||||||
|
strokeDashoffset={dashOffset as unknown as number}
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</G>
|
||||||
|
</Svg>
|
||||||
|
|
||||||
|
{showCenterText && (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<Text style={styles.centerText}>{percentText}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
centerText: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: '#8B74F3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- ExpoAppleAuthentication (6.4.2):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoAsset (11.1.7):
|
- ExpoAsset (11.1.7):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoBlur (14.1.5):
|
- ExpoBlur (14.1.5):
|
||||||
@@ -2087,6 +2089,55 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
|
- RNSVG (15.12.1):
|
||||||
|
- DoubleConversion
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.11.18.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-hermes
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- RNSVG/common (= 15.12.1)
|
||||||
|
- Yoga
|
||||||
|
- RNSVG/common (15.12.1):
|
||||||
|
- DoubleConversion
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly (= 2024.11.18.00)
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-hermes
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- Yoga
|
||||||
- SDWebImage (5.21.1):
|
- SDWebImage (5.21.1):
|
||||||
- SDWebImage/Core (= 5.21.1)
|
- SDWebImage/Core (= 5.21.1)
|
||||||
- SDWebImage/Core (5.21.1)
|
- SDWebImage/Core (5.21.1)
|
||||||
@@ -2107,6 +2158,7 @@ DEPENDENCIES:
|
|||||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||||
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
|
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
|
||||||
- Expo (from `../node_modules/expo`)
|
- Expo (from `../node_modules/expo`)
|
||||||
|
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
||||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||||
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
||||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||||
@@ -2199,6 +2251,7 @@ DEPENDENCIES:
|
|||||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../node_modules/react-native-screens`)
|
||||||
|
- RNSVG (from `../node_modules/react-native-svg`)
|
||||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -2223,6 +2276,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/expo-image-loader/ios"
|
:path: "../node_modules/expo-image-loader/ios"
|
||||||
Expo:
|
Expo:
|
||||||
:path: "../node_modules/expo"
|
:path: "../node_modules/expo"
|
||||||
|
ExpoAppleAuthentication:
|
||||||
|
:path: "../node_modules/expo-apple-authentication/ios"
|
||||||
ExpoAsset:
|
ExpoAsset:
|
||||||
:path: "../node_modules/expo-asset/ios"
|
:path: "../node_modules/expo-asset/ios"
|
||||||
ExpoBlur:
|
ExpoBlur:
|
||||||
@@ -2404,6 +2459,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-reanimated"
|
:path: "../node_modules/react-native-reanimated"
|
||||||
RNScreens:
|
RNScreens:
|
||||||
:path: "../node_modules/react-native-screens"
|
:path: "../node_modules/react-native-screens"
|
||||||
|
RNSVG:
|
||||||
|
:path: "../node_modules/react-native-svg"
|
||||||
Yoga:
|
Yoga:
|
||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
@@ -2413,6 +2470,7 @@ SPEC CHECKSUMS:
|
|||||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||||
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
|
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
|
||||||
Expo: a40d525c930dd1c8a158e082756ee071955baccb
|
Expo: a40d525c930dd1c8a158e082756ee071955baccb
|
||||||
|
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
||||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||||
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
||||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||||
@@ -2506,6 +2564,7 @@ SPEC CHECKSUMS:
|
|||||||
RNGestureHandler: 7d0931a61d7ba0259f32db0ba7d0963c3ed15d2b
|
RNGestureHandler: 7d0931a61d7ba0259f32db0ba7d0963c3ed15d2b
|
||||||
RNReanimated: 2313402fe27fecb7237619e9c6fcee3177f08a65
|
RNReanimated: 2313402fe27fecb7237619e9c6fcee3177f08a65
|
||||||
RNScreens: 482e9707f9826230810c92e765751af53826d509
|
RNScreens: 482e9707f9826230810c92e765751af53826d509
|
||||||
|
RNSVG: ba53827311fd9f8a14e06626365a749ce7713975
|
||||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||||
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
||||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = digitalpilates/Images.xcassets; sourceTree = "<group>"; };
|
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = digitalpilates/Images.xcassets; sourceTree = "<group>"; };
|
||||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = digitalpilates/Info.plist; sourceTree = "<group>"; };
|
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = digitalpilates/Info.plist; sourceTree = "<group>"; };
|
||||||
4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = "<group>"; };
|
4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = digitalpilates/SplashScreen.storyboard; sourceTree = "<group>"; };
|
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = digitalpilates/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */,
|
4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */,
|
||||||
EA6A757B2DE1747F7B3664B4 /* Pods-digitalpilates.release.xcconfig */,
|
EA6A757B2DE1747F7B3664B4 /* Pods-digitalpilates.release.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -290,6 +289,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||||
@@ -304,6 +304,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||||
@@ -338,6 +339,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
|
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -374,6 +376,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
|
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
||||||
INFOPLIST_FILE = digitalpilates/Info.plist;
|
INFOPLIST_FILE = digitalpilates/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.developer.healthkit</key>
|
<key>com.apple.developer.applesignin</key>
|
||||||
<true/>
|
<array>
|
||||||
<key>com.apple.developer.healthkit.access</key>
|
<string>Default</string>
|
||||||
<array/>
|
</array>
|
||||||
</dict>
|
<key>com.apple.developer.healthkit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.developer.healthkit.access</key>
|
||||||
|
<array/>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
159
package-lock.json
generated
159
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
|
"expo-apple-authentication": "6.4.2",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
"expo-constants": "~17.1.7",
|
"expo-constants": "~17.1.7",
|
||||||
"expo-font": "~13.3.2",
|
"expo-font": "~13.3.2",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
|
"react-native-svg": "^15.12.1",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5"
|
"react-native-webview": "13.13.5"
|
||||||
},
|
},
|
||||||
@@ -4543,6 +4545,11 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||||
|
},
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||||
@@ -5204,6 +5211,52 @@
|
|||||||
"hyphenate-style-name": "^1.0.3"
|
"hyphenate-style-name": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.0.14",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@@ -5423,6 +5476,58 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@@ -5498,6 +5603,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/env-editor": {
|
"node_modules/env-editor": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||||
@@ -6213,6 +6330,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-apple-authentication": {
|
||||||
|
"version": "6.4.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/expo-apple-authentication/-/expo-apple-authentication-6.4.2.tgz",
|
||||||
|
"integrity": "sha512-X4u1n3Ql1hOpztXHbKNq4I1l4+Ff82gC6RmEeW43Eht7VE6E8PrQBpYKw+JJv8osrCJt7R5O1PZwed6WLN5oig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "11.1.7",
|
"version": "11.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz",
|
||||||
@@ -8703,6 +8829,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.0.14",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||||
|
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@@ -9343,6 +9475,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nullthrows": {
|
"node_modules/nullthrows": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||||
@@ -10538,6 +10682,21 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-svg": {
|
||||||
|
"version": "15.12.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-svg/-/react-native-svg-15.12.1.tgz",
|
||||||
|
"integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-tree": "^1.1.3",
|
||||||
|
"warn-once": "0.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-web": {
|
"node_modules/react-native-web": {
|
||||||
"version": "0.20.0",
|
"version": "0.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
|
||||||
|
|||||||
@@ -19,10 +19,12 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
|
"expo-apple-authentication": "6.4.2",
|
||||||
"expo-constants": "~17.1.7",
|
"expo-constants": "~17.1.7",
|
||||||
"expo-font": "~13.3.2",
|
"expo-font": "~13.3.2",
|
||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
"expo-image": "~2.4.0",
|
"expo-image": "~2.4.0",
|
||||||
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "^14.1.5",
|
"expo-linear-gradient": "^14.1.5",
|
||||||
"expo-linking": "~7.1.7",
|
"expo-linking": "~7.1.7",
|
||||||
"expo-router": "~5.1.4",
|
"expo-router": "~5.1.4",
|
||||||
@@ -39,9 +41,9 @@
|
|||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
|
"react-native-svg": "^15.12.1",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5"
|
||||||
"expo-image-picker": "~16.1.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".expo/types/**/*.ts",
|
".expo/types/**/*.ts",
|
||||||
"expo-env.d.ts"
|
"expo-env.d.ts",
|
||||||
|
"types/**/*.d.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
39
types/react-native-svg.d.ts
vendored
Normal file
39
types/react-native-svg.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
declare module 'react-native-svg' {
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ViewProps } from 'react-native';
|
||||||
|
|
||||||
|
export interface SvgProps extends ViewProps {
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
viewBox?: string;
|
||||||
|
}
|
||||||
|
export default function Svg(props: React.PropsWithChildren<SvgProps>): React.ReactElement | null;
|
||||||
|
|
||||||
|
export interface CommonProps {
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
strokeLinecap?: 'butt' | 'round' | 'square';
|
||||||
|
strokeLinejoin?: 'miter' | 'round' | 'bevel';
|
||||||
|
strokeDasharray?: string | number[];
|
||||||
|
strokeDashoffset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircleProps extends CommonProps {
|
||||||
|
cx?: number;
|
||||||
|
cy?: number;
|
||||||
|
r?: number;
|
||||||
|
originX?: number;
|
||||||
|
originY?: number;
|
||||||
|
}
|
||||||
|
export const Circle: React.ComponentType<CircleProps>;
|
||||||
|
|
||||||
|
export interface GProps extends CommonProps {
|
||||||
|
rotation?: number;
|
||||||
|
originX?: number;
|
||||||
|
originY?: number;
|
||||||
|
}
|
||||||
|
export const G: React.ComponentType<React.PropsWithChildren<GProps>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -38,12 +38,15 @@ export async function ensureHealthPermissions(): Promise<boolean> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTodayHealthData(): Promise<TodayHealthData> {
|
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> {
|
||||||
console.log('开始获取今日健康数据...');
|
console.log('开始获取指定日期健康数据...', date);
|
||||||
|
|
||||||
const start = new Date();
|
const start = new Date(date);
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
const options = { startDate: start.toISOString() } as any;
|
const end = new Date(date);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const options = { startDate: start.toISOString(), endDate: end.toISOString() } as any;
|
||||||
|
|
||||||
console.log('查询选项:', options);
|
console.log('查询选项:', options);
|
||||||
|
|
||||||
@@ -73,11 +76,16 @@ export async function fetchTodayHealthData(): Promise<TodayHealthData> {
|
|||||||
return resolve(0);
|
return resolve(0);
|
||||||
}
|
}
|
||||||
console.log('卡路里数据:', res);
|
console.log('卡路里数据:', res);
|
||||||
// library returns value as number in kilocalories
|
// 求和该日内的所有记录(单位:千卡)
|
||||||
resolve(res[0]?.value || 0);
|
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
|
||||||
|
resolve(total);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('今日健康数据获取完成:', { steps, calories });
|
console.log('指定日期健康数据获取完成:', { steps, calories });
|
||||||
return { steps, activeEnergyBurned: calories };
|
return { steps, activeEnergyBurned: calories };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchTodayHealthData(): Promise<TodayHealthData> {
|
||||||
|
return fetchHealthDataForDate(new Date());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user