feat: 新增任务管理功能及相关组件

- 将目标页面改为任务列表,支持任务的创建、完成和跳过功能
- 新增任务卡片和任务进度卡片组件,展示任务状态和进度
- 实现任务数据的获取和管理,集成Redux状态管理
- 更新API服务,支持任务相关的CRUD操作
- 编写任务管理功能实现文档,详细描述功能和组件架构
This commit is contained in:
richarjiang
2025-08-22 17:30:14 +08:00
parent 231620d778
commit 259f10540e
21 changed files with 2756 additions and 608 deletions

260
components/TaskCard.tsx Normal file
View File

@@ -0,0 +1,260 @@
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { TaskListItem } from '@/types/goals';
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface TaskCardProps {
task: TaskListItem;
onPress?: (task: TaskListItem) => void;
onComplete?: (task: TaskListItem) => void;
onSkip?: (task: TaskListItem) => void;
}
export const TaskCard: React.FC<TaskCardProps> = ({
task,
onPress,
onComplete,
onSkip,
}) => {
const theme = useColorScheme() ?? 'light';
const colorTokens = Colors[theme];
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return '#10B981';
case 'in_progress':
return '#F59E0B';
case 'overdue':
return '#EF4444';
case 'skipped':
return '#6B7280';
default:
return '#3B82F6';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'completed':
return '已完成';
case 'in_progress':
return '进行中';
case 'overdue':
return '已过期';
case 'skipped':
return '已跳过';
default:
return '待开始';
}
};
const getCategoryColor = (category?: string) => {
if (!category) return '#6B7280';
if (category.includes('运动') || category.includes('健身')) return '#EF4444';
if (category.includes('工作')) return '#3B82F6';
if (category.includes('健康')) return '#10B981';
if (category.includes('财务')) return '#F59E0B';
return '#6B7280';
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (date.toDateString() === today.toDateString()) {
return '今天';
} else if (date.toDateString() === tomorrow.toDateString()) {
return '明天';
} else {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
};
return (
<TouchableOpacity
style={[styles.container, { backgroundColor: colorTokens.background }]}
onPress={() => onPress?.(task)}
activeOpacity={0.7}
>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
{task.title}
</Text>
{task.goal?.category && (
<View style={[styles.categoryTag, { backgroundColor: getCategoryColor(task.goal.category) }]}>
<Text style={styles.categoryText}>{task.goal?.category}</Text>
</View>
)}
</View>
<View style={[styles.statusTag, { backgroundColor: getStatusColor(task.status) }]}>
<Text style={styles.statusText}>{getStatusText(task.status)}</Text>
</View>
</View>
{task.description && (
<Text style={[styles.description, { color: colorTokens.textSecondary }]} numberOfLines={2}>
{task.description}
</Text>
)}
<View style={styles.progressContainer}>
<View style={styles.progressInfo}>
<Text style={[styles.progressText, { color: colorTokens.textSecondary }]}>
: {task.currentCount}/{task.targetCount}
</Text>
<Text style={[styles.progressText, { color: colorTokens.textSecondary }]}>
{task.progressPercentage}%
</Text>
</View>
<View style={[styles.progressBar, { backgroundColor: colorTokens.border }]}>
<View
style={[
styles.progressFill,
{
width: `${task.progressPercentage}%`,
backgroundColor: getStatusColor(task.status),
},
]}
/>
</View>
</View>
<View style={styles.footer}>
<Text style={[styles.dateText, { color: colorTokens.textSecondary }]}>
{formatDate(task.startDate)}
</Text>
{task.status === 'pending' || task.status === 'in_progress' ? (
<View style={styles.actionButtons}>
<TouchableOpacity
style={[styles.actionButton, styles.skipButton]}
onPress={() => onSkip?.(task)}
>
<Text style={styles.skipButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.completeButton]}
onPress={() => onComplete?.(task)}
>
<Text style={styles.completeButtonText}></Text>
</TouchableOpacity>
</View>
) : null}
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
},
titleContainer: {
flex: 1,
marginRight: 8,
},
title: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
categoryTag: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 12,
alignSelf: 'flex-start',
},
categoryText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '500',
},
statusTag: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '500',
},
description: {
fontSize: 14,
marginBottom: 12,
lineHeight: 20,
},
progressContainer: {
marginBottom: 12,
},
progressInfo: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 6,
},
progressText: {
fontSize: 12,
fontWeight: '500',
},
progressBar: {
height: 6,
borderRadius: 3,
overflow: 'hidden',
},
progressFill: {
height: '100%',
borderRadius: 3,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
dateText: {
fontSize: 12,
fontWeight: '500',
},
actionButtons: {
flexDirection: 'row',
gap: 8,
},
actionButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
skipButton: {
backgroundColor: '#F3F4F6',
},
skipButtonText: {
color: '#6B7280',
fontSize: 12,
fontWeight: '500',
},
completeButton: {
backgroundColor: '#10B981',
},
completeButtonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '500',
},
});