660 lines
18 KiB
TypeScript
660 lines
18 KiB
TypeScript
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 { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||
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 safeAreaTop = useSafeAreaTop()
|
||
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={{
|
||
paddingTop: safeAreaTop
|
||
}} />
|
||
<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={{
|
||
paddingTop: safeAreaTop
|
||
}} />
|
||
<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} contentContainerStyle={{
|
||
paddingTop: safeAreaTop
|
||
}} 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',
|
||
},
|
||
});
|