From b807e498ed41c54ba3fbc0d6aa0a4cba305ad0b6 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sat, 23 Aug 2025 13:33:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建任务详情页面,展示任务的详细信息,包括状态、描述、优先级和进度 - 实现任务完成和跳过功能,用户可通过按钮操作更新任务状态 - 添加评论功能,用户可以对任务进行评论并发送 - 优化任务卡片,点击后可跳转至任务详情页面 - 更新相关样式,确保界面一致性和美观性 --- app/task-detail.tsx | 649 ++++++++++++++++++++++++++++++++ components/TaskCard.tsx | 38 +- components/TaskProgressCard.tsx | 5 +- components/ui/HeaderBar.tsx | 47 +-- constants/Routes.ts | 6 + 5 files changed, 680 insertions(+), 65 deletions(-) create mode 100644 app/task-detail.tsx diff --git a/app/task-detail.tsx b/app/task-detail.tsx new file mode 100644 index 0000000..d290d80 --- /dev/null +++ b/app/task-detail.tsx @@ -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 ( + + router.back()} + /> + + 加载中... + + + ); + } + + if (!task) { + return ( + + router.back()} + /> + + 任务不存在 + + + ); + } + + return ( + + {/* 使用HeaderBar组件 */} + router.back()} + right={ + task.status !== 'completed' && task.status !== 'skipped' && task.currentCount < task.targetCount ? ( + + + + ) : undefined + } + /> + + + {/* 任务标题和创建时间 */} + + {task.title} + + {formatDate(task.startDate)} + + + + {/* 状态标签 */} + + + {getStatusText(task.status)} + + + + {/* 描述区域 */} + + 描述 + + {task.description || '暂无描述'} + + + + {/* 优先级和难度 */} + + + 优先级 + + + + + + + + 难度 + + + 非常简单 (少于一天) + + + + + {/* 任务进度信息 */} + + 进度 + + {/* 进度条 */} + + 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 && ( + + )} + {/* 进度文本 */} + + {task.currentCount}/{task.targetCount} + + + + {/* 进度详细信息 */} + + + 目标次数 + {task.targetCount} + + + 已完成 + {task.currentCount} + + + 剩余天数 + {task.daysRemaining} + + + + + {/* 评论区域 */} + + 评论区域 + + + + + + + + + + + + + + {/* 底部操作按钮 */} + {task.status !== 'completed' && task.status !== 'skipped' && task.currentCount < task.targetCount && ( + + + + 跳过任务 + + + )} + + + ); +} + +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', + }, +}); diff --git a/components/TaskCard.tsx b/components/TaskCard.tsx index 36ab748..9994f88 100644 --- a/components/TaskCard.tsx +++ b/components/TaskCard.tsx @@ -5,6 +5,7 @@ import { useColorScheme } from '@/hooks/useColorScheme'; import { completeTask, skipTask } from '@/store/tasksSlice'; import { TaskListItem } from '@/types/goals'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import { useRouter } from 'expo-router'; import React from 'react'; import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -19,6 +20,7 @@ export const TaskCard: React.FC = ({ const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); const { showConfirm } = useGlobalDialog(); + const router = useRouter(); const getStatusText = (status: string) => { @@ -123,6 +125,10 @@ export const TaskCard: React.FC = ({ ); }; + const handleTaskPress = () => { + router.push(`/task-detail?taskId=${task.id}`); + }; + const renderActionIcons = () => { if (task.status === 'completed' || task.status === 'overdue' || task.status === 'skipped') { return null; @@ -158,7 +164,7 @@ export const TaskCard: React.FC = ({ return ( {}} + onPress={handleTaskPress} activeOpacity={0.7} > {/* 头部区域 */} @@ -205,23 +211,21 @@ export const TaskCard: React.FC = ({ )} {/* 进度百分比文本 */} - {Math.round(task.progressPercentage)}% + {task.currentCount}/{task.targetCount} {/* 底部信息 */} - {/* 模拟团队成员头像 */} + {/* 团队成员头像 */} - - A - - - B - - - C + + @@ -395,17 +399,15 @@ const styles = StyleSheet.create({ avatar: { width: 24, height: 24, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', + borderRadius: 24, marginRight: -8, borderWidth: 2, borderColor: '#FFFFFF', + overflow: 'hidden', }, - avatarText: { - fontSize: 10, - fontWeight: '600', - color: '#FFFFFF', + avatarImage: { + width: '100%', + height: '100%', }, infoSection: { flexDirection: 'row', diff --git a/components/TaskProgressCard.tsx b/components/TaskProgressCard.tsx index 92f5a94..dd981dd 100644 --- a/components/TaskProgressCard.tsx +++ b/components/TaskProgressCard.tsx @@ -22,8 +22,7 @@ export const TaskProgressCard: React.FC = ({ {/* 标题区域 */} - 任务状态统计 - 各状态任务数量分布 + 统计 {headerButtons && ( @@ -82,7 +81,7 @@ const styles = StyleSheet.create({ marginBottom: 20, flexDirection: 'row', justifyContent: 'space-between', - alignItems: 'flex-start', + alignItems: 'center', }, titleContainer: { flex: 1, diff --git a/components/ui/HeaderBar.tsx b/components/ui/HeaderBar.tsx index 8eb3392..d763d81 100644 --- a/components/ui/HeaderBar.tsx +++ b/components/ui/HeaderBar.tsx @@ -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 = () => { if (!showBottomBorder) return {}; @@ -110,13 +78,13 @@ export function HeaderBar({ ) : ( @@ -157,17 +125,8 @@ const styles = StyleSheet.create({ backButton: { width: 32, height: 32, - borderRadius: 16, alignItems: 'center', justifyContent: 'center', - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 1, - }, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, }, titleContainer: { flex: 1, diff --git a/constants/Routes.ts b/constants/Routes.ts index ca57a47..d1f340f 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -37,6 +37,9 @@ export const ROUTES = { // 营养相关路由 NUTRITION_RECORDS: '/nutrition/records', + // 任务相关路由 + TASK_DETAIL: '/task-detail', + // 目标管理路由 (已移至tab中) // GOAL_MANAGEMENT: '/goal-management', } as const; @@ -56,6 +59,9 @@ export const ROUTE_PARAMS = { // 文章参数 ARTICLE_ID: 'id', + // 任务参数 + TASK_ID: 'taskId', + // 重定向参数 REDIRECT_TO: 'redirectTo', REDIRECT_PARAMS: 'redirectParams',