feat: 增强任务管理功能,新增任务筛选和状态统计组件

- 在目标页面中集成任务筛选标签,支持按状态(全部、待完成、已完成)过滤任务
- 更新任务卡片,展示任务状态和优先级信息
- 新增任务进度卡片,统计各状态任务数量
- 优化空状态展示,根据筛选条件动态显示提示信息
- 引入新图标和图片资源,提升界面视觉效果
This commit is contained in:
2025-08-22 20:45:15 +08:00
parent 259f10540e
commit 9e719a9eda
8 changed files with 593 additions and 262 deletions

View File

@@ -1,5 +1,6 @@
import CreateGoalModal from '@/components/CreateGoalModal'; import CreateGoalModal from '@/components/CreateGoalModal';
import { TaskCard } from '@/components/TaskCard'; import { TaskCard } from '@/components/TaskCard';
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
import { TaskProgressCard } from '@/components/TaskProgressCard'; import { TaskProgressCard } from '@/components/TaskProgressCard';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
@@ -13,7 +14,7 @@ import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react'; 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() { export default function GoalsScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
@@ -40,6 +41,7 @@ export default function GoalsScreen() {
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
// 页面聚焦时重新加载数据 // 页面聚焦时重新加载数据
useFocusEffect( useFocusEffect(
@@ -160,6 +162,30 @@ export default function GoalsScreen() {
router.push('/goals-detail'); 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 renderTaskItem = ({ item }: { item: TaskListItem }) => (
<TaskCard <TaskCard
@@ -171,16 +197,34 @@ export default function GoalsScreen() {
); );
// 渲染空状态 // 渲染空状态
const renderEmptyState = () => ( const renderEmptyState = () => {
let title = '暂无任务';
let subtitle = '创建目标后,系统会自动生成相应的任务';
if (selectedFilter === 'pending') {
title = '暂无待完成的任务';
subtitle = '当前没有待完成的任务';
} else if (selectedFilter === 'completed') {
title = '暂无已完成的任务';
subtitle = '完成一些任务后,它们会显示在这里';
}
return (
<View style={styles.emptyState}> <View style={styles.emptyState}>
<Image
source={require('@/assets/images/task/ImageEmpty.png')}
style={styles.emptyStateImage}
resizeMode="contain"
/>
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}> <Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
{title}
</Text> </Text>
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}> <Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
{subtitle}
</Text> </Text>
</View> </View>
); );
};
// 渲染加载更多 // 渲染加载更多
const renderLoadMore = () => { const renderLoadMore = () => {
@@ -209,39 +253,71 @@ export default function GoalsScreen() {
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
/> />
{/* 装饰性圆圈 */} <View style={{
<View style={styles.decorativeCircle1} /> position: 'absolute',
<View style={styles.decorativeCircle2} /> 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.content}>
{/* 标题区域 */} {/* 标题区域 */}
<View style={styles.header}> <View style={styles.header}>
<Text style={[styles.pageTitle, { color: colorTokens.text }]}> <View>
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
</Text>
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
</Text> </Text>
<View style={styles.headerButtons}>
<TouchableOpacity
style={styles.goalsButton}
onPress={handleNavigateToGoals}
>
<MaterialIcons name="flag" size={16} color="#0EA5E9" />
</TouchableOpacity>
<TouchableOpacity
style={styles.addButton}
onPress={() => setShowCreateModal(true)}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
{/* 任务进度卡片 */} {/* 任务进度卡片 */}
<TaskProgressCard tasks={tasks} /> <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={() => setShowCreateModal(true)}
>
<Text style={styles.cardAddButtonText}>+</Text>
</TouchableOpacity>
</View>
}
/>
</View>
{/* 任务筛选标签 */}
<TaskFilterTabs
selectedFilter={selectedFilter}
onFilterChange={handleFilterChange}
taskCounts={taskCounts}
/>
{/* 任务列表 */} {/* 任务列表 */}
<View style={styles.taskListContainer}> <View style={styles.taskListContainer}>
<FlatList <FlatList
data={tasks} data={filteredTasks}
renderItem={renderTaskItem} renderItem={renderTaskItem}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
contentContainerStyle={styles.taskList} contentContainerStyle={styles.taskList}
@@ -340,6 +416,12 @@ const styles = StyleSheet.create({
fontWeight: '800', fontWeight: '800',
marginBottom: 4, marginBottom: 4,
}, },
pageTitle2: {
fontSize: 16,
fontWeight: '400',
color: '#FFFFFF',
lineHeight: 24,
},
addButton: { addButton: {
width: 30, width: 30,
height: 30, height: 30,
@@ -362,7 +444,6 @@ const styles = StyleSheet.create({
taskListContainer: { taskListContainer: {
flex: 1, flex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderTopLeftRadius: 24, borderTopLeftRadius: 24,
borderTopRightRadius: 24, borderTopRightRadius: 24,
overflow: 'hidden', overflow: 'hidden',
@@ -377,6 +458,11 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 60, paddingVertical: 60,
}, },
emptyStateImage: {
width: 223,
height: 59,
marginBottom: 20,
},
emptyStateTitle: { emptyStateTitle: {
fontSize: 18, fontSize: 18,
fontWeight: '600', fontWeight: '600',
@@ -395,4 +481,40 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
fontWeight: '500', 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,
},
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,8 +1,9 @@
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme'; import { useColorScheme } from '@/hooks/useColorScheme';
import { TaskListItem } from '@/types/goals'; import { TaskListItem } from '@/types/goals';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import React from 'react'; import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface TaskCardProps { interface TaskCardProps {
task: TaskListItem; task: TaskListItem;
@@ -50,28 +51,39 @@ export const TaskCard: React.FC<TaskCardProps> = ({
} }
}; };
const getCategoryColor = (category?: string) => { const getPriorityColor = (status: string) => {
if (!category) return '#6B7280'; switch (status) {
if (category.includes('运动') || category.includes('健身')) return '#EF4444'; case 'overdue':
if (category.includes('工作')) return '#3B82F6'; return '#EF4444'; // High - 过期任务
if (category.includes('健康')) return '#10B981'; case 'in_progress':
if (category.includes('财务')) return '#F59E0B'; return '#F59E0B'; // Medium - 进行中
return '#6B7280'; 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 formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
const today = new Date(); const month = date.toLocaleDateString('zh-CN', { month: 'short' });
const tomorrow = new Date(today); const day = date.getDate();
tomorrow.setDate(tomorrow.getDate() + 1); return `${day} ${month}`;
if (date.toDateString() === today.toDateString()) {
return '今天';
} else if (date.toDateString() === tomorrow.toDateString()) {
return '明天';
} else {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
}; };
return ( return (
@@ -80,71 +92,74 @@ export const TaskCard: React.FC<TaskCardProps> = ({
onPress={() => onPress?.(task)} onPress={() => onPress?.(task)}
activeOpacity={0.7} activeOpacity={0.7}
> >
{/* 头部区域 */}
<View style={styles.header}> <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}> <Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
{task.title} {task.title}
</Text> </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>
</View> </View>
{task.description && ( {/* 状态和优先级标签 */}
<Text style={[styles.description, { color: colorTokens.textSecondary }]} numberOfLines={2}> <View style={styles.tagsContainer}>
{task.description} <View style={styles.statusTag}>
</Text> <MaterialIcons name="schedule" size={12} color="#6B7280" />
)} <Text style={styles.statusTagText}>{getStatusText(task.status)}</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>
<View style={[styles.progressBar, { backgroundColor: colorTokens.border }]}> <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 <View
style={[ style={[
styles.progressFill, styles.progressFill,
{ {
width: `${task.progressPercentage}%`, width: `${Math.min(task.progressPercentage, 100)}%`,
backgroundColor: getStatusColor(task.status), backgroundColor: colorTokens.primary,
}, },
]} ]}
/> />
</View> </View>
</View>
{/* 底部信息 */}
<View style={styles.footer}> <View style={styles.footer}>
<Text style={[styles.dateText, { color: colorTokens.textSecondary }]}> <View style={styles.teamSection}>
{formatDate(task.startDate)} {/* 模拟团队成员头像 */}
</Text> <View style={styles.avatars}>
<View style={[styles.avatar, { backgroundColor: '#FBBF24' }]}>
{task.status === 'pending' || task.status === 'in_progress' ? ( <Text style={styles.avatarText}>A</Text>
<View style={styles.actionButtons}> </View>
<TouchableOpacity <View style={[styles.avatar, { backgroundColor: '#34D399' }]}>
style={[styles.actionButton, styles.skipButton]} <Text style={styles.avatarText}>B</Text>
onPress={() => onSkip?.(task)} </View>
> <View style={[styles.avatar, { backgroundColor: '#60A5FA' }]}>
<Text style={styles.skipButtonText}></Text> <Text style={styles.avatarText}>C</Text>
</TouchableOpacity> </View>
<TouchableOpacity </View>
style={[styles.actionButton, styles.completeButton]} </View>
onPress={() => onComplete?.(task)}
> <View style={styles.infoSection}>
<Text style={styles.completeButtonText}></Text> <View style={styles.infoTag}>
</TouchableOpacity> <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>
) : null}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );
@@ -162,99 +177,119 @@ const styles = StyleSheet.create({
elevation: 3, elevation: 3,
}, },
header: { header: {
flexDirection: 'row', marginBottom: 12,
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
}, },
titleContainer: { titleSection: {
flex: 1, flexDirection: 'row',
marginRight: 8, 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: { title: {
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
marginBottom: 4, lineHeight: 22,
flex: 1,
}, },
categoryTag: { tagsContainer: {
paddingHorizontal: 8, flexDirection: 'row',
paddingVertical: 2, gap: 8,
borderRadius: 12, marginBottom: 12,
alignSelf: 'flex-start',
},
categoryText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '500',
}, },
statusTag: { 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, paddingHorizontal: 8,
paddingVertical: 4, paddingVertical: 4,
borderRadius: 12, borderRadius: 12,
}, },
statusText: { priorityTagText: {
fontSize: 12,
fontWeight: '500',
color: '#FFFFFF', 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: { progressBar: {
height: 6, height: 2,
borderRadius: 3, backgroundColor: '#E5E7EB',
borderRadius: 1,
marginBottom: 16,
overflow: 'hidden', overflow: 'hidden',
}, },
progressFill: { progressFill: {
height: '100%', height: '100%',
borderRadius: 3, borderRadius: 1,
}, },
footer: { footer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}, },
dateText: { teamSection: {
fontSize: 12, flexDirection: 'row',
fontWeight: '500', 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', flexDirection: 'row',
gap: 8, gap: 8,
}, },
actionButton: { infoTag: {
paddingHorizontal: 12, flexDirection: 'row',
paddingVertical: 6, alignItems: 'center',
borderRadius: 16, gap: 4,
}, paddingHorizontal: 8,
skipButton: { paddingVertical: 4,
borderRadius: 12,
backgroundColor: '#F3F4F6', backgroundColor: '#F3F4F6',
}, },
skipButtonText: { infoTagText: {
color: '#6B7280',
fontSize: 12,
fontWeight: '500',
},
completeButton: {
backgroundColor: '#10B981',
},
completeButtonText: {
color: '#FFFFFF',
fontSize: 12, fontSize: 12,
fontWeight: '500', fontWeight: '500',
color: '#374151',
}, },
}); });

View File

@@ -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<TaskFilterTabsProps> = ({
selectedFilter,
onFilterChange,
taskCounts,
}) => {
return (
<View style={styles.container}>
<View style={styles.tabContainer}>
{/* 全部 Tab */}
<TouchableOpacity
style={[
styles.tab,
selectedFilter === 'all' && styles.activeTab
]}
onPress={() => onFilterChange('all')}
>
<Text style={[
styles.tabText,
selectedFilter === 'all' && styles.activeTabText
]}>
</Text>
<View style={[
styles.badge,
selectedFilter === 'all' ? styles.activeBadge : styles.inactiveBadge
]}>
<Text style={[
styles.badgeText,
selectedFilter === 'all' && styles.activeBadgeText
]}>
{taskCounts.all}
</Text>
</View>
</TouchableOpacity>
{/* 待完成 Tab */}
<TouchableOpacity
style={[
styles.tab,
selectedFilter === 'pending' && styles.activeTab
]}
onPress={() => onFilterChange('pending')}
>
<Text style={[
styles.tabText,
selectedFilter === 'pending' && styles.activeTabText
]}>
</Text>
<View style={[
styles.badge,
selectedFilter === 'pending' ? styles.activeBadge : styles.inactiveBadge
]}>
<Text style={[
styles.badgeText,
selectedFilter === 'pending' && styles.activeBadgeText
]}>
{taskCounts.pending}
</Text>
</View>
</TouchableOpacity>
{/* 已完成 Tab */}
<TouchableOpacity
style={[
styles.tab,
selectedFilter === 'completed' && styles.activeTab
]}
onPress={() => onFilterChange('completed')}
>
<Text style={[
styles.tabText,
selectedFilter === 'completed' && styles.activeTabText
]}>
</Text>
<View style={[
styles.badge,
selectedFilter === 'completed' ? styles.activeBadge : styles.inactiveBadge
]}>
<Text style={[
styles.badgeText,
selectedFilter === 'completed' && styles.activeBadgeText
]}>
{taskCounts.completed}
</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
};
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',
},
});

View File

@@ -1,140 +1,139 @@
import { TaskListItem } from '@/types/goals'; 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'; import { StyleSheet, Text, View } from 'react-native';
interface TaskProgressCardProps { interface TaskProgressCardProps {
tasks: TaskListItem[]; tasks: TaskListItem[];
headerButtons?: ReactNode;
} }
export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
tasks, tasks,
headerButtons,
}) => { }) => {
// 计算今日任务完成进度 // 计算各状态的任务数量
const todayTasks = tasks.filter(task => task.isToday); const pendingTasks = tasks.filter(task => task.status === 'pending');
const completedTodayTasks = todayTasks.filter(task => task.status === 'completed'); const completedTasks = tasks.filter(task => task.status === 'completed');
const progressPercentage = todayTasks.length > 0 const skippedTasks = tasks.filter(task => task.status === 'skipped');
? Math.round((completedTodayTasks.length / todayTasks.length) * 100)
: 0;
// 计算进度角度
const progressAngle = (progressPercentage / 100) * 360;
return ( return (
<View style={styles.container}> <View style={styles.container}>
{/* 左侧内容 */} {/* 标题区域 */}
<View style={styles.leftContent}> <View style={styles.header}>
<View style={styles.textContainer}> <View style={styles.titleContainer}>
<Text style={styles.title}></Text> <Text style={styles.title}></Text>
<Text style={styles.subtitle}>!</Text> <Text style={styles.subtitle}></Text>
</View>
{headerButtons && (
<View style={styles.headerButtons}>
{headerButtons}
</View>
)}
</View> </View>
{/* 状态卡片区域 */}
<View style={styles.statusCards}>
{/* 待完成 卡片 */}
<View style={styles.statusCard}>
<View style={styles.cardHeader}>
<MaterialIcons name="pending" size={16} color="#7A5AF8" />
<Text style={styles.cardLabel} numberOfLines={1}></Text>
</View>
<Text style={styles.cardCount}>{pendingTasks.length}</Text>
</View> </View>
{/* 右侧进度圆环 */} {/* 已完成 卡片 */}
<View style={styles.progressContainer}> <View style={styles.statusCard}>
{/* 背景圆环 */} <View style={styles.cardHeader}>
<View style={[styles.progressCircle, styles.progressBackground]} /> <MaterialIcons name="check-circle" size={16} color="#10B981" />
<Text style={styles.cardLabel} numberOfLines={1}></Text>
{/* 进度圆环 */} </View>
<View style={[styles.progressCircle, styles.progressFill]}> <Text style={styles.cardCount}>{completedTasks.length}</Text>
<View
style={[
styles.progressArc,
{
width: 68,
height: 68,
borderRadius: 34,
borderWidth: 6,
borderColor: '#8B5CF6',
borderTopColor: progressAngle > 0 ? '#8B5CF6' : 'transparent',
borderRightColor: progressAngle > 90 ? '#8B5CF6' : 'transparent',
borderBottomColor: progressAngle > 180 ? '#8B5CF6' : 'transparent',
borderLeftColor: progressAngle > 270 ? '#8B5CF6' : 'transparent',
transform: [{ rotate: '-90deg' }],
},
]}
/>
</View> </View>
{/* 进度文字 */} {/* 已跳过 卡片 */}
<View style={styles.progressTextContainer}> <View style={styles.statusCard}>
<Text style={styles.progressText}>{progressPercentage}%</Text> <View style={styles.cardHeader}>
<MaterialIcons name="skip-next" size={16} color="#6B7280" />
<Text style={styles.cardLabel} numberOfLines={1}></Text>
</View>
<Text style={styles.cardCount}>{skippedTasks.length}</Text>
</View> </View>
</View> </View>
</View> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: '#8B5CF6', backgroundColor: '#FFFFFF',
borderRadius: 16, borderRadius: 16,
padding: 20, padding: 20,
marginHorizontal: 20, marginHorizontal: 20,
marginBottom: 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', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
position: 'relative', gap: 8,
shadowColor: '#8B5CF6',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
leftContent: {
flex: 1,
marginRight: 20,
},
textContainer: {
marginBottom: 16,
}, },
title: { title: {
color: '#FFFFFF', fontSize: 20,
fontSize: 16, fontWeight: '700',
fontWeight: '600', color: '#1F2937',
marginBottom: 2, marginBottom: 4,
}, },
subtitle: { subtitle: {
color: '#FFFFFF',
fontSize: 14, fontSize: 14,
color: '#6B7280',
fontWeight: '400', fontWeight: '400',
opacity: 0.9,
}, },
progressContainer: { statusCards: {
width: 80, flexDirection: 'row',
height: 80, justifyContent: 'space-between',
justifyContent: 'center', 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', alignItems: 'center',
position: 'relative', marginBottom: 8,
gap: 6,
flexWrap: 'wrap',
}, },
progressCircle: { cardLabel: {
position: 'absolute', fontSize: 11,
width: 80, fontWeight: '500',
height: 80, color: '#1F2937',
borderRadius: 40, lineHeight: 14,
}, },
progressBackground: { cardCount: {
borderWidth: 6, fontSize: 24,
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,
fontWeight: '700', fontWeight: '700',
color: '#1F2937',
}, },
}); });

View File

@@ -6,8 +6,8 @@
// 原子调色板(与设计图一致) // 原子调色板(与设计图一致)
export const palette = { export const palette = {
// Primary // Primary
primary: '#87CEEB', primary: '#7A5AF8',
ink: '#192126', ink: '#FFFFFF',
// Secondary / Neutrals // Secondary / Neutrals
neutral100: '#888F92', neutral100: '#888F92',
@@ -18,7 +18,7 @@ export const palette = {
purple: '#A48AED', purple: '#A48AED',
red: '#ED4747', red: '#ED4747',
orange: '#FCC46F', orange: '#FCC46F',
blue: '#87CEEB', // 更贴近logo背景的天空蓝 blue: '#7A5AF8', // 更贴近logo背景的天空蓝
blueSecondary: '#4682B4', // 钢蓝色,用于选中状态 blueSecondary: '#4682B4', // 钢蓝色,用于选中状态
green: '#9ceb87', // 温暖的绿色,用于心情日历等 green: '#9ceb87', // 温暖的绿色,用于心情日历等
} as const; } as const;