feat: 更新应用配置和引入新依赖
- 修改 app.json,禁用平板支持以优化用户体验 - 在 package.json 和 package-lock.json 中新增 react-native-toast-message 依赖,支持消息提示功能 - 在多个组件中集成 Toast 组件,提升用户交互反馈 - 更新训练计划相关逻辑,优化状态管理和数据处理 - 调整样式以适应新功能的展示和交互
This commit is contained in:
2
app.json
2
app.json
@@ -10,7 +10,7 @@
|
|||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"jsEngine": "jsc",
|
"jsEngine": "jsc",
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.anonymous.digitalpilates",
|
"bundleIdentifier": "com.anonymous.digitalpilates",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"ITSAppUsesNonExemptEncryption": false,
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
|
|||||||
@@ -160,11 +160,6 @@ export default function HomeScreen() {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
async function load() {
|
async function load() {
|
||||||
if (!isLoggedIn) {
|
|
||||||
console.log('fetchRecommendations not logged in');
|
|
||||||
setItems(getFallbackItems());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const cards = await fetchRecommendations();
|
const cards = await fetchRecommendations();
|
||||||
|
|
||||||
@@ -295,7 +290,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.featureCard, styles.featureCardPrimary]}
|
style={[styles.featureCard, styles.featureCardPrimary]}
|
||||||
onPress={() => router.push('/ai-posture-assessment')}
|
onPress={() => pushIfAuthedElseLogin('/ai-posture-assessment')}
|
||||||
>
|
>
|
||||||
<View style={styles.featureIconWrapper}>
|
<View style={styles.featureIconWrapper}>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { store } from '@/store';
|
|||||||
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RNExitApp from 'react-native-exit-app';
|
import RNExitApp from 'react-native-exit-app';
|
||||||
|
import Toast from 'react-native-toast-message';
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
onAgree={handlePrivacyAgree}
|
onAgree={handlePrivacyAgree}
|
||||||
onDisagree={handlePrivacyDisagree}
|
onDisagree={handlePrivacyDisagree}
|
||||||
/>
|
/>
|
||||||
|
<Toast />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
<Toast />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Bootstrapper>
|
</Bootstrapper>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch } from '@/hooks/redux';
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { login } from '@/store/userSlice';
|
import { login } from '@/store/userSlice';
|
||||||
|
import Toast from 'react-native-toast-message';
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -111,6 +112,11 @@ export default function LoginScreen() {
|
|||||||
throw new Error('未获取到 Apple 身份令牌');
|
throw new Error('未获取到 Apple 身份令牌');
|
||||||
}
|
}
|
||||||
await dispatch(login({ appleIdentityToken: identityToken })).unwrap();
|
await dispatch(login({ appleIdentityToken: identityToken })).unwrap();
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
text1: '登录成功',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
// 登录成功后处理重定向
|
// 登录成功后处理重定向
|
||||||
const to = searchParams?.redirectTo as string | undefined;
|
const to = searchParams?.redirectTo as string | undefined;
|
||||||
const paramsJson = searchParams?.redirectParams as string | undefined;
|
const paramsJson = searchParams?.redirectParams as string | undefined;
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export default function SplashScreen() {
|
|||||||
// 如果出现错误,默认显示引导页面
|
// 如果出现错误,默认显示引导页面
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// router.replace('/onboarding');
|
// router.replace('/onboarding');
|
||||||
// setIsLoading(false);
|
|
||||||
// }, 1000);
|
// }, 1000);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
type WeightUnit = 'kg' | 'lb';
|
type WeightUnit = 'kg' | 'lb';
|
||||||
type HeightUnit = 'cm' | 'ft';
|
type HeightUnit = 'cm' | 'ft';
|
||||||
@@ -50,7 +49,6 @@ const STORAGE_KEY = '@user_profile';
|
|||||||
export default function EditProfileScreen() {
|
export default function EditProfileScreen() {
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const colors = Colors[colorScheme ?? 'light'];
|
const colors = Colors[colorScheme ?? 'light'];
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const accountProfile = useAppSelector((s) => (s as any)?.user?.profile as any);
|
const accountProfile = useAppSelector((s) => (s as any)?.user?.profile as any);
|
||||||
const userId: string | undefined = useMemo(() => {
|
const userId: string | undefined = useMemo(() => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import MaskedView from '@react-native-masked-view/masked-view';
|
import MaskedView from '@react-native-masked-view/masked-view';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Alert, FlatList, Modal, Pressable, SafeAreaView, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
import { Alert, FlatList, Modal, Pressable, SafeAreaView, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeInUp,
|
FadeInUp,
|
||||||
@@ -21,6 +21,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
|
|||||||
import { Colors, palette } from '@/constants/Colors';
|
import { Colors, palette } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { TrainingPlan } from '@/services/trainingPlanApi';
|
||||||
import {
|
import {
|
||||||
addExercise,
|
addExercise,
|
||||||
clearExercises,
|
clearExercises,
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
loadExercises,
|
loadExercises,
|
||||||
toggleCompletion
|
toggleCompletion
|
||||||
} from '@/store/scheduleExerciseSlice';
|
} from '@/store/scheduleExerciseSlice';
|
||||||
import { activatePlan, clearError, deletePlan, loadPlans, type TrainingPlan } from '@/store/trainingPlanSlice';
|
import { activatePlan, clearError, deletePlan, loadPlans } from '@/store/trainingPlanSlice';
|
||||||
import { buildClassicalSession } from '@/utils/classicalSession';
|
import { buildClassicalSession } from '@/utils/classicalSession';
|
||||||
|
|
||||||
// Tab 类型定义
|
// Tab 类型定义
|
||||||
@@ -248,13 +249,14 @@ export default function TrainingPlanScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const params = useLocalSearchParams<{ planId?: string; tab?: string }>();
|
const params = useLocalSearchParams<{ planId?: string; tab?: string }>();
|
||||||
const { plans, currentId, loading, error } = useAppSelector((s) => s.trainingPlan);
|
const { plans, loading, error } = useAppSelector((s) => s.trainingPlan);
|
||||||
const { exercises, error: scheduleError } = useAppSelector((s) => s.scheduleExercise);
|
const { exercises, error: scheduleError } = useAppSelector((s) => s.scheduleExercise);
|
||||||
|
|
||||||
|
console.log('plans', plans);
|
||||||
// Tab 状态管理 - 支持从URL参数设置初始tab
|
// Tab 状态管理 - 支持从URL参数设置初始tab
|
||||||
const initialTab: TabType = params.tab === 'schedule' ? 'schedule' : 'list';
|
const initialTab: TabType = params.tab === 'schedule' ? 'schedule' : 'list';
|
||||||
const [activeTab, setActiveTab] = useState<TabType>(initialTab);
|
const [activeTab, setActiveTab] = useState<TabType>(initialTab);
|
||||||
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(params.planId || currentId || null);
|
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(params.planId || null);
|
||||||
|
|
||||||
// 一键排课配置
|
// 一键排课配置
|
||||||
const [genVisible, setGenVisible] = useState(false);
|
const [genVisible, setGenVisible] = useState(false);
|
||||||
@@ -274,9 +276,21 @@ export default function TrainingPlanScreen() {
|
|||||||
}
|
}
|
||||||
}, [selectedPlanId, dispatch]);
|
}, [selectedPlanId, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
// 每次页面获得焦点时,如果当前有选中的计划,重新加载其排课数据
|
||||||
dispatch(loadPlans());
|
useFocusEffect(
|
||||||
}, [dispatch]);
|
useCallback(() => {
|
||||||
|
if (selectedPlanId) {
|
||||||
|
dispatch(loadExercises(selectedPlanId));
|
||||||
|
}
|
||||||
|
}, [selectedPlanId, dispatch])
|
||||||
|
);
|
||||||
|
|
||||||
|
// 每次页面获得焦点时重新加载训练计划数据
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
dispatch(loadPlans());
|
||||||
|
}, [dispatch])
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -314,7 +328,7 @@ export default function TrainingPlanScreen() {
|
|||||||
const handleTabChange = (tab: TabType) => {
|
const handleTabChange = (tab: TabType) => {
|
||||||
if (tab === 'schedule' && !selectedPlanId && plans.length > 0) {
|
if (tab === 'schedule' && !selectedPlanId && plans.length > 0) {
|
||||||
// 如果没有选中计划但要切换到排课页面,自动选择当前激活的计划或第一个计划
|
// 如果没有选中计划但要切换到排课页面,自动选择当前激活的计划或第一个计划
|
||||||
const targetPlan = plans.find(p => p.id === currentId) || plans[0];
|
const targetPlan = plans.find(p => p.isActive) || plans[0];
|
||||||
setSelectedPlanId(targetPlan.id);
|
setSelectedPlanId(targetPlan.id);
|
||||||
}
|
}
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
@@ -426,7 +440,7 @@ export default function TrainingPlanScreen() {
|
|||||||
key={p.id}
|
key={p.id}
|
||||||
plan={p}
|
plan={p}
|
||||||
index={index}
|
index={index}
|
||||||
isActive={p.id === currentId}
|
isActive={p.isActive}
|
||||||
onPress={() => handlePlanSelect(p)}
|
onPress={() => handlePlanSelect(p)}
|
||||||
onDelete={() => dispatch(deletePlan(p.id))}
|
onDelete={() => dispatch(deletePlan(p.id))}
|
||||||
onActivate={() => handleActivate(p.id)}
|
onActivate={() => handleActivate(p.id)}
|
||||||
@@ -1098,7 +1112,7 @@ const styles = StyleSheet.create({
|
|||||||
// 主内容区域
|
// 主内容区域
|
||||||
mainContent: {
|
mainContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingBottom: 100, // 为底部 tab 留出空间
|
paddingBottom: 60, // 为底部 tab 留出空间
|
||||||
},
|
},
|
||||||
|
|
||||||
// 排课页面样式
|
// 排课页面样式
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ThemedView } from '@/components/ThemedView';
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { palette } from '@/constants/Colors';
|
import { palette } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
|
import { PlanGoal } from '@/services/trainingPlanApi';
|
||||||
import {
|
import {
|
||||||
clearError,
|
clearError,
|
||||||
loadPlans,
|
loadPlans,
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
setStartDateNextMonday,
|
setStartDateNextMonday,
|
||||||
setStartWeight,
|
setStartWeight,
|
||||||
toggleDayOfWeek,
|
toggleDayOfWeek,
|
||||||
type PlanGoal
|
|
||||||
} from '@/store/trainingPlanSlice';
|
} from '@/store/trainingPlanSlice';
|
||||||
|
|
||||||
const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'];
|
const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import {
|
import {
|
||||||
BMI_CATEGORIES,
|
BMI_CATEGORIES,
|
||||||
canCalculateBMI,
|
canCalculateBMI,
|
||||||
@@ -5,7 +6,6 @@ import {
|
|||||||
type BMIResult
|
type BMIResult
|
||||||
} from '@/utils/bmi';
|
} from '@/utils/bmi';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import Toast from 'react-native-toast-message';
|
||||||
|
|
||||||
interface BMICardProps {
|
interface BMICardProps {
|
||||||
weight?: number;
|
weight?: number;
|
||||||
@@ -24,7 +25,7 @@ interface BMICardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BMICard({ weight, height, style }: BMICardProps) {
|
export function BMICard({ weight, height, style }: BMICardProps) {
|
||||||
const router = useRouter();
|
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
const [showInfoModal, setShowInfoModal] = useState(false);
|
const [showInfoModal, setShowInfoModal] = useState(false);
|
||||||
|
|
||||||
const canCalculate = canCalculateBMI(weight, height);
|
const canCalculate = canCalculateBMI(weight, height);
|
||||||
@@ -41,7 +42,11 @@ export function BMICard({ weight, height, style }: BMICardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleGoToProfile = () => {
|
const handleGoToProfile = () => {
|
||||||
router.push('/profile/edit');
|
Toast.show({
|
||||||
|
text1: '请先登录',
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
pushIfAuthedElseLogin('/profile/edit');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShowInfoModal = () => {
|
const handleShowInfoModal = () => {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function useAuthGuard() {
|
|||||||
await dispatch(logoutAction()).unwrap();
|
await dispatch(logoutAction()).unwrap();
|
||||||
|
|
||||||
// 跳转到登录页面
|
// 跳转到登录页面
|
||||||
router.replace('/auth/login');
|
router.push('/auth/login');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('退出登录失败:', error);
|
console.error('退出登录失败:', error);
|
||||||
Alert.alert('错误', '退出登录失败,请稍后重试');
|
Alert.alert('错误', '退出登录失败,请稍后重试');
|
||||||
@@ -105,7 +105,7 @@ export function useAuthGuard() {
|
|||||||
Alert.alert('账号已注销', '您的账号已成功注销', [
|
Alert.alert('账号已注销', '您的账号已成功注销', [
|
||||||
{
|
{
|
||||||
text: '确定',
|
text: '确定',
|
||||||
onPress: () => router.replace('/auth/login'),
|
onPress: () => router.push('/auth/login'),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -344,10 +344,14 @@
|
|||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates;
|
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates;
|
||||||
PRODUCT_NAME = digitalpilates;
|
PRODUCT_NAME = digitalpilates;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "digitalpilates/digitalpilates-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "digitalpilates/digitalpilates-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -377,9 +381,13 @@
|
|||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates;
|
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates;
|
||||||
PRODUCT_NAME = digitalpilates;
|
PRODUCT_NAME = digitalpilates;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "digitalpilates/digitalpilates-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "digitalpilates/digitalpilates-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -439,10 +447,7 @@
|
|||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = "$(inherited) ";
|
||||||
"$(inherited)",
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
@@ -497,10 +502,7 @@
|
|||||||
);
|
);
|
||||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = "$(inherited) ";
|
||||||
"$(inherited)",
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = false;
|
USE_HERMES = false;
|
||||||
|
|||||||
@@ -1,91 +1,84 @@
|
|||||||
<?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>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>普拉提助手</string>
|
<string>普拉提助手</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.2</string>
|
<string>1.0.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>digitalpilates</string>
|
<string>digitalpilates</string>
|
||||||
<string>com.anonymous.digitalpilates</string>
|
<string>com.anonymous.digitalpilates</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>2</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>应用需要使用相机以拍摄您的体态照片用于AI测评。</string>
|
<string>应用需要使用相机以拍摄您的体态照片用于AI测评。</string>
|
||||||
<key>NSHealthShareUsageDescription</key>
|
<key>NSHealthShareUsageDescription</key>
|
||||||
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
|
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
|
||||||
<key>NSHealthUpdateUsageDescription</key>
|
<key>NSHealthUpdateUsageDescription</key>
|
||||||
<string>Allow $(PRODUCT_NAME) to update health info</string>
|
<string>Allow $(PRODUCT_NAME) to update health info</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>应用需要访问相册以选择您的体态照片用于AI测评。</string>
|
<string>应用需要访问相册以选择您的体态照片用于AI测评。</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
<array>
|
<array>
|
||||||
<string>arm64</string>
|
<string>arm64</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIRequiresFullScreen</key>
|
<key>UIRequiresFullScreen</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>UIStatusBarStyle</key>
|
<key>UIStatusBarStyle</key>
|
||||||
<string>UIStatusBarStyleDefault</string>
|
<string>UIStatusBarStyleDefault</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<array>
|
<string>Light</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<false/>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
</dict>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIUserInterfaceStyle</key>
|
|
||||||
<string>Light</string>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -49,6 +49,7 @@
|
|||||||
"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-svg": "^15.12.1",
|
||||||
|
"react-native-toast-message": "^2.3.3",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5",
|
||||||
"react-redux": "^9.2.0"
|
"react-redux": "^9.2.0"
|
||||||
@@ -11227,6 +11228,16 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-toast-message": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-uuid": {
|
"node_modules/react-native-uuid": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/react-native-uuid/-/react-native-uuid-2.0.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/react-native-uuid/-/react-native-uuid-2.0.3.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"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-svg": "^15.12.1",
|
||||||
|
"react-native-toast-message": "^2.3.3",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5",
|
||||||
"react-redux": "^9.2.0"
|
"react-redux": "^9.2.0"
|
||||||
|
|||||||
@@ -11,56 +11,52 @@ export interface CreateTrainingPlanDto {
|
|||||||
preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | '';
|
preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrainingPlanResponse {
|
export type PlanMode = 'daysOfWeek' | 'sessionsPerWeek';
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: string;
|
|
||||||
startDate: string;
|
|
||||||
mode: 'daysOfWeek' | 'sessionsPerWeek';
|
|
||||||
daysOfWeek: number[];
|
|
||||||
sessionsPerWeek: number;
|
|
||||||
goal: string;
|
|
||||||
startWeightKg: number | null;
|
|
||||||
preferredTimeOfDay: 'morning' | 'noon' | 'evening' | '';
|
|
||||||
updatedAt: string;
|
|
||||||
deleted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrainingPlanSummary {
|
export type PlanGoal =
|
||||||
id: string;
|
| 'postpartum_recovery' // 产后恢复
|
||||||
createdAt: string;
|
| 'fat_loss' // 减脂塑形
|
||||||
startDate: string;
|
| 'posture_correction' // 体态矫正
|
||||||
goal: string;
|
| 'core_strength' // 核心力量
|
||||||
mode: 'daysOfWeek' | 'sessionsPerWeek';
|
| 'flexibility' // 柔韧灵活
|
||||||
daysOfWeek: number[];
|
| 'rehab' // 康复保健
|
||||||
sessionsPerWeek: number;
|
| 'stress_relief'; // 释压放松
|
||||||
preferredTimeOfDay: 'morning' | 'noon' | 'evening' | '';
|
|
||||||
startWeightKg: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export type TrainingPlan = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string; // ISO
|
||||||
|
startDate: string; // ISO (当天或下周一)
|
||||||
|
mode: PlanMode;
|
||||||
|
daysOfWeek: number[]; // 0(日) - 6(六)
|
||||||
|
sessionsPerWeek: number; // 1..7
|
||||||
|
goal: PlanGoal | '';
|
||||||
|
startWeightKg?: number;
|
||||||
|
preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | '';
|
||||||
|
name?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
export interface TrainingPlanListResponse {
|
export interface TrainingPlanListResponse {
|
||||||
list: TrainingPlanSummary[];
|
list: TrainingPlan[];
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrainingPlanApi {
|
class TrainingPlanApi {
|
||||||
async create(dto: CreateTrainingPlanDto): Promise<TrainingPlanResponse> {
|
async create(dto: CreateTrainingPlanDto): Promise<TrainingPlan> {
|
||||||
return api.post<TrainingPlanResponse>('/training-plans', dto);
|
return api.post<TrainingPlan>('/training-plans', dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async list(page: number = 1, limit: number = 10): Promise<TrainingPlanListResponse> {
|
async list(page: number = 1, limit: number = 10): Promise<TrainingPlanListResponse> {
|
||||||
return api.get<TrainingPlanListResponse>(`/training-plans?page=${page}&limit=${limit}`);
|
return api.get<TrainingPlanListResponse>(`/training-plans?page=${page}&limit=${limit}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async detail(id: string): Promise<TrainingPlanResponse> {
|
async detail(id: string): Promise<TrainingPlan> {
|
||||||
return api.get<TrainingPlanResponse>(`/training-plans/${id}`);
|
return api.get<TrainingPlan>(`/training-plans/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, dto: CreateTrainingPlanDto): Promise<TrainingPlanResponse> {
|
async update(id: string, dto: CreateTrainingPlanDto): Promise<TrainingPlan> {
|
||||||
return api.post<TrainingPlanResponse>(`/training-plans/${id}/update`, dto);
|
return api.post<TrainingPlan>(`/training-plans/${id}/update`, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<{ success: boolean }> {
|
async delete(id: string): Promise<{ success: boolean }> {
|
||||||
@@ -71,9 +67,9 @@ class TrainingPlanApi {
|
|||||||
return api.post<{ success: boolean }>(`/training-plans/${id}/activate`);
|
return api.post<{ success: boolean }>(`/training-plans/${id}/activate`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActivePlan(): Promise<TrainingPlanResponse | null> {
|
async getActivePlan(): Promise<TrainingPlan | null> {
|
||||||
try {
|
try {
|
||||||
return api.get<TrainingPlanResponse>('/training-plans/active');
|
return api.get<TrainingPlan>('/training-plans/active');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果没有激活的计划,返回null
|
// 如果没有激活的计划,返回null
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,41 +1,17 @@
|
|||||||
import { CreateTrainingPlanDto, trainingPlanApi, TrainingPlanSummary } from '@/services/trainingPlanApi';
|
import { CreateTrainingPlanDto, PlanGoal, PlanMode, TrainingPlan, trainingPlanApi } from '@/services/trainingPlanApi';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export type PlanMode = 'daysOfWeek' | 'sessionsPerWeek';
|
|
||||||
|
|
||||||
export type PlanGoal =
|
|
||||||
| 'postpartum_recovery' // 产后恢复
|
|
||||||
| 'fat_loss' // 减脂塑形
|
|
||||||
| 'posture_correction' // 体态矫正
|
|
||||||
| 'core_strength' // 核心力量
|
|
||||||
| 'flexibility' // 柔韧灵活
|
|
||||||
| 'rehab' // 康复保健
|
|
||||||
| 'stress_relief'; // 释压放松
|
|
||||||
|
|
||||||
export type TrainingPlan = {
|
|
||||||
id: string;
|
|
||||||
createdAt: string; // ISO
|
|
||||||
startDate: string; // ISO (当天或下周一)
|
|
||||||
mode: PlanMode;
|
|
||||||
daysOfWeek: number[]; // 0(日) - 6(六)
|
|
||||||
sessionsPerWeek: number; // 1..7
|
|
||||||
goal: PlanGoal | '';
|
|
||||||
startWeightKg?: number;
|
|
||||||
preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | '';
|
|
||||||
name?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TrainingPlanState = {
|
export type TrainingPlanState = {
|
||||||
plans: TrainingPlan[];
|
plans: TrainingPlan[];
|
||||||
currentId?: string | null;
|
|
||||||
editingId?: string | null;
|
editingId?: string | null;
|
||||||
draft: Omit<TrainingPlan, 'id' | 'createdAt'>;
|
draft: Omit<TrainingPlan, 'id' | 'createdAt'>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY_LEGACY_SINGLE = '@training_plan';
|
|
||||||
const STORAGE_KEY_LIST = '@training_plans';
|
const STORAGE_KEY_LIST = '@training_plans';
|
||||||
|
|
||||||
function nextMondayISO(): string {
|
function nextMondayISO(): string {
|
||||||
@@ -50,7 +26,6 @@ function nextMondayISO(): string {
|
|||||||
|
|
||||||
const initialState: TrainingPlanState = {
|
const initialState: TrainingPlanState = {
|
||||||
plans: [],
|
plans: [],
|
||||||
currentId: null,
|
|
||||||
editingId: null,
|
editingId: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -73,13 +48,9 @@ export const loadPlans = createAsyncThunk('trainingPlan/loadPlans', async (_, {
|
|||||||
try {
|
try {
|
||||||
// 尝试从服务器获取数据
|
// 尝试从服务器获取数据
|
||||||
const response = await trainingPlanApi.list(1, 100); // 获取所有计划
|
const response = await trainingPlanApi.list(1, 100); // 获取所有计划
|
||||||
console.log('response', response);
|
const plans = response.list;
|
||||||
const plans: TrainingPlanSummary[] = response.list;
|
|
||||||
|
|
||||||
// 读取最后一次使用的 currentId(从本地存储)
|
return { plans };
|
||||||
const currentId = (await AsyncStorage.getItem(`${STORAGE_KEY_LIST}__currentId`)) || null;
|
|
||||||
|
|
||||||
return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null };
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 如果API调用失败,回退到本地存储
|
// 如果API调用失败,回退到本地存储
|
||||||
console.warn('API调用失败,使用本地存储:', error.message);
|
console.warn('API调用失败,使用本地存储:', error.message);
|
||||||
@@ -89,27 +60,13 @@ export const loadPlans = createAsyncThunk('trainingPlan/loadPlans', async (_, {
|
|||||||
if (listStr) {
|
if (listStr) {
|
||||||
try {
|
try {
|
||||||
const plans = JSON.parse(listStr) as TrainingPlan[];
|
const plans = JSON.parse(listStr) as TrainingPlan[];
|
||||||
const currentId = (await AsyncStorage.getItem(`${STORAGE_KEY_LIST}__currentId`)) || null;
|
return { plans } as { plans: TrainingPlan[] };
|
||||||
return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null };
|
|
||||||
} catch {
|
} catch {
|
||||||
// 解析失败则视为无数据
|
// 解析失败则视为无数据
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 旧版:单计划
|
return { plans: [] } as { plans: TrainingPlan[] };
|
||||||
const legacyStr = await AsyncStorage.getItem(STORAGE_KEY_LEGACY_SINGLE);
|
|
||||||
if (legacyStr) {
|
|
||||||
try {
|
|
||||||
const legacy = JSON.parse(legacyStr) as TrainingPlan;
|
|
||||||
const plans = [legacy];
|
|
||||||
const currentId = legacy.id;
|
|
||||||
return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null };
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { plans: [], currentId: null } as { plans: TrainingPlan[]; currentId: string | null };
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,28 +91,9 @@ export const saveDraftAsPlan = createAsyncThunk(
|
|||||||
preferredTimeOfDay: draft.preferredTimeOfDay,
|
preferredTimeOfDay: draft.preferredTimeOfDay,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await trainingPlanApi.create(createDto);
|
const newPlan = await trainingPlanApi.create(createDto);
|
||||||
|
|
||||||
const plan: TrainingPlan = {
|
return newPlan;
|
||||||
id: response.id,
|
|
||||||
createdAt: response.createdAt,
|
|
||||||
startDate: response.startDate,
|
|
||||||
mode: response.mode,
|
|
||||||
daysOfWeek: response.daysOfWeek,
|
|
||||||
sessionsPerWeek: response.sessionsPerWeek,
|
|
||||||
goal: response.goal as PlanGoal,
|
|
||||||
startWeightKg: response.startWeightKg || undefined,
|
|
||||||
preferredTimeOfDay: response.preferredTimeOfDay,
|
|
||||||
name: response.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextPlans = [...(s.plans || []), plan];
|
|
||||||
|
|
||||||
// 同时保存到本地存储作为缓存
|
|
||||||
await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans));
|
|
||||||
await AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, plan.id);
|
|
||||||
|
|
||||||
return { plans: nextPlans, currentId: plan.id } as { plans: TrainingPlan[]; currentId: string };
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return rejectWithValue(error.message || '创建训练计划失败');
|
return rejectWithValue(error.message || '创建训练计划失败');
|
||||||
}
|
}
|
||||||
@@ -257,16 +195,11 @@ export const deletePlan = createAsyncThunk(
|
|||||||
|
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
const nextPlans = (s.plans || []).filter((p) => p.id !== planId);
|
const nextPlans = (s.plans || []).filter((p) => p.id !== planId);
|
||||||
let nextCurrentId = s.currentId || null;
|
|
||||||
if (nextCurrentId === planId) {
|
|
||||||
nextCurrentId = nextPlans.length > 0 ? nextPlans[nextPlans.length - 1].id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同时更新本地存储
|
// 同时更新本地存储
|
||||||
await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans));
|
await AsyncStorage.setItem(STORAGE_KEY_LIST, JSON.stringify(nextPlans));
|
||||||
await AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, nextCurrentId ?? '');
|
|
||||||
|
|
||||||
return { plans: nextPlans, currentId: nextCurrentId } as { plans: TrainingPlan[]; currentId: string | null };
|
return { plans: nextPlans } as { plans: TrainingPlan[] };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return rejectWithValue(error.message || '删除训练计划失败');
|
return rejectWithValue(error.message || '删除训练计划失败');
|
||||||
}
|
}
|
||||||
@@ -277,11 +210,6 @@ const trainingPlanSlice = createSlice({
|
|||||||
name: 'trainingPlan',
|
name: 'trainingPlan',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setCurrentPlan(state, action: PayloadAction<string | null | undefined>) {
|
|
||||||
state.currentId = action.payload ?? null;
|
|
||||||
// 保存到本地存储
|
|
||||||
AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, action.payload ?? '');
|
|
||||||
},
|
|
||||||
setMode(state, action: PayloadAction<PlanMode>) {
|
setMode(state, action: PayloadAction<PlanMode>) {
|
||||||
state.draft.mode = action.payload;
|
state.draft.mode = action.payload;
|
||||||
},
|
},
|
||||||
@@ -333,13 +261,6 @@ const trainingPlanSlice = createSlice({
|
|||||||
.addCase(loadPlans.fulfilled, (state, action) => {
|
.addCase(loadPlans.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.plans = action.payload.plans;
|
state.plans = action.payload.plans;
|
||||||
state.currentId = action.payload.currentId;
|
|
||||||
// 若存在历史计划,初始化 draft 基于该计划(便于编辑)
|
|
||||||
const current = state.plans.find((p) => p.id === state.currentId) || state.plans[state.plans.length - 1];
|
|
||||||
if (current) {
|
|
||||||
const { id, createdAt, ...rest } = current;
|
|
||||||
state.draft = { ...rest };
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.addCase(loadPlans.rejected, (state, action) => {
|
.addCase(loadPlans.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@@ -352,8 +273,6 @@ const trainingPlanSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(saveDraftAsPlan.fulfilled, (state, action) => {
|
.addCase(saveDraftAsPlan.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.plans = action.payload.plans;
|
|
||||||
state.currentId = action.payload.currentId;
|
|
||||||
})
|
})
|
||||||
.addCase(saveDraftAsPlan.rejected, (state, action) => {
|
.addCase(saveDraftAsPlan.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@@ -393,9 +312,6 @@ const trainingPlanSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(activatePlan.fulfilled, (state, action) => {
|
.addCase(activatePlan.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.currentId = action.payload.id;
|
|
||||||
// 保存到本地存储
|
|
||||||
AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, action.payload.id);
|
|
||||||
})
|
})
|
||||||
.addCase(activatePlan.rejected, (state, action) => {
|
.addCase(activatePlan.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@@ -409,7 +325,6 @@ const trainingPlanSlice = createSlice({
|
|||||||
.addCase(deletePlan.fulfilled, (state, action) => {
|
.addCase(deletePlan.fulfilled, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.plans = action.payload.plans;
|
state.plans = action.payload.plans;
|
||||||
state.currentId = action.payload.currentId;
|
|
||||||
})
|
})
|
||||||
.addCase(deletePlan.rejected, (state, action) => {
|
.addCase(deletePlan.rejected, (state, action) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@@ -419,7 +334,6 @@ const trainingPlanSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
setCurrentPlan,
|
|
||||||
setMode,
|
setMode,
|
||||||
toggleDayOfWeek,
|
toggleDayOfWeek,
|
||||||
setSessionsPerWeek,
|
setSessionsPerWeek,
|
||||||
|
|||||||
Reference in New Issue
Block a user