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',