feat: 新增任务详情页面及相关功能
- 创建任务详情页面,展示任务的详细信息,包括状态、描述、优先级和进度 - 实现任务完成和跳过功能,用户可通过按钮操作更新任务状态 - 添加评论功能,用户可以对任务进行评论并发送 - 优化任务卡片,点击后可跳转至任务详情页面 - 更新相关样式,确保界面一致性和美观性
This commit is contained in:
649
app/task-detail.tsx
Normal file
649
app/task-detail.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
|||||||
import { completeTask, skipTask } from '@/store/tasksSlice';
|
import { completeTask, skipTask } from '@/store/tasksSlice';
|
||||||
import { TaskListItem } from '@/types/goals';
|
import { TaskListItem } from '@/types/goals';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { showConfirm } = useGlobalDialog();
|
const { showConfirm } = useGlobalDialog();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
@@ -123,6 +125,10 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTaskPress = () => {
|
||||||
|
router.push(`/task-detail?taskId=${task.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const renderActionIcons = () => {
|
const renderActionIcons = () => {
|
||||||
if (task.status === 'completed' || task.status === 'overdue' || task.status === 'skipped') {
|
if (task.status === 'completed' || task.status === 'overdue' || task.status === 'skipped') {
|
||||||
return null;
|
return null;
|
||||||
@@ -158,7 +164,7 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.container, { backgroundColor: colorTokens.background }]}
|
style={[styles.container, { backgroundColor: colorTokens.background }]}
|
||||||
onPress={() => {}}
|
onPress={handleTaskPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
{/* 头部区域 */}
|
{/* 头部区域 */}
|
||||||
@@ -205,23 +211,21 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
{/* 进度百分比文本 */}
|
{/* 进度百分比文本 */}
|
||||||
<View style={styles.progressTextContainer}>
|
<View style={styles.progressTextContainer}>
|
||||||
<Text style={styles.progressText}>{Math.round(task.progressPercentage)}%</Text>
|
<Text style={styles.progressText}>{task.currentCount}/{task.targetCount}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 底部信息 */}
|
{/* 底部信息 */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<View style={styles.teamSection}>
|
<View style={styles.teamSection}>
|
||||||
{/* 模拟团队成员头像 */}
|
{/* 团队成员头像 */}
|
||||||
<View style={styles.avatars}>
|
<View style={styles.avatars}>
|
||||||
<View style={[styles.avatar, { backgroundColor: '#FBBF24' }]}>
|
<View style={styles.avatar}>
|
||||||
<Text style={styles.avatarText}>A</Text>
|
<Image
|
||||||
</View>
|
source={require('@/assets/images/Sealife.jpeg')}
|
||||||
<View style={[styles.avatar, { backgroundColor: '#34D399' }]}>
|
style={styles.avatarImage}
|
||||||
<Text style={styles.avatarText}>B</Text>
|
resizeMode="cover"
|
||||||
</View>
|
/>
|
||||||
<View style={[styles.avatar, { backgroundColor: '#60A5FA' }]}>
|
|
||||||
<Text style={styles.avatarText}>C</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -395,17 +399,15 @@ const styles = StyleSheet.create({
|
|||||||
avatar: {
|
avatar: {
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
borderRadius: 12,
|
borderRadius: 24,
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: -8,
|
marginRight: -8,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#FFFFFF',
|
borderColor: '#FFFFFF',
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
avatarText: {
|
avatarImage: {
|
||||||
fontSize: 10,
|
width: '100%',
|
||||||
fontWeight: '600',
|
height: '100%',
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
},
|
||||||
infoSection: {
|
infoSection: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
|
|||||||
{/* 标题区域 */}
|
{/* 标题区域 */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.title}>任务状态统计</Text>
|
<Text style={styles.title}>统计</Text>
|
||||||
<Text style={styles.subtitle}>各状态任务数量分布</Text>
|
|
||||||
</View>
|
</View>
|
||||||
{headerButtons && (
|
{headerButtons && (
|
||||||
<View style={styles.headerButtons}>
|
<View style={styles.headerButtons}>
|
||||||
@@ -82,7 +81,7 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -45,38 +45,6 @@ export function HeaderBar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBackButtonStyle = () => {
|
|
||||||
const baseStyle = [styles.backButton];
|
|
||||||
|
|
||||||
switch (variant) {
|
|
||||||
case 'elevated':
|
|
||||||
return [...baseStyle, {
|
|
||||||
backgroundColor: `${theme.primary}15`, // 15% 透明度
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: `${theme.primary}20`, // 20% 透明度
|
|
||||||
}];
|
|
||||||
case 'minimal':
|
|
||||||
return [...baseStyle, {
|
|
||||||
backgroundColor: `${theme.neutral100}80`, // 80% 透明度
|
|
||||||
}];
|
|
||||||
default:
|
|
||||||
return [...baseStyle, {
|
|
||||||
backgroundColor: `${theme.accentGreen}20`, // 20% 透明度
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBackButtonIconColor = () => {
|
|
||||||
switch (variant) {
|
|
||||||
case 'elevated':
|
|
||||||
return theme.primary;
|
|
||||||
case 'minimal':
|
|
||||||
return theme.textSecondary;
|
|
||||||
default:
|
|
||||||
return theme.onPrimary;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBorderStyle = () => {
|
const getBorderStyle = () => {
|
||||||
if (!showBottomBorder) return {};
|
if (!showBottomBorder) return {};
|
||||||
|
|
||||||
@@ -110,13 +78,13 @@ export function HeaderBar({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onBack}
|
onPress={onBack}
|
||||||
style={getBackButtonStyle()}
|
style={styles.backButton}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-back"
|
name="chevron-back"
|
||||||
size={20}
|
size={24}
|
||||||
color={getBackButtonIconColor()}
|
color={theme.text}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
@@ -157,17 +125,8 @@ const styles = StyleSheet.create({
|
|||||||
backButton: {
|
backButton: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 16,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: {
|
|
||||||
width: 0,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 2,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
},
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export const ROUTES = {
|
|||||||
// 营养相关路由
|
// 营养相关路由
|
||||||
NUTRITION_RECORDS: '/nutrition/records',
|
NUTRITION_RECORDS: '/nutrition/records',
|
||||||
|
|
||||||
|
// 任务相关路由
|
||||||
|
TASK_DETAIL: '/task-detail',
|
||||||
|
|
||||||
// 目标管理路由 (已移至tab中)
|
// 目标管理路由 (已移至tab中)
|
||||||
// GOAL_MANAGEMENT: '/goal-management',
|
// GOAL_MANAGEMENT: '/goal-management',
|
||||||
} as const;
|
} as const;
|
||||||
@@ -56,6 +59,9 @@ export const ROUTE_PARAMS = {
|
|||||||
// 文章参数
|
// 文章参数
|
||||||
ARTICLE_ID: 'id',
|
ARTICLE_ID: 'id',
|
||||||
|
|
||||||
|
// 任务参数
|
||||||
|
TASK_ID: 'taskId',
|
||||||
|
|
||||||
// 重定向参数
|
// 重定向参数
|
||||||
REDIRECT_TO: 'redirectTo',
|
REDIRECT_TO: 'redirectTo',
|
||||||
REDIRECT_PARAMS: 'redirectParams',
|
REDIRECT_PARAMS: 'redirectParams',
|
||||||
|
|||||||
Reference in New Issue
Block a user