- 将目标创建成功后的提示从系统默认的 Alert.alert 改为使用自定义确认弹窗,提升用户体验和视觉一致性 - 在任务筛选中新增“已跳过”选项,支持用户更好地管理任务状态 - 更新任务卡片和进度卡片,展示跳过任务的数量和状态 - 调整相关组件样式,确保界面一致性和美观性 - 编写相关文档,详细描述新功能和使用方法
522 lines
14 KiB
TypeScript
522 lines
14 KiB
TypeScript
import CreateGoalModal from '@/components/CreateGoalModal';
|
||
import { TaskCard } from '@/components/TaskCard';
|
||
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
|
||
import { TaskProgressCard } from '@/components/TaskProgressCard';
|
||
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 { clearErrors, createGoal } from '@/store/goalsSlice';
|
||
import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice';
|
||
import { CreateGoalRequest, TaskListItem } from '@/types/goals';
|
||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||
import { useFocusEffect } from '@react-navigation/native';
|
||
import dayjs from 'dayjs';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { useRouter } from 'expo-router';
|
||
import React, { useCallback, useEffect, useState } from 'react';
|
||
import { Alert, FlatList, Image, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
|
||
export default function GoalsScreen() {
|
||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||
const colorTokens = Colors[theme];
|
||
const dispatch = useAppDispatch();
|
||
const router = useRouter();
|
||
const { showConfirm } = useGlobalDialog();
|
||
|
||
// Redux状态
|
||
const {
|
||
tasks,
|
||
tasksLoading,
|
||
tasksError,
|
||
tasksPagination,
|
||
completeLoading,
|
||
completeError,
|
||
skipLoading,
|
||
skipError,
|
||
} = useAppSelector((state) => state.tasks);
|
||
|
||
const {
|
||
createLoading,
|
||
createError
|
||
} = useAppSelector((state) => state.goals);
|
||
|
||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
|
||
const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗
|
||
|
||
// 页面聚焦时重新加载数据
|
||
useFocusEffect(
|
||
useCallback(() => {
|
||
console.log('useFocusEffect - loading tasks');
|
||
loadTasks();
|
||
}, [dispatch])
|
||
);
|
||
|
||
// 加载任务列表
|
||
const loadTasks = async () => {
|
||
try {
|
||
await dispatch(fetchTasks({
|
||
startDate: dayjs().startOf('day').toISOString(),
|
||
endDate: dayjs().endOf('day').toISOString(),
|
||
})).unwrap();
|
||
} catch (error) {
|
||
console.error('Failed to load tasks:', error);
|
||
}
|
||
};
|
||
|
||
// 下拉刷新
|
||
const onRefresh = async () => {
|
||
setRefreshing(true);
|
||
try {
|
||
await loadTasks();
|
||
} finally {
|
||
setRefreshing(false);
|
||
}
|
||
};
|
||
|
||
// 加载更多任务
|
||
const handleLoadMoreTasks = async () => {
|
||
if (tasksPagination.hasMore && !tasksLoading) {
|
||
try {
|
||
await dispatch(loadMoreTasks()).unwrap();
|
||
} catch (error) {
|
||
console.error('Failed to load more tasks:', error);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理错误提示
|
||
useEffect(() => {
|
||
console.log('tasksError', tasksError);
|
||
console.log('createError', createError);
|
||
console.log('completeError', completeError);
|
||
console.log('skipError', skipError);
|
||
|
||
if (tasksError) {
|
||
Alert.alert('错误', tasksError);
|
||
dispatch(clearTaskErrors());
|
||
}
|
||
if (createError) {
|
||
Alert.alert('创建失败', createError);
|
||
dispatch(clearErrors());
|
||
}
|
||
if (completeError) {
|
||
Alert.alert('完成失败', completeError);
|
||
dispatch(clearTaskErrors());
|
||
}
|
||
if (skipError) {
|
||
Alert.alert('跳过失败', skipError);
|
||
dispatch(clearTaskErrors());
|
||
}
|
||
}, [tasksError, createError, completeError, skipError, dispatch]);
|
||
|
||
// 重置弹窗表单数据
|
||
const handleModalSuccess = () => {
|
||
// 不需要在这里改变 modalKey,因为弹窗已经关闭了
|
||
// 下次打开时会自动使用新的 modalKey
|
||
};
|
||
|
||
// 创建目标处理函数
|
||
const handleCreateGoal = async (goalData: CreateGoalRequest) => {
|
||
try {
|
||
await dispatch(createGoal(goalData)).unwrap();
|
||
setShowCreateModal(false);
|
||
|
||
// 使用确认弹窗显示成功消息
|
||
showConfirm(
|
||
{
|
||
title: '目标创建成功',
|
||
message: '恭喜!您的目标已成功创建。系统将自动生成相应的任务,帮助您实现目标。',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
icon: 'checkmark-circle',
|
||
iconColor: '#10B981',
|
||
},
|
||
() => {
|
||
// 用户点击确定后的回调
|
||
console.log('用户确认了目标创建成功');
|
||
}
|
||
);
|
||
|
||
// 创建目标后重新加载任务列表
|
||
loadTasks();
|
||
} catch (error) {
|
||
// 错误已在useEffect中处理
|
||
}
|
||
};
|
||
|
||
|
||
|
||
// 导航到目标管理页面
|
||
const handleNavigateToGoals = () => {
|
||
router.push('/goals-detail');
|
||
};
|
||
|
||
// 计算各状态的任务数量
|
||
const taskCounts = {
|
||
all: tasks.length,
|
||
pending: tasks.filter(task => task.status === 'pending').length,
|
||
completed: tasks.filter(task => task.status === 'completed').length,
|
||
skipped: tasks.filter(task => task.status === 'skipped').length,
|
||
};
|
||
|
||
// 根据筛选条件过滤任务
|
||
const filteredTasks = React.useMemo(() => {
|
||
switch (selectedFilter) {
|
||
case 'pending':
|
||
return tasks.filter(task => task.status === 'pending');
|
||
case 'completed':
|
||
return tasks.filter(task => task.status === 'completed');
|
||
case 'skipped':
|
||
return tasks.filter(task => task.status === 'skipped');
|
||
default:
|
||
return tasks;
|
||
}
|
||
}, [tasks, selectedFilter]);
|
||
|
||
// 处理筛选变化
|
||
const handleFilterChange = (filter: TaskFilterType) => {
|
||
setSelectedFilter(filter);
|
||
};
|
||
|
||
// 渲染任务项
|
||
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
||
<TaskCard
|
||
task={item}
|
||
/>
|
||
);
|
||
|
||
// 渲染空状态
|
||
const renderEmptyState = () => {
|
||
let title = '暂无任务';
|
||
let subtitle = '创建目标后,系统会自动生成相应的任务';
|
||
|
||
if (selectedFilter === 'pending') {
|
||
title = '暂无待完成的任务';
|
||
subtitle = '当前没有待完成的任务';
|
||
} else if (selectedFilter === 'completed') {
|
||
title = '暂无已完成的任务';
|
||
subtitle = '完成一些任务后,它们会显示在这里';
|
||
} else if (selectedFilter === 'skipped') {
|
||
title = '暂无已跳过的任务';
|
||
subtitle = '跳过一些任务后,它们会显示在这里';
|
||
}
|
||
|
||
return (
|
||
<View style={styles.emptyState}>
|
||
<Image
|
||
source={require('@/assets/images/task/ImageEmpty.png')}
|
||
style={styles.emptyStateImage}
|
||
resizeMode="contain"
|
||
/>
|
||
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
|
||
{title}
|
||
</Text>
|
||
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
|
||
{subtitle}
|
||
</Text>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// 渲染加载更多
|
||
const renderLoadMore = () => {
|
||
if (!tasksPagination.hasMore) return null;
|
||
return (
|
||
<View style={styles.loadMoreContainer}>
|
||
<Text style={[styles.loadMoreText, { color: colorTokens.textSecondary }]}>
|
||
{tasksLoading ? '加载中...' : '上拉加载更多'}
|
||
</Text>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<StatusBar
|
||
backgroundColor="transparent"
|
||
translucent
|
||
/>
|
||
|
||
{/* 背景渐变 */}
|
||
<LinearGradient
|
||
colors={['#F0F9FF', '#E0F2FE']}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
/>
|
||
|
||
<View style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
backgroundColor: '#7A5AF8',
|
||
height: 233,
|
||
borderBottomLeftRadius: 24,
|
||
borderBottomRightRadius: 24,
|
||
}}>
|
||
{/* 右下角图片 */}
|
||
<Image
|
||
source={require('@/assets/images/task/imageTodo.png')}
|
||
style={styles.bottomRightImage}
|
||
resizeMode="contain"
|
||
/>
|
||
</View>
|
||
|
||
<View style={styles.content}>
|
||
{/* 标题区域 */}
|
||
<View style={styles.header}>
|
||
<View>
|
||
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
|
||
等待完成
|
||
</Text>
|
||
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
|
||
让我们检查你的目标!
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 任务进度卡片 */}
|
||
<View >
|
||
<TaskProgressCard
|
||
tasks={tasks}
|
||
headerButtons={
|
||
<View style={styles.cardHeaderButtons}>
|
||
<TouchableOpacity
|
||
style={[styles.cardGoalsButton, { borderColor: colorTokens.primary }]}
|
||
onPress={handleNavigateToGoals}
|
||
>
|
||
<MaterialIcons name="flag" size={16} color={colorTokens.primary} />
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[styles.cardAddButton, { backgroundColor: colorTokens.primary }]}
|
||
onPress={() => {
|
||
setModalKey(prev => prev + 1); // 每次打开弹窗时使用新的 key
|
||
setShowCreateModal(true);
|
||
}}
|
||
>
|
||
<Text style={styles.cardAddButtonText}>+</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
}
|
||
/>
|
||
</View>
|
||
|
||
{/* 任务筛选标签 */}
|
||
<TaskFilterTabs
|
||
selectedFilter={selectedFilter}
|
||
onFilterChange={handleFilterChange}
|
||
taskCounts={taskCounts}
|
||
/>
|
||
|
||
{/* 任务列表 */}
|
||
<View style={styles.taskListContainer}>
|
||
<FlatList
|
||
data={filteredTasks}
|
||
renderItem={renderTaskItem}
|
||
keyExtractor={(item) => item.id}
|
||
contentContainerStyle={styles.taskList}
|
||
showsVerticalScrollIndicator={false}
|
||
refreshControl={
|
||
<RefreshControl
|
||
refreshing={refreshing}
|
||
onRefresh={onRefresh}
|
||
colors={['#0EA5E9']}
|
||
tintColor="#0EA5E9"
|
||
/>
|
||
}
|
||
onEndReached={handleLoadMoreTasks}
|
||
onEndReachedThreshold={0.1}
|
||
ListEmptyComponent={renderEmptyState}
|
||
ListFooterComponent={renderLoadMore}
|
||
/>
|
||
</View>
|
||
|
||
{/* 创建目标弹窗 */}
|
||
<CreateGoalModal
|
||
key={modalKey}
|
||
visible={showCreateModal}
|
||
onClose={() => setShowCreateModal(false)}
|
||
onSubmit={handleCreateGoal}
|
||
onSuccess={handleModalSuccess}
|
||
loading={createLoading}
|
||
/>
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
opacity: 0.6,
|
||
},
|
||
decorativeCircle1: {
|
||
position: 'absolute',
|
||
top: -20,
|
||
right: -20,
|
||
width: 60,
|
||
height: 60,
|
||
borderRadius: 30,
|
||
backgroundColor: '#0EA5E9',
|
||
opacity: 0.1,
|
||
},
|
||
decorativeCircle2: {
|
||
position: 'absolute',
|
||
bottom: -15,
|
||
left: -15,
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
backgroundColor: '#0EA5E9',
|
||
opacity: 0.05,
|
||
},
|
||
content: {
|
||
flex: 1,
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 20,
|
||
paddingTop: 20,
|
||
paddingBottom: 16,
|
||
},
|
||
headerButtons: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
},
|
||
goalsButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
borderRadius: 20,
|
||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 4,
|
||
elevation: 3,
|
||
},
|
||
pageTitle: {
|
||
fontSize: 28,
|
||
fontWeight: '800',
|
||
marginBottom: 4,
|
||
},
|
||
pageTitle2: {
|
||
fontSize: 16,
|
||
fontWeight: '400',
|
||
color: '#FFFFFF',
|
||
lineHeight: 24,
|
||
},
|
||
addButton: {
|
||
width: 30,
|
||
height: 30,
|
||
borderRadius: 20,
|
||
backgroundColor: '#0EA5E9',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 4,
|
||
elevation: 3,
|
||
},
|
||
addButtonText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 22,
|
||
fontWeight: '600',
|
||
lineHeight: 22,
|
||
},
|
||
|
||
taskListContainer: {
|
||
flex: 1,
|
||
borderTopLeftRadius: 24,
|
||
borderTopRightRadius: 24,
|
||
overflow: 'hidden',
|
||
},
|
||
taskList: {
|
||
paddingHorizontal: 20,
|
||
paddingTop: 20,
|
||
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20, // 避让底部导航栏 + 额外间距
|
||
},
|
||
emptyState: {
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: 60,
|
||
},
|
||
emptyStateImage: {
|
||
width: 223,
|
||
height: 59,
|
||
marginBottom: 20,
|
||
},
|
||
emptyStateTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
marginBottom: 8,
|
||
},
|
||
emptyStateSubtitle: {
|
||
fontSize: 14,
|
||
textAlign: 'center',
|
||
lineHeight: 20,
|
||
},
|
||
loadMoreContainer: {
|
||
alignItems: 'center',
|
||
paddingVertical: 20,
|
||
},
|
||
loadMoreText: {
|
||
fontSize: 14,
|
||
fontWeight: '500',
|
||
},
|
||
bottomRightImage: {
|
||
position: 'absolute',
|
||
top: 56,
|
||
right: 36,
|
||
width: 80,
|
||
height: 80,
|
||
},
|
||
// 任务进度卡片中的按钮样式
|
||
cardHeaderButtons: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
},
|
||
cardGoalsButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
borderRadius: 16,
|
||
backgroundColor: '#F3F4F6',
|
||
borderWidth: 1,
|
||
},
|
||
cardAddButton: {
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
cardAddButtonText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 18,
|
||
fontWeight: '600',
|
||
lineHeight: 18,
|
||
},
|
||
});
|