- 将目标页面改为任务列表,支持任务的创建、完成和跳过功能 - 新增任务卡片和任务进度卡片组件,展示任务状态和进度 - 实现任务数据的获取和管理,集成Redux状态管理 - 更新API服务,支持任务相关的CRUD操作 - 编写任务管理功能实现文档,详细描述功能和组件架构
261 lines
6.6 KiB
TypeScript
261 lines
6.6 KiB
TypeScript
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',
|
|
},
|
|
});
|