diff --git a/app.json b/app.json index 1762424..d086e1f 100644 --- a/app.json +++ b/app.json @@ -10,7 +10,7 @@ "newArchEnabled": true, "jsEngine": "jsc", "ios": { - "supportsTablet": true, + "supportsTablet": false, "bundleIdentifier": "com.anonymous.digitalpilates", "infoPlist": { "ITSAppUsesNonExemptEncryption": false, diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5637f32..8a6fe38 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -160,11 +160,6 @@ export default function HomeScreen() { React.useEffect(() => { let canceled = false; async function load() { - if (!isLoggedIn) { - console.log('fetchRecommendations not logged in'); - setItems(getFallbackItems()); - return; - } try { const cards = await fetchRecommendations(); @@ -295,7 +290,7 @@ export default function HomeScreen() { router.push('/ai-posture-assessment')} + onPress={() => pushIfAuthedElseLogin('/ai-posture-assessment')} > + ); } @@ -91,6 +93,7 @@ export default function RootLayout() { + diff --git a/app/auth/login.tsx b/app/auth/login.tsx index ee1f1b9..65f8d7b 100644 --- a/app/auth/login.tsx +++ b/app/auth/login.tsx @@ -13,6 +13,7 @@ import { Colors } from '@/constants/Colors'; import { useAppDispatch } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { login } from '@/store/userSlice'; +import Toast from 'react-native-toast-message'; export default function LoginScreen() { const router = useRouter(); @@ -111,6 +112,11 @@ export default function LoginScreen() { throw new Error('未获取到 Apple 身份令牌'); } await dispatch(login({ appleIdentityToken: identityToken })).unwrap(); + + Toast.show({ + text1: '登录成功', + type: 'success', + }); // 登录成功后处理重定向 const to = searchParams?.redirectTo as string | undefined; const paramsJson = searchParams?.redirectParams as string | undefined; diff --git a/app/index.tsx b/app/index.tsx index 0756595..ed68baf 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -31,7 +31,6 @@ export default function SplashScreen() { // 如果出现错误,默认显示引导页面 // setTimeout(() => { // router.replace('/onboarding'); - // setIsLoading(false); // }, 1000); } setIsLoading(false); diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx index 9efae71..540bc5a 100644 --- a/app/profile/edit.tsx +++ b/app/profile/edit.tsx @@ -29,7 +29,6 @@ import { TouchableOpacity, View, } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; type WeightUnit = 'kg' | 'lb'; type HeightUnit = 'cm' | 'ft'; @@ -50,7 +49,6 @@ const STORAGE_KEY = '@user_profile'; export default function EditProfileScreen() { const colorScheme = useColorScheme(); const colors = Colors[colorScheme ?? 'light']; - const insets = useSafeAreaInsets(); const dispatch = useAppDispatch(); const accountProfile = useAppSelector((s) => (s as any)?.user?.profile as any); const userId: string | undefined = useMemo(() => { diff --git a/app/training-plan.tsx b/app/training-plan.tsx index 08c9917..b34c968 100644 --- a/app/training-plan.tsx +++ b/app/training-plan.tsx @@ -1,8 +1,8 @@ import { Ionicons } from '@expo/vector-icons'; import MaskedView from '@react-native-masked-view/masked-view'; import { LinearGradient } from 'expo-linear-gradient'; -import { useLocalSearchParams, useRouter } from 'expo-router'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'; +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 Animated, { FadeInUp, @@ -21,6 +21,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors, palette } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { TrainingPlan } from '@/services/trainingPlanApi'; import { addExercise, clearExercises, @@ -29,7 +30,7 @@ import { loadExercises, toggleCompletion } 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'; // Tab 类型定义 @@ -248,13 +249,14 @@ export default function TrainingPlanScreen() { const router = useRouter(); const dispatch = useAppDispatch(); 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); + console.log('plans', plans); // Tab 状态管理 - 支持从URL参数设置初始tab const initialTab: TabType = params.tab === 'schedule' ? 'schedule' : 'list'; const [activeTab, setActiveTab] = useState(initialTab); - const [selectedPlanId, setSelectedPlanId] = useState(params.planId || currentId || null); + const [selectedPlanId, setSelectedPlanId] = useState(params.planId || null); // 一键排课配置 const [genVisible, setGenVisible] = useState(false); @@ -274,9 +276,21 @@ export default function TrainingPlanScreen() { } }, [selectedPlanId, dispatch]); - useEffect(() => { - dispatch(loadPlans()); - }, [dispatch]); + // 每次页面获得焦点时,如果当前有选中的计划,重新加载其排课数据 + useFocusEffect( + useCallback(() => { + if (selectedPlanId) { + dispatch(loadExercises(selectedPlanId)); + } + }, [selectedPlanId, dispatch]) + ); + + // 每次页面获得焦点时重新加载训练计划数据 + useFocusEffect( + useCallback(() => { + dispatch(loadPlans()); + }, [dispatch]) + ); useEffect(() => { if (error) { @@ -314,7 +328,7 @@ export default function TrainingPlanScreen() { const handleTabChange = (tab: TabType) => { 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); } setActiveTab(tab); @@ -426,7 +440,7 @@ export default function TrainingPlanScreen() { key={p.id} plan={p} index={index} - isActive={p.id === currentId} + isActive={p.isActive} onPress={() => handlePlanSelect(p)} onDelete={() => dispatch(deletePlan(p.id))} onActivate={() => handleActivate(p.id)} @@ -1098,7 +1112,7 @@ const styles = StyleSheet.create({ // 主内容区域 mainContent: { flex: 1, - paddingBottom: 100, // 为底部 tab 留出空间 + paddingBottom: 60, // 为底部 tab 留出空间 }, // 排课页面样式 diff --git a/app/training-plan/create.tsx b/app/training-plan/create.tsx index 95d970c..0d8bb1c 100644 --- a/app/training-plan/create.tsx +++ b/app/training-plan/create.tsx @@ -8,6 +8,7 @@ import { ThemedView } from '@/components/ThemedView'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { palette } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { PlanGoal } from '@/services/trainingPlanApi'; import { clearError, loadPlans, @@ -21,7 +22,6 @@ import { setStartDateNextMonday, setStartWeight, toggleDayOfWeek, - type PlanGoal } from '@/store/trainingPlanSlice'; const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六']; diff --git a/components/BMICard.tsx b/components/BMICard.tsx index 7fccbbf..74f070c 100644 --- a/components/BMICard.tsx +++ b/components/BMICard.tsx @@ -1,3 +1,4 @@ +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { BMI_CATEGORIES, canCalculateBMI, @@ -5,7 +6,6 @@ import { type BMIResult } from '@/utils/bmi'; import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; import React, { useState } from 'react'; import { Modal, @@ -16,6 +16,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import Toast from 'react-native-toast-message'; interface BMICardProps { weight?: number; @@ -24,7 +25,7 @@ interface BMICardProps { } export function BMICard({ weight, height, style }: BMICardProps) { - const router = useRouter(); + const { pushIfAuthedElseLogin } = useAuthGuard(); const [showInfoModal, setShowInfoModal] = useState(false); const canCalculate = canCalculateBMI(weight, height); @@ -41,7 +42,11 @@ export function BMICard({ weight, height, style }: BMICardProps) { } const handleGoToProfile = () => { - router.push('/profile/edit'); + Toast.show({ + text1: '请先登录', + type: 'info', + }); + pushIfAuthedElseLogin('/profile/edit'); }; const handleShowInfoModal = () => { @@ -436,4 +441,4 @@ const styles = StyleSheet.create({ borderRadius: 12, lineHeight: 20, }, -}); \ No newline at end of file +}); diff --git a/hooks/useAuthGuard.ts b/hooks/useAuthGuard.ts index bf68749..956ac10 100644 --- a/hooks/useAuthGuard.ts +++ b/hooks/useAuthGuard.ts @@ -64,7 +64,7 @@ export function useAuthGuard() { await dispatch(logoutAction()).unwrap(); // 跳转到登录页面 - router.replace('/auth/login'); + router.push('/auth/login'); } catch (error) { console.error('退出登录失败:', error); Alert.alert('错误', '退出登录失败,请稍后重试'); @@ -105,7 +105,7 @@ export function useAuthGuard() { Alert.alert('账号已注销', '您的账号已成功注销', [ { text: '确定', - onPress: () => router.replace('/auth/login'), + onPress: () => router.push('/auth/login'), }, ]); } catch (error: any) { diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index b18038b..8779af2 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -344,10 +344,14 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.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_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -377,9 +381,13 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.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_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -439,10 +447,7 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -497,10 +502,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = false; diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index ceea14f..15e1b7d 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -1,91 +1,84 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - 普拉提助手 - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0.2 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - digitalpilates - com.anonymous.digitalpilates - - - - CFBundleVersion - 2 - ITSAppUsesNonExemptEncryption - - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsLocalNetworking - - - NSCameraUsageDescription - 应用需要使用相机以拍摄您的体态照片用于AI测评。 - NSHealthShareUsageDescription - 应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。 - NSHealthUpdateUsageDescription - Allow $(PRODUCT_NAME) to update health info - NSPhotoLibraryAddUsageDescription - 应用需要写入相册以保存拍摄的体态照片(可选)。 - NSPhotoLibraryUsageDescription - 应用需要访问相册以选择您的体态照片用于AI测评。 - NSUserActivityTypes - - $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Light - UIViewControllerBasedStatusBarAppearance - - - \ No newline at end of file + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 普拉提助手 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.2 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + digitalpilates + com.anonymous.digitalpilates + + + + CFBundleVersion + 2 + ITSAppUsesNonExemptEncryption + + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSCameraUsageDescription + 应用需要使用相机以拍摄您的体态照片用于AI测评。 + NSHealthShareUsageDescription + 应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。 + NSHealthUpdateUsageDescription + Allow $(PRODUCT_NAME) to update health info + NSPhotoLibraryAddUsageDescription + 应用需要写入相册以保存拍摄的体态照片(可选)。 + NSPhotoLibraryUsageDescription + 应用需要访问相册以选择您的体态照片用于AI测评。 + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + + + diff --git a/package-lock.json b/package-lock.json index 64e9e93..052dbe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", "react-native-svg": "^15.12.1", + "react-native-toast-message": "^2.3.3", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "react-redux": "^9.2.0" @@ -11227,6 +11228,16 @@ "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": { "version": "2.0.3", "resolved": "https://mirrors.tencent.com/npm/react-native-uuid/-/react-native-uuid-2.0.3.tgz", diff --git a/package.json b/package.json index 7eb4cb6..9be136a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", "react-native-svg": "^15.12.1", + "react-native-toast-message": "^2.3.3", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", "react-redux": "^9.2.0" diff --git a/services/trainingPlanApi.ts b/services/trainingPlanApi.ts index e34a534..07658b3 100644 --- a/services/trainingPlanApi.ts +++ b/services/trainingPlanApi.ts @@ -11,56 +11,52 @@ export interface CreateTrainingPlanDto { preferredTimeOfDay?: 'morning' | 'noon' | 'evening' | ''; } -export interface TrainingPlanResponse { - 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 type PlanMode = 'daysOfWeek' | 'sessionsPerWeek'; -export interface TrainingPlanSummary { - id: string; - createdAt: string; - startDate: string; - goal: string; - mode: 'daysOfWeek' | 'sessionsPerWeek'; - daysOfWeek: number[]; - sessionsPerWeek: number; - preferredTimeOfDay: 'morning' | 'noon' | 'evening' | ''; - startWeightKg: number | null; -} +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; + isActive?: boolean; +}; export interface TrainingPlanListResponse { - list: TrainingPlanSummary[]; + list: TrainingPlan[]; total: number; page: number; limit: number; } class TrainingPlanApi { - async create(dto: CreateTrainingPlanDto): Promise { - return api.post('/training-plans', dto); + async create(dto: CreateTrainingPlanDto): Promise { + return api.post('/training-plans', dto); } async list(page: number = 1, limit: number = 10): Promise { return api.get(`/training-plans?page=${page}&limit=${limit}`); } - async detail(id: string): Promise { - return api.get(`/training-plans/${id}`); + async detail(id: string): Promise { + return api.get(`/training-plans/${id}`); } - async update(id: string, dto: CreateTrainingPlanDto): Promise { - return api.post(`/training-plans/${id}/update`, dto); + async update(id: string, dto: CreateTrainingPlanDto): Promise { + return api.post(`/training-plans/${id}/update`, dto); } async delete(id: string): Promise<{ success: boolean }> { @@ -71,9 +67,9 @@ class TrainingPlanApi { return api.post<{ success: boolean }>(`/training-plans/${id}/activate`); } - async getActivePlan(): Promise { + async getActivePlan(): Promise { try { - return api.get('/training-plans/active'); + return api.get('/training-plans/active'); } catch (error) { // 如果没有激活的计划,返回null return null; diff --git a/store/trainingPlanSlice.ts b/store/trainingPlanSlice.ts index 6a2e200..2dacef1 100644 --- a/store/trainingPlanSlice.ts +++ b/store/trainingPlanSlice.ts @@ -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 { 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 = { plans: TrainingPlan[]; - currentId?: string | null; editingId?: string | null; draft: Omit; loading: boolean; error: string | null; }; -const STORAGE_KEY_LEGACY_SINGLE = '@training_plan'; const STORAGE_KEY_LIST = '@training_plans'; function nextMondayISO(): string { @@ -50,7 +26,6 @@ function nextMondayISO(): string { const initialState: TrainingPlanState = { plans: [], - currentId: null, editingId: null, loading: false, error: null, @@ -73,13 +48,9 @@ export const loadPlans = createAsyncThunk('trainingPlan/loadPlans', async (_, { try { // 尝试从服务器获取数据 const response = await trainingPlanApi.list(1, 100); // 获取所有计划 - console.log('response', response); - const plans: TrainingPlanSummary[] = response.list; + const plans = response.list; - // 读取最后一次使用的 currentId(从本地存储) - const currentId = (await AsyncStorage.getItem(`${STORAGE_KEY_LIST}__currentId`)) || null; - - return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null }; + return { plans }; } catch (error: any) { // 如果API调用失败,回退到本地存储 console.warn('API调用失败,使用本地存储:', error.message); @@ -89,27 +60,13 @@ export const loadPlans = createAsyncThunk('trainingPlan/loadPlans', async (_, { if (listStr) { try { const plans = JSON.parse(listStr) as TrainingPlan[]; - const currentId = (await AsyncStorage.getItem(`${STORAGE_KEY_LIST}__currentId`)) || null; - return { plans, currentId } as { plans: TrainingPlan[]; currentId: string | null }; + return { plans } as { plans: TrainingPlan[] }; } catch { // 解析失败则视为无数据 } } - // 旧版:单计划 - 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 }; + return { plans: [] } as { plans: TrainingPlan[] }; } }); @@ -134,28 +91,9 @@ export const saveDraftAsPlan = createAsyncThunk( preferredTimeOfDay: draft.preferredTimeOfDay, }; - const response = await trainingPlanApi.create(createDto); + const newPlan = await trainingPlanApi.create(createDto); - const plan: TrainingPlan = { - 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 }; + return newPlan; } catch (error: any) { return rejectWithValue(error.message || '创建训练计划失败'); } @@ -257,16 +195,11 @@ export const deletePlan = createAsyncThunk( // 更新本地状态 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}__currentId`, nextCurrentId ?? ''); - return { plans: nextPlans, currentId: nextCurrentId } as { plans: TrainingPlan[]; currentId: string | null }; + return { plans: nextPlans } as { plans: TrainingPlan[] }; } catch (error: any) { return rejectWithValue(error.message || '删除训练计划失败'); } @@ -277,11 +210,6 @@ const trainingPlanSlice = createSlice({ name: 'trainingPlan', initialState, reducers: { - setCurrentPlan(state, action: PayloadAction) { - state.currentId = action.payload ?? null; - // 保存到本地存储 - AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, action.payload ?? ''); - }, setMode(state, action: PayloadAction) { state.draft.mode = action.payload; }, @@ -333,13 +261,6 @@ const trainingPlanSlice = createSlice({ .addCase(loadPlans.fulfilled, (state, action) => { state.loading = false; 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) => { state.loading = false; @@ -352,8 +273,6 @@ const trainingPlanSlice = createSlice({ }) .addCase(saveDraftAsPlan.fulfilled, (state, action) => { state.loading = false; - state.plans = action.payload.plans; - state.currentId = action.payload.currentId; }) .addCase(saveDraftAsPlan.rejected, (state, action) => { state.loading = false; @@ -393,9 +312,6 @@ const trainingPlanSlice = createSlice({ }) .addCase(activatePlan.fulfilled, (state, action) => { state.loading = false; - state.currentId = action.payload.id; - // 保存到本地存储 - AsyncStorage.setItem(`${STORAGE_KEY_LIST}__currentId`, action.payload.id); }) .addCase(activatePlan.rejected, (state, action) => { state.loading = false; @@ -409,7 +325,6 @@ const trainingPlanSlice = createSlice({ .addCase(deletePlan.fulfilled, (state, action) => { state.loading = false; state.plans = action.payload.plans; - state.currentId = action.payload.currentId; }) .addCase(deletePlan.rejected, (state, action) => { state.loading = false; @@ -419,7 +334,6 @@ const trainingPlanSlice = createSlice({ }); export const { - setCurrentPlan, setMode, toggleDayOfWeek, setSessionsPerWeek,