feat: 新增任务详情页面及相关功能

- 创建任务详情页面,展示任务的详细信息,包括状态、描述、优先级和进度
- 实现任务完成和跳过功能,用户可通过按钮操作更新任务状态
- 添加评论功能,用户可以对任务进行评论并发送
- 优化任务卡片,点击后可跳转至任务详情页面
- 更新相关样式,确保界面一致性和美观性
This commit is contained in:
2025-08-23 13:33:39 +08:00
parent 75806df660
commit b807e498ed
5 changed files with 680 additions and 65 deletions

649
app/task-detail.tsx Normal file
View File

@@ -0,0 +1,649 @@
import { useGlobalDialog } from '@/components/ui/DialogProvider';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { completeTask, skipTask } from '@/store/tasksSlice';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
Alert,
Image,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
export default function TaskDetailScreen() {
const { taskId } = useLocalSearchParams<{ taskId: string }>();
const router = useRouter();
const theme = useColorScheme() ?? 'light';
const colorTokens = Colors[theme];
const dispatch = useAppDispatch();
const { showConfirm } = useGlobalDialog();
// 从Redux中获取任务数据
const { tasks, tasksLoading } = useAppSelector(state => state.tasks);
const task = tasks.find(t => t.id === taskId) || null;
const [comment, setComment] = useState('');
const getStatusText = (status: string) => {
switch (status) {
case 'completed':
return '已完成';
case 'in_progress':
return '进行中';
case 'overdue':
return '已过期';
case 'skipped':
return '已跳过';
default:
return '待开始';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return '#10B981';
case 'in_progress':
return '#7A5AF8';
case 'overdue':
return '#EF4444';
case 'skipped':
return '#6B7280';
default:
return '#6B7280';
}
};
const getDifficultyText = (difficulty: string) => {
switch (difficulty) {
case 'very_easy':
return '非常简单 (少于一天)';
case 'easy':
return '简单 (1-2天)';
case 'medium':
return '中等 (3-5天)';
case 'hard':
return '困难 (1-2周)';
case 'very_hard':
return '非常困难 (2周以上)';
default:
return '非常简单 (少于一天)';
}
};
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'very_easy':
return '#10B981';
case 'easy':
return '#34D399';
case 'medium':
return '#F59E0B';
case 'hard':
return '#F97316';
case 'very_hard':
return '#EF4444';
default:
return '#10B981';
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
return `创建于 ${date.toLocaleDateString('zh-CN', options)}`;
};
const handleCompleteTask = async () => {
if (!task || task.status === 'completed') {
return;
}
try {
await dispatch(completeTask({
taskId: task.id,
completionData: {
count: 1,
notes: '通过任务详情页面完成'
}
})).unwrap();
// 检查任务是否真正完成(当前完成次数是否达到目标次数)
const updatedTask = tasks.find(t => t.id === task.id);
if (updatedTask && updatedTask.currentCount >= updatedTask.targetCount) {
Alert.alert('成功', '任务已完成!');
router.back();
} else {
Alert.alert('成功', '任务进度已更新!');
}
} catch (error) {
Alert.alert('错误', '完成任务失败,请重试');
}
};
const handleSkipTask = async () => {
if (!task || task.status === 'completed' || task.status === 'skipped') {
return;
}
showConfirm(
{
title: '确认跳过任务',
message: `确定要跳过任务"${task.title}"吗?\n\n跳过后的任务将不会显示在任务列表中且无法恢复。`,
confirmText: '跳过',
cancelText: '取消',
destructive: true,
icon: 'warning',
iconColor: '#F59E0B',
},
async () => {
try {
await dispatch(skipTask({
taskId: task.id,
skipData: {
reason: '用户主动跳过'
}
})).unwrap();
Alert.alert('成功', '任务已跳过!');
router.back();
} catch (error) {
Alert.alert('错误', '跳过任务失败,请重试');
}
}
);
};
const handleSendComment = () => {
if (comment.trim()) {
// 这里应该调用API发送评论
console.log('发送评论:', comment);
setComment('');
Alert.alert('成功', '评论已发送!');
}
};
if (tasksLoading) {
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
<HeaderBar
title="任务详情"
onBack={() => router.back()}
/>
<View style={styles.loadingContainer}>
<Text style={[styles.loadingText, { color: colorTokens.text }]}>...</Text>
</View>
</View>
);
}
if (!task) {
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
<HeaderBar
title="任务详情"
onBack={() => router.back()}
/>
<View style={styles.errorContainer}>
<Text style={[styles.errorText, { color: colorTokens.text }]}></Text>
</View>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
{/* 使用HeaderBar组件 */}
<HeaderBar
title="任务详情"
onBack={() => router.back()}
right={
task.status !== 'completed' && task.status !== 'skipped' && task.currentCount < task.targetCount ? (
<TouchableOpacity onPress={handleCompleteTask} style={styles.completeButton}>
<Image
source={require('@/assets/images/task/iconTaskHeader.png')}
style={styles.taskIcon}
resizeMode="contain"
/>
</TouchableOpacity>
) : undefined
}
/>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{/* 任务标题和创建时间 */}
<View style={styles.titleSection}>
<Text style={[styles.taskTitle, { color: colorTokens.text }]}>{task.title}</Text>
<Text style={[styles.createdDate, { color: colorTokens.textSecondary }]}>
{formatDate(task.startDate)}
</Text>
</View>
{/* 状态标签 */}
<View style={styles.statusContainer}>
<View style={[styles.statusTag, { backgroundColor: getStatusColor(task.status) }]}>
<Text style={styles.statusTagText}>{getStatusText(task.status)}</Text>
</View>
</View>
{/* 描述区域 */}
<View style={styles.descriptionSection}>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.descriptionText, { color: colorTokens.textSecondary }]}>
{task.description || '暂无描述'}
</Text>
</View>
{/* 优先级和难度 */}
<View style={styles.infoSection}>
<View style={styles.infoItem}>
<Text style={[styles.infoLabel, { color: colorTokens.text }]}></Text>
<View style={styles.priorityTag}>
<MaterialIcons name="flag" size={16} color="#FFFFFF" />
<Text style={styles.priorityTagText}></Text>
</View>
</View>
<View style={styles.infoItem}>
<Text style={[styles.infoLabel, { color: colorTokens.text }]}></Text>
<View style={[styles.difficultyTag, { backgroundColor: getDifficultyColor('very_easy') }]}>
<MaterialIcons name="sentiment-satisfied" size={16} color="#FFFFFF" />
<Text style={styles.difficultyTagText}> ()</Text>
</View>
</View>
</View>
{/* 任务进度信息 */}
<View style={styles.progressSection}>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}></Text>
{/* 进度条 */}
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{
width: task.progressPercentage > 0 ? `${Math.min(task.progressPercentage, 100)}%` : '2%',
backgroundColor: task.progressPercentage >= 100
? '#10B981'
: task.progressPercentage >= 50
? '#F59E0B'
: task.progressPercentage > 0
? colorTokens.primary
: '#E5E7EB',
},
]}
/>
{task.progressPercentage > 0 && task.progressPercentage < 100 && (
<View style={styles.progressGlow} />
)}
{/* 进度文本 */}
<View style={styles.progressTextContainer}>
<Text style={styles.progressText}>{task.currentCount}/{task.targetCount}</Text>
</View>
</View>
{/* 进度详细信息 */}
<View style={styles.progressInfo}>
<View style={styles.progressItem}>
<Text style={[styles.progressLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.progressValue, { color: colorTokens.text }]}>{task.targetCount}</Text>
</View>
<View style={styles.progressItem}>
<Text style={[styles.progressLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.progressValue, { color: colorTokens.text }]}>{task.currentCount}</Text>
</View>
<View style={styles.progressItem}>
<Text style={[styles.progressLabel, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.progressValue, { color: colorTokens.text }]}>{task.daysRemaining}</Text>
</View>
</View>
</View>
{/* 评论区域 */}
<View style={styles.commentSection}>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}></Text>
<View style={styles.commentInputContainer}>
<View style={styles.commentAvatar}>
<Image
source={require('@/assets/images/Sealife.jpeg')}
style={styles.commentAvatarImage}
resizeMode="cover"
/>
</View>
<View style={styles.commentInputWrapper}>
<TextInput
style={[styles.commentInput, {
color: colorTokens.text,
backgroundColor: '#F3F4F6'
}]}
placeholder="写评论..."
placeholderTextColor="#9CA3AF"
value={comment}
onChangeText={setComment}
multiline
maxLength={500}
/>
<TouchableOpacity
style={[styles.sendButton, {
backgroundColor: comment.trim() ? '#6B7280' : '#D1D5DB'
}]}
onPress={handleSendComment}
disabled={!comment.trim()}
>
<MaterialIcons name="send" size={16} color="#FFFFFF" />
</TouchableOpacity>
</View>
</View>
</View>
{/* 底部操作按钮 */}
{task.status !== 'completed' && task.status !== 'skipped' && task.currentCount < task.targetCount && (
<View style={styles.actionButtons}>
<TouchableOpacity
style={[styles.actionButton, styles.skipButton]}
onPress={handleSkipTask}
>
<MaterialIcons name="skip-next" size={20} color="#6B7280" />
<Text style={styles.skipButtonText}></Text>
</TouchableOpacity>
</View>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: 16,
fontWeight: '500',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
fontSize: 16,
fontWeight: '500',
},
completeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#7A5AF8',
alignItems: 'center',
justifyContent: 'center',
},
taskIcon: {
width: 20,
height: 20,
},
scrollView: {
flex: 1,
},
titleSection: {
padding: 16,
paddingBottom: 8,
},
taskTitle: {
fontSize: 20,
fontWeight: '600',
lineHeight: 28,
marginBottom: 4,
},
createdDate: {
fontSize: 14,
fontWeight: '400',
opacity: 0.7,
},
statusContainer: {
paddingHorizontal: 16,
paddingBottom: 16,
alignItems: 'flex-end',
},
statusTag: {
alignSelf: 'flex-end',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
statusTagText: {
fontSize: 14,
fontWeight: '600',
color: '#FFFFFF',
},
imagePlaceholder: {
height: 240,
backgroundColor: '#F9FAFB',
marginHorizontal: 16,
marginBottom: 20,
borderRadius: 12,
borderWidth: 2,
borderColor: '#E5E7EB',
borderStyle: 'dashed',
alignItems: 'center',
justifyContent: 'center',
},
imagePlaceholderText: {
fontSize: 16,
fontWeight: '500',
color: '#9CA3AF',
marginTop: 8,
},
descriptionSection: {
paddingHorizontal: 16,
marginBottom: 20,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 12,
},
descriptionText: {
fontSize: 15,
lineHeight: 22,
fontWeight: '400',
},
infoSection: {
paddingHorizontal: 16,
marginBottom: 20,
},
infoItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 12,
},
infoLabel: {
fontSize: 15,
fontWeight: '500',
},
priorityTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: '#EF4444',
},
priorityTagText: {
fontSize: 13,
fontWeight: '600',
color: '#FFFFFF',
},
difficultyTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
difficultyTagText: {
fontSize: 13,
fontWeight: '600',
color: '#FFFFFF',
},
progressSection: {
paddingHorizontal: 16,
marginBottom: 20,
},
progressBar: {
height: 6,
backgroundColor: '#F3F4F6',
borderRadius: 3,
marginBottom: 16,
overflow: 'visible',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
position: 'relative',
},
progressFill: {
height: '100%',
borderRadius: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 3,
},
progressGlow: {
position: 'absolute',
right: 0,
top: 0,
width: 8,
height: '100%',
backgroundColor: 'rgba(255, 255, 255, 0.6)',
borderRadius: 3,
},
progressTextContainer: {
position: 'absolute',
right: 0,
top: -6,
backgroundColor: '#FFFFFF',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
borderWidth: 1,
borderColor: '#E5E7EB',
zIndex: 1,
},
progressText: {
fontSize: 10,
fontWeight: '600',
color: '#374151',
},
progressInfo: {
flexDirection: 'row',
justifyContent: 'space-between',
},
progressItem: {
alignItems: 'center',
flex: 1,
},
progressLabel: {
fontSize: 13,
fontWeight: '400',
marginBottom: 4,
},
progressValue: {
fontSize: 18,
fontWeight: '600',
},
commentSection: {
paddingHorizontal: 16,
marginBottom: 20,
},
commentInputContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
commentAvatar: {
width: 28,
height: 28,
borderRadius: 14,
overflow: 'hidden',
},
commentAvatarImage: {
width: '100%',
height: '100%',
},
commentInputWrapper: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
commentInput: {
flex: 1,
minHeight: 36,
maxHeight: 120,
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 18,
fontSize: 15,
textAlignVertical: 'top',
},
sendButton: {
width: 28,
height: 28,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
actionButtons: {
paddingHorizontal: 16,
paddingBottom: 20,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
},
skipButton: {
backgroundColor: '#F9FAFB',
borderColor: '#E5E7EB',
},
skipButtonText: {
fontSize: 16,
fontWeight: '500',
color: '#6B7280',
},
});