diff --git a/app/(tabs)/goals.tsx b/app/(tabs)/goals.tsx index c5ec085..d607974 100644 --- a/app/(tabs)/goals.tsx +++ b/app/(tabs)/goals.tsx @@ -1,5 +1,6 @@ import CreateGoalModal from '@/components/CreateGoalModal'; import { TaskCard } from '@/components/TaskCard'; +import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs'; import { TaskProgressCard } from '@/components/TaskProgressCard'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; @@ -13,7 +14,7 @@ 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, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +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'; @@ -40,6 +41,7 @@ export default function GoalsScreen() { const [showCreateModal, setShowCreateModal] = useState(false); const [refreshing, setRefreshing] = useState(false); + const [selectedFilter, setSelectedFilter] = useState('all'); // 页面聚焦时重新加载数据 useFocusEffect( @@ -160,6 +162,30 @@ export default function GoalsScreen() { 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, + }; + + // 根据筛选条件过滤任务 + const filteredTasks = React.useMemo(() => { + switch (selectedFilter) { + case 'pending': + return tasks.filter(task => task.status === 'pending'); + case 'completed': + return tasks.filter(task => task.status === 'completed'); + default: + return tasks; + } + }, [tasks, selectedFilter]); + + // 处理筛选变化 + const handleFilterChange = (filter: TaskFilterType) => { + setSelectedFilter(filter); + }; + // 渲染任务项 const renderTaskItem = ({ item }: { item: TaskListItem }) => ( ( - - - 暂无任务 - - - 创建目标后,系统会自动生成相应的任务 - - - ); + const renderEmptyState = () => { + let title = '暂无任务'; + let subtitle = '创建目标后,系统会自动生成相应的任务'; + + if (selectedFilter === 'pending') { + title = '暂无待完成的任务'; + subtitle = '当前没有待完成的任务'; + } else if (selectedFilter === 'completed') { + title = '暂无已完成的任务'; + subtitle = '完成一些任务后,它们会显示在这里'; + } + + return ( + + + + {title} + + + {subtitle} + + + ); + }; // 渲染加载更多 const renderLoadMore = () => { @@ -209,39 +253,71 @@ export default function GoalsScreen() { end={{ x: 1, y: 1 }} /> - {/* 装饰性圆圈 */} - - + + {/* 右下角图片 */} + + {/* 标题区域 */} - - 任务 + + + 等待完成 + + + 让我们检查你的目标! - - - - - setShowCreateModal(true)} - > - + - {/* 任务进度卡片 */} - + + + + + + setShowCreateModal(true)} + > + + + + + } + /> + + + {/* 任务筛选标签 */} + {/* 任务列表 */} item.id} contentContainerStyle={styles.taskList} @@ -340,6 +416,12 @@ const styles = StyleSheet.create({ fontWeight: '800', marginBottom: 4, }, + pageTitle2: { + fontSize: 16, + fontWeight: '400', + color: '#FFFFFF', + lineHeight: 24, + }, addButton: { width: 30, height: 30, @@ -362,7 +444,6 @@ const styles = StyleSheet.create({ taskListContainer: { flex: 1, - backgroundColor: 'rgba(255, 255, 255, 0.95)', borderTopLeftRadius: 24, borderTopRightRadius: 24, overflow: 'hidden', @@ -377,6 +458,11 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingVertical: 60, }, + emptyStateImage: { + width: 223, + height: 59, + marginBottom: 20, + }, emptyStateTitle: { fontSize: 18, fontWeight: '600', @@ -395,4 +481,40 @@ const styles = StyleSheet.create({ 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, + }, }); diff --git a/assets/images/task/ImageEmpty.png b/assets/images/task/ImageEmpty.png new file mode 100644 index 0000000..449bb85 Binary files /dev/null and b/assets/images/task/ImageEmpty.png differ diff --git a/assets/images/task/iconTaskHeader.png b/assets/images/task/iconTaskHeader.png new file mode 100644 index 0000000..b43e318 Binary files /dev/null and b/assets/images/task/iconTaskHeader.png differ diff --git a/assets/images/task/imageTodo.png b/assets/images/task/imageTodo.png new file mode 100644 index 0000000..b21a042 Binary files /dev/null and b/assets/images/task/imageTodo.png differ diff --git a/components/TaskCard.tsx b/components/TaskCard.tsx index 070cae7..6fb6b5f 100644 --- a/components/TaskCard.tsx +++ b/components/TaskCard.tsx @@ -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 = ({ } }; - 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 = ({ onPress={() => onPress?.(task)} activeOpacity={0.7} > + {/* 头部区域 */} - + + + + {task.title} - {task.goal?.category && ( - - {task.goal?.category} - - )} - - - {getStatusText(task.status)} - {task.description && ( - - {task.description} - - )} - - - - - 进度: {task.currentCount}/{task.targetCount} - - - {task.progressPercentage}% - + {/* 状态和优先级标签 */} + + + + {getStatusText(task.status)} - - + + + {getPriorityText(task.status)} + {/* 进度条 */} + + + + + {/* 底部信息 */} - - {formatDate(task.startDate)} - - - {task.status === 'pending' || task.status === 'in_progress' ? ( - - onSkip?.(task)} - > - 跳过 - - onComplete?.(task)} - > - 完成 - + + {/* 模拟团队成员头像 */} + + + A + + + B + + + C + - ) : null} + + + + + + {formatDate(task.startDate)} + + + + 2 + + ); @@ -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', }, }); diff --git a/components/TaskFilterTabs.tsx b/components/TaskFilterTabs.tsx new file mode 100644 index 0000000..1cb278f --- /dev/null +++ b/components/TaskFilterTabs.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +export type TaskFilterType = 'all' | 'pending' | 'completed'; + +interface TaskFilterTabsProps { + selectedFilter: TaskFilterType; + onFilterChange: (filter: TaskFilterType) => void; + taskCounts: { + all: number; + pending: number; + completed: number; + }; +} + +export const TaskFilterTabs: React.FC = ({ + selectedFilter, + onFilterChange, + taskCounts, +}) => { + return ( + + + {/* 全部 Tab */} + onFilterChange('all')} + > + + 全部 + + + + {taskCounts.all} + + + + + {/* 待完成 Tab */} + onFilterChange('pending')} + > + + 待完成 + + + + {taskCounts.pending} + + + + + {/* 已完成 Tab */} + onFilterChange('completed')} + > + + 已完成 + + + + {taskCounts.completed} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 20, + }, + tabContainer: { + backgroundColor: '#FFFFFF', + borderRadius: 24, + padding: 4, + flexDirection: 'row', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 1, + borderWidth: 1, + borderColor: '#E5E7EB', + }, + tab: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 20, + gap: 6, + }, + activeTab: { + backgroundColor: '#7A5AF8', + shadowColor: '#7A5AF8', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, + }, + tabText: { + fontSize: 14, + fontWeight: '500', + color: '#374151', + }, + activeTabText: { + color: '#FFFFFF', + fontWeight: '600', + }, + badge: { + minWidth: 20, + height: 20, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 6, + }, + inactiveBadge: { + backgroundColor: '#E5E7EB', + }, + activeBadge: { + backgroundColor: '#EF4444', + }, + badgeText: { + fontSize: 12, + fontWeight: '600', + color: '#374151', + }, + activeBadgeText: { + color: '#FFFFFF', + }, +}); diff --git a/components/TaskProgressCard.tsx b/components/TaskProgressCard.tsx index 7f81399..647369d 100644 --- a/components/TaskProgressCard.tsx +++ b/components/TaskProgressCard.tsx @@ -1,140 +1,139 @@ import { TaskListItem } from '@/types/goals'; -import React from 'react'; +import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import React, { ReactNode } from 'react'; import { StyleSheet, Text, View } from 'react-native'; interface TaskProgressCardProps { tasks: TaskListItem[]; + headerButtons?: ReactNode; } export const TaskProgressCard: React.FC = ({ tasks, + headerButtons, }) => { - // 计算今日任务完成进度 - const todayTasks = tasks.filter(task => task.isToday); - const completedTodayTasks = todayTasks.filter(task => task.status === 'completed'); - const progressPercentage = todayTasks.length > 0 - ? Math.round((completedTodayTasks.length / todayTasks.length) * 100) - : 0; - - // 计算进度角度 - const progressAngle = (progressPercentage / 100) * 360; + // 计算各状态的任务数量 + const pendingTasks = tasks.filter(task => task.status === 'pending'); + const completedTasks = tasks.filter(task => task.status === 'completed'); + const skippedTasks = tasks.filter(task => task.status === 'skipped'); return ( - {/* 左侧内容 */} - - - 今日目标 - 加油,快完成啦! + {/* 标题区域 */} + + + 任务状态统计 + 各状态任务数量分布 - + {headerButtons && ( + + {headerButtons} + + )} - {/* 右侧进度圆环 */} - - {/* 背景圆环 */} - - - {/* 进度圆环 */} - - 0 ? '#8B5CF6' : 'transparent', - borderRightColor: progressAngle > 90 ? '#8B5CF6' : 'transparent', - borderBottomColor: progressAngle > 180 ? '#8B5CF6' : 'transparent', - borderLeftColor: progressAngle > 270 ? '#8B5CF6' : 'transparent', - transform: [{ rotate: '-90deg' }], - }, - ]} - /> + {/* 状态卡片区域 */} + + {/* 待完成 卡片 */} + + + + 待完成 + + {pendingTasks.length} - {/* 进度文字 */} - - {progressPercentage}% + {/* 已完成 卡片 */} + + + + 已完成 + + {completedTasks.length} + + + {/* 已跳过 卡片 */} + + + + 已跳过 + + {skippedTasks.length} - ); }; const styles = StyleSheet.create({ container: { - backgroundColor: '#8B5CF6', + backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginHorizontal: 20, marginBottom: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + header: { + marginBottom: 20, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + titleContainer: { + flex: 1, + }, + headerButtons: { flexDirection: 'row', alignItems: 'center', - position: 'relative', - shadowColor: '#8B5CF6', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, - }, - leftContent: { - flex: 1, - marginRight: 20, - }, - textContainer: { - marginBottom: 16, + gap: 8, }, title: { - color: '#FFFFFF', - fontSize: 16, - fontWeight: '600', - marginBottom: 2, + fontSize: 20, + fontWeight: '700', + color: '#1F2937', + marginBottom: 4, }, subtitle: { - color: '#FFFFFF', fontSize: 14, + color: '#6B7280', fontWeight: '400', - opacity: 0.9, }, - progressContainer: { - width: 80, - height: 80, - justifyContent: 'center', + statusCards: { + flexDirection: 'row', + justifyContent: 'space-between', + gap: 12, + }, + statusCard: { + flex: 1, + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 12, + borderWidth: 1, + borderColor: '#E5E7EB', + alignItems: 'flex-start', + minHeight: 80, + }, + cardHeader: { + flexDirection: 'row', alignItems: 'center', - position: 'relative', + marginBottom: 8, + gap: 6, + flexWrap: 'wrap', }, - progressCircle: { - position: 'absolute', - width: 80, - height: 80, - borderRadius: 40, + cardLabel: { + fontSize: 11, + fontWeight: '500', + color: '#1F2937', + lineHeight: 14, }, - progressBackground: { - borderWidth: 6, - borderColor: 'rgba(255, 255, 255, 0.3)', - }, - progressFill: { - borderWidth: 6, - borderColor: 'transparent', - justifyContent: 'center', - alignItems: 'center', - }, - progressArc: { - position: 'absolute', - }, - progressTextContainer: { - position: 'absolute', - justifyContent: 'center', - alignItems: 'center', - }, - progressText: { - color: '#FFFFFF', - fontSize: 16, + cardCount: { + fontSize: 24, fontWeight: '700', + color: '#1F2937', }, }); diff --git a/constants/Colors.ts b/constants/Colors.ts index 5b48891..37b61d3 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -6,8 +6,8 @@ // 原子调色板(与设计图一致) export const palette = { // Primary - primary: '#87CEEB', - ink: '#192126', + primary: '#7A5AF8', + ink: '#FFFFFF', // Secondary / Neutrals neutral100: '#888F92', @@ -18,7 +18,7 @@ export const palette = { purple: '#A48AED', red: '#ED4747', orange: '#FCC46F', - blue: '#87CEEB', // 更贴近logo背景的天空蓝 + blue: '#7A5AF8', // 更贴近logo背景的天空蓝 blueSecondary: '#4682B4', // 钢蓝色,用于选中状态 green: '#9ceb87', // 温暖的绿色,用于心情日历等 } as const;