feat: 增强任务管理功能,新增任务筛选和状态统计组件
- 在目标页面中集成任务筛选标签,支持按状态(全部、待完成、已完成)过滤任务 - 更新任务卡片,展示任务状态和优先级信息 - 新增任务进度卡片,统计各状态任务数量 - 优化空状态展示,根据筛选条件动态显示提示信息 - 引入新图标和图片资源,提升界面视觉效果
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { TaskListItem } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: TaskListItem;
|
||||
@@ -50,28 +51,39 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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 getPriorityColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'overdue':
|
||||
return '#EF4444'; // High - 过期任务
|
||||
case 'in_progress':
|
||||
return '#F59E0B'; // Medium - 进行中
|
||||
case 'completed':
|
||||
return '#10B981'; // Low - 已完成
|
||||
default:
|
||||
return '#6B7280'; // Default - 待开始
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'overdue':
|
||||
return '高';
|
||||
case 'in_progress':
|
||||
return '中';
|
||||
case 'completed':
|
||||
return '低';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
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' });
|
||||
}
|
||||
const month = date.toLocaleDateString('zh-CN', { month: 'short' });
|
||||
const day = date.getDate();
|
||||
return `${day} ${month}`;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -80,71 +92,74 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
onPress={() => onPress?.(task)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* 头部区域 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<View style={styles.titleSection}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Image
|
||||
source={require('@/assets/images/task/iconTaskHeader.png')}
|
||||
style={styles.taskIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<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 style={styles.tagsContainer}>
|
||||
<View style={styles.statusTag}>
|
||||
<MaterialIcons name="schedule" size={12} color="#6B7280" />
|
||||
<Text style={styles.statusTagText}>{getStatusText(task.status)}</Text>
|
||||
</View>
|
||||
<View style={[styles.progressBar, { backgroundColor: colorTokens.border }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressFill,
|
||||
{
|
||||
width: `${task.progressPercentage}%`,
|
||||
backgroundColor: getStatusColor(task.status),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View style={[styles.priorityTag, { backgroundColor: getPriorityColor(task.status) }]}>
|
||||
<MaterialIcons name="flag" size={12} color="#FFFFFF" />
|
||||
<Text style={styles.priorityTagText}>{getPriorityText(task.status)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 进度条 */}
|
||||
<View style={styles.progressBar}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressFill,
|
||||
{
|
||||
width: `${Math.min(task.progressPercentage, 100)}%`,
|
||||
backgroundColor: colorTokens.primary,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</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 style={styles.teamSection}>
|
||||
{/* 模拟团队成员头像 */}
|
||||
<View style={styles.avatars}>
|
||||
<View style={[styles.avatar, { backgroundColor: '#FBBF24' }]}>
|
||||
<Text style={styles.avatarText}>A</Text>
|
||||
</View>
|
||||
<View style={[styles.avatar, { backgroundColor: '#34D399' }]}>
|
||||
<Text style={styles.avatarText}>B</Text>
|
||||
</View>
|
||||
<View style={[styles.avatar, { backgroundColor: '#60A5FA' }]}>
|
||||
<Text style={styles.avatarText}>C</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.infoSection}>
|
||||
<View style={styles.infoTag}>
|
||||
<MaterialIcons name="event" size={12} color="#6B7280" />
|
||||
<Text style={styles.infoTagText}>{formatDate(task.startDate)}</Text>
|
||||
</View>
|
||||
<View style={styles.infoTag}>
|
||||
<MaterialIcons name="chat-bubble-outline" size={12} color="#6B7280" />
|
||||
<Text style={styles.infoTagText}>2</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -162,99 +177,119 @@ const styles = StyleSheet.create({
|
||||
elevation: 3,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
marginRight: 8,
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#7A5AF8',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
},
|
||||
taskIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
lineHeight: 22,
|
||||
flex: 1,
|
||||
},
|
||||
categoryTag: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
categoryText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
tagsContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
statusTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F3F4F6',
|
||||
},
|
||||
statusTagText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
},
|
||||
priorityTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
statusText: {
|
||||
priorityTagText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
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,
|
||||
height: 2,
|
||||
backgroundColor: '#E5E7EB',
|
||||
borderRadius: 1,
|
||||
marginBottom: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
borderRadius: 3,
|
||||
borderRadius: 1,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
teamSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
actionButtons: {
|
||||
avatars: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatar: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: -8,
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
avatarText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
infoSection: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
actionButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
},
|
||||
skipButton: {
|
||||
infoTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F3F4F6',
|
||||
},
|
||||
skipButtonText: {
|
||||
color: '#6B7280',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
completeButton: {
|
||||
backgroundColor: '#10B981',
|
||||
},
|
||||
completeButtonText: {
|
||||
color: '#FFFFFF',
|
||||
infoTagText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user