Files
digital-pilates/app/task-detail.tsx
richarjiang 20a244e375 feat: 实现目标通知功能及相关组件
- 新增目标通知功能,支持根据用户创建目标时选择的频率和开始时间自动创建本地定时推送通知
- 实现每日、每周和每月的重复类型,用户可自定义选择提醒时间和重复规则
- 集成目标通知测试组件,方便开发者测试不同类型的通知
- 更新相关文档,详细描述目标通知功能的实现和使用方法
- 优化目标页面,确保用户体验和界面一致性
2025-08-23 17:13:04 +08:00

650 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
{/* 底部操作按钮 */}
{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>
)}
{/* 评论区域 */}
<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>
</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',
},
});