Files
digital-pilates/app/goals-list.tsx
richarjiang 0a8b20f0ec feat: 增强目标管理功能及相关组件
- 在 GoalsListScreen 中新增目标编辑功能,支持用户编辑现有目标
- 更新 CreateGoalModal 组件,支持编辑模式下的目标更新
- 在 NutritionRecordsScreen 中新增删除营养记录功能,允许用户删除不需要的记录
- 更新 NutritionRecordCard 组件,增加操作选项,支持删除记录
- 修改 dietRecords 服务,添加删除营养记录的 API 调用
- 优化 goalsSlice,确保目标更新逻辑与 Redux 状态管理一致
2025-08-26 22:34:03 +08:00

472 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { GoalCard } from '@/components/GoalCard';
import { CreateGoalModal } from '@/components/model/CreateGoalModal';
import { useGlobalDialog } from '@/components/ui/DialogProvider';
import { Colors } from '@/constants/Colors';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { deleteGoal, fetchGoals, loadMoreGoals, updateGoal } from '@/store/goalsSlice';
import { CreateGoalRequest, GoalListItem, UpdateGoalRequest } from '@/types/goals';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useFocusEffect } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Alert, FlatList, RefreshControl, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function GoalsListScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const dispatch = useAppDispatch();
const router = useRouter();
const { showConfirm } = useGlobalDialog();
// Redux状态
const {
goals,
goalsLoading,
goalsError,
goalsPagination,
updateLoading,
updateError,
} = useAppSelector((state) => state.goals);
const [refreshing, setRefreshing] = useState(false);
// 编辑目标相关状态
const [showEditModal, setShowEditModal] = useState(false);
const [editingGoal, setEditingGoal] = useState<GoalListItem | null>(null);
// 页面聚焦时重新加载数据
useFocusEffect(
useCallback(() => {
console.log('useFocusEffect - loading goals');
loadGoals();
}, [dispatch])
);
// 加载目标列表
const loadGoals = async () => {
try {
await dispatch(fetchGoals({
page: 1,
pageSize: 20,
sortBy: 'createdAt',
sortOrder: 'desc',
})).unwrap();
} catch (error) {
console.error('Failed to load goals:', error);
// 在开发模式下如果API调用失败使用模拟数据
if (__DEV__) {
console.log('Using mock data for development');
// 添加模拟数据用于测试左滑删除功能
const mockGoals: GoalListItem[] = [
{
id: 'mock-1',
userId: 'test-user-1',
title: '每日运动30分钟',
repeatType: 'daily',
frequency: 1,
status: 'active',
completedCount: 5,
targetCount: 30,
hasReminder: true,
reminderTime: '09:00',
category: '运动',
priority: 5,
startDate: '2024-01-01',
startTime: 900,
endTime: 1800,
progressPercentage: 17,
},
{
id: 'mock-2',
userId: 'test-user-1',
title: '每天喝8杯水',
repeatType: 'daily',
frequency: 8,
status: 'active',
completedCount: 6,
targetCount: 8,
hasReminder: true,
reminderTime: '10:00',
category: '健康',
priority: 8,
startDate: '2024-01-01',
startTime: 600,
endTime: 2200,
progressPercentage: 75,
},
{
id: 'mock-3',
userId: 'test-user-1',
title: '每周读书2小时',
repeatType: 'weekly',
frequency: 2,
status: 'paused',
completedCount: 1,
targetCount: 2,
hasReminder: false,
category: '学习',
priority: 3,
startDate: '2024-01-01',
startTime: 800,
endTime: 2000,
progressPercentage: 50,
},
];
// 直接更新 Redux 状态(仅用于开发测试)
dispatch({
type: 'goals/fetchGoals/fulfilled',
payload: {
query: { page: 1, pageSize: 20, sortBy: 'createdAt', sortOrder: 'desc' },
response: {
list: mockGoals,
page: 1,
pageSize: 20,
total: mockGoals.length,
}
}
});
}
}
};
// 下拉刷新
const onRefresh = async () => {
setRefreshing(true);
try {
await loadGoals();
} finally {
setRefreshing(false);
}
};
// 加载更多目标
const handleLoadMoreGoals = async () => {
if (goalsPagination.hasMore && !goalsLoading) {
try {
await dispatch(loadMoreGoals()).unwrap();
} catch (error) {
console.error('Failed to load more goals:', error);
}
}
};
// 处理删除目标
const handleDeleteGoal = async (goalId: string) => {
try {
await dispatch(deleteGoal(goalId)).unwrap();
// 删除成功Redux 会自动更新状态
} catch (error) {
console.error('Failed to delete goal:', error);
Alert.alert('错误', '删除目标失败,请重试');
}
};
// 处理错误提示
useEffect(() => {
if (goalsError) {
Alert.alert('错误', goalsError);
}
if (updateError) {
Alert.alert('更新失败', updateError);
}
}, [goalsError, updateError]);
// 根据筛选条件过滤目标
const filteredGoals = useMemo(() => {
return goals;
}, [goals]);
// 处理目标点击
const handleGoalPress = (goal: GoalListItem) => {
setEditingGoal(goal);
setShowEditModal(true);
};
// 将 GoalListItem 转换为 CreateGoalRequest 格式
const convertGoalToModalData = (goal: GoalListItem): Partial<CreateGoalRequest> => {
return {
title: goal.title,
description: goal.description,
repeatType: goal.repeatType,
frequency: goal.frequency,
category: goal.category,
priority: goal.priority,
hasReminder: goal.hasReminder,
reminderTime: goal.reminderTime,
customRepeatRule: goal.customRepeatRule,
endDate: goal.endDate,
};
};
// 处理更新目标
const handleUpdateGoal = async (goalId: string, goalData: UpdateGoalRequest) => {
try {
await dispatch(updateGoal({ goalId, goalData })).unwrap();
setShowEditModal(false);
setEditingGoal(null);
// 使用全局弹窗显示成功消息
showConfirm(
{
title: '目标更新成功',
message: '恭喜!您的目标已成功更新。',
confirmText: '确定',
cancelText: '',
icon: 'checkmark-circle',
iconColor: '#10B981',
},
() => {
console.log('用户确认了目标更新成功');
}
);
} catch (error) {
console.error('Failed to update goal:', error);
Alert.alert('错误', '更新目标失败,请重试');
// 更新失败时不关闭弹窗,保持编辑状态
}
};
// 处理编辑弹窗关闭
const handleCloseEditModal = () => {
setShowEditModal(false);
setEditingGoal(null);
};
// 渲染目标项
const renderGoalItem = ({ item }: { item: GoalListItem }) => (
<GoalCard
goal={item}
onPress={handleGoalPress}
onDelete={handleDeleteGoal}
showStatus={false}
/>
);
// 渲染空状态
const renderEmptyState = () => {
let title = '暂无目标';
let subtitle = '创建您的第一个目标,开始您的健康之旅';
return (
<View style={styles.emptyState}>
<MaterialIcons name="flag" size={64} color="#D1D5DB" />
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
{title}
</Text>
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
{subtitle}
</Text>
<TouchableOpacity
style={[styles.createButton, { backgroundColor: colorTokens.primary }]}
onPress={() => router.push('/(tabs)/goals')}
>
<Text style={styles.createButtonText}></Text>
</TouchableOpacity>
</View>
);
};
// 渲染加载更多
const renderLoadMore = () => {
if (!goalsPagination.hasMore) return null;
return (
<View style={styles.loadMoreContainer}>
<Text style={[styles.loadMoreText, { color: colorTokens.textSecondary }]}>
{goalsLoading ? '加载中...' : '上拉加载更多'}
</Text>
</View>
);
};
return (
<SafeAreaView style={styles.container}>
<StatusBar
backgroundColor="transparent"
translucent
/>
{/* 背景渐变 */}
<LinearGradient
colors={['#F8FAFC', '#F1F5F9']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
<View style={styles.content}>
{/* 标题区域 */}
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<MaterialIcons name="arrow-back" size={24} color={colorTokens.text} />
</TouchableOpacity>
<Text style={[styles.pageTitle, { color: colorTokens.text }]}>
</Text>
<TouchableOpacity
style={styles.addButton}
onPress={() => router.push('/(tabs)/goals')}
>
<MaterialIcons name="add" size={24} color="#FFFFFF" />
</TouchableOpacity>
</View>
{/* 目标列表 */}
<View style={styles.goalsListContainer}>
<FlatList
data={filteredGoals}
renderItem={renderGoalItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.goalsList}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#7A5AF8']}
tintColor="#7A5AF8"
/>
}
onEndReached={handleLoadMoreGoals}
onEndReachedThreshold={0.1}
ListEmptyComponent={renderEmptyState}
ListFooterComponent={renderLoadMore}
/>
</View>
{/* 编辑目标弹窗 */}
{editingGoal && (
<CreateGoalModal
visible={showEditModal}
onClose={handleCloseEditModal}
onSubmit={() => {}} // 编辑模式下不使用这个回调
onUpdate={handleUpdateGoal}
loading={updateLoading}
initialData={convertGoalToModalData(editingGoal)}
editGoalId={editingGoal.id}
/>
)}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
content: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 16,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
pageTitle: {
fontSize: 24,
fontWeight: '700',
flex: 1,
textAlign: 'center',
},
addButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#7A5AF8',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
goalsListContainer: {
flex: 1,
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: 'hidden',
},
goalsList: {
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
},
emptyState: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 80,
},
emptyStateTitle: {
fontSize: 18,
fontWeight: '600',
marginTop: 16,
marginBottom: 8,
},
emptyStateSubtitle: {
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
marginBottom: 24,
paddingHorizontal: 40,
},
createButton: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 20,
},
createButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
loadMoreContainer: {
alignItems: 'center',
paddingVertical: 20,
},
loadMoreText: {
fontSize: 14,
fontWeight: '500',
},
});