feat: 移除目标管理功能模块
删除了完整的目标管理功能,包括目标创建、编辑、任务管理等相关页面和组件。同时移除了相关的API服务、Redux状态管理、类型定义和通知功能。应用版本从1.0.20升级到1.0.21。
This commit is contained in:
@@ -22,7 +22,6 @@ type TabConfig = {
|
||||
const TAB_CONFIGS: Record<string, TabConfig> = {
|
||||
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
||||
fasting: { icon: 'timer', title: '断食' },
|
||||
goals: { icon: 'flag.fill', title: '习惯' },
|
||||
challenges: { icon: 'trophy.fill', title: '挑战' },
|
||||
personal: { icon: 'person.fill', title: '个人' },
|
||||
};
|
||||
@@ -38,7 +37,6 @@ export default function TabLayout() {
|
||||
const routeMap: Record<string, string> = {
|
||||
statistics: ROUTES.TAB_STATISTICS,
|
||||
fasting: ROUTES.TAB_FASTING,
|
||||
goals: ROUTES.TAB_GOALS,
|
||||
challenges: ROUTES.TAB_CHALLENGES,
|
||||
personal: ROUTES.TAB_PERSONAL,
|
||||
};
|
||||
@@ -182,10 +180,6 @@ export default function TabLayout() {
|
||||
<Icon sf="timer" drawable="custom_android_drawable" />
|
||||
<Label>断食</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="goals">
|
||||
<Icon sf="flag.fill" drawable="custom_settings_drawable" />
|
||||
<Label>习惯</Label>
|
||||
</NativeTabs.Trigger>
|
||||
<NativeTabs.Trigger name="challenges">
|
||||
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
||||
<Label>挑战</Label>
|
||||
@@ -205,7 +199,6 @@ export default function TabLayout() {
|
||||
|
||||
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
||||
<Tabs.Screen name="fasting" options={{ title: '断食' }} />
|
||||
<Tabs.Screen name="goals" options={{ title: '习惯' }} />
|
||||
<Tabs.Screen name="challenges" options={{ title: '挑战' }} />
|
||||
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
||||
</Tabs>
|
||||
|
||||
@@ -1,920 +0,0 @@
|
||||
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||
import GoalTemplateModal from '@/components/GoalTemplateModal';
|
||||
import { GoalsPageGuide } from '@/components/GoalsPageGuide';
|
||||
import { GuideTestButton } from '@/components/GuideTestButton';
|
||||
import { TaskCard } from '@/components/TaskCard';
|
||||
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
|
||||
import { TaskProgressCard } from '@/components/TaskProgressCard';
|
||||
import { CreateGoalModal } from '@/components/model/CreateGoalModal';
|
||||
import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||
import { GoalTemplate } from '@/constants/goalTemplates';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
||||
import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice';
|
||||
import { CreateGoalRequest, TaskListItem } from '@/types/goals';
|
||||
import { checkGuideCompleted, markGuideCompleted } from '@/utils/guideHelpers';
|
||||
import { GoalNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Lottie from 'lottie-react-native';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Alert, FlatList, Image, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export default function GoalsScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||||
|
||||
const { showConfirm } = useGlobalDialog();
|
||||
|
||||
// Redux状态
|
||||
const {
|
||||
tasks,
|
||||
tasksLoading,
|
||||
tasksError,
|
||||
tasksPagination,
|
||||
completeError,
|
||||
skipError,
|
||||
} = useAppSelector((state) => state.tasks);
|
||||
|
||||
|
||||
const {
|
||||
createLoading,
|
||||
createError
|
||||
} = useAppSelector((state) => state.goals);
|
||||
|
||||
const userProfile = useAppSelector((state) => state.user.profile);
|
||||
|
||||
const [showTemplateModal, setShowTemplateModal] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
|
||||
const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗
|
||||
const [showGuide, setShowGuide] = useState(false); // 控制引导显示
|
||||
const [selectedTemplateData, setSelectedTemplateData] = useState<Partial<CreateGoalRequest> | undefined>();
|
||||
|
||||
// 庆祝动画引用
|
||||
const celebrationAnimationRef = useRef<CelebrationAnimationRef>(null);
|
||||
|
||||
// 页面聚焦时重新加载数据
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
console.log('useFocusEffect - loading tasks isLoggedIn', isLoggedIn);
|
||||
|
||||
if (isLoggedIn) {
|
||||
loadTasks();
|
||||
checkAndShowGuide();
|
||||
}
|
||||
}, [dispatch, isLoggedIn])
|
||||
);
|
||||
|
||||
// 检查并显示用户引导
|
||||
const checkAndShowGuide = async () => {
|
||||
try {
|
||||
const hasCompletedGuide = await checkGuideCompleted('GOALS_PAGE');
|
||||
if (!hasCompletedGuide) {
|
||||
// 延迟显示引导,确保页面完全加载
|
||||
setTimeout(() => {
|
||||
setShowGuide(true);
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查引导状态失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
|
||||
await dispatch(fetchTasks({
|
||||
startDate: dayjs().startOf('day').toISOString(),
|
||||
endDate: dayjs().endOf('day').toISOString(),
|
||||
})).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to load tasks:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
if (!isLoggedIn) return
|
||||
|
||||
await loadTasks();
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多任务
|
||||
const handleLoadMoreTasks = async () => {
|
||||
if (!isLoggedIn) return
|
||||
|
||||
if (tasksPagination.hasMore && !tasksLoading) {
|
||||
try {
|
||||
await dispatch(loadMoreTasks()).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to load more tasks:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理错误提示
|
||||
useEffect(() => {
|
||||
|
||||
if (tasksError) {
|
||||
Alert.alert('错误', tasksError);
|
||||
dispatch(clearTaskErrors());
|
||||
}
|
||||
if (createError) {
|
||||
Alert.alert('创建失败', createError);
|
||||
dispatch(clearErrors());
|
||||
}
|
||||
if (completeError) {
|
||||
Alert.alert('完成失败', completeError);
|
||||
dispatch(clearTaskErrors());
|
||||
}
|
||||
if (skipError) {
|
||||
Alert.alert('跳过失败', skipError);
|
||||
dispatch(clearTaskErrors());
|
||||
}
|
||||
}, [tasksError, createError, completeError, skipError, dispatch]);
|
||||
|
||||
// 重置弹窗表单数据
|
||||
const handleModalSuccess = () => {
|
||||
// 不需要在这里改变 modalKey,因为弹窗已经关闭了
|
||||
// 下次打开时会自动使用新的 modalKey
|
||||
setSelectedTemplateData(undefined);
|
||||
};
|
||||
|
||||
// 处理模板选择
|
||||
const handleSelectTemplate = (template: GoalTemplate) => {
|
||||
setSelectedTemplateData(template.data);
|
||||
setShowTemplateModal(false);
|
||||
setModalKey(prev => prev + 1);
|
||||
setShowCreateModal(true);
|
||||
};
|
||||
|
||||
// 处理创建自定义目标
|
||||
const handleCreateCustomGoal = () => {
|
||||
setSelectedTemplateData(undefined);
|
||||
setShowTemplateModal(false);
|
||||
setModalKey(prev => prev + 1);
|
||||
setShowCreateModal(true);
|
||||
};
|
||||
|
||||
// 打开模板选择弹窗
|
||||
const handleOpenTemplateModal = () => {
|
||||
setSelectedTemplateData(undefined);
|
||||
setShowTemplateModal(true);
|
||||
};
|
||||
|
||||
// 创建目标处理函数
|
||||
const handleCreateGoal = async (goalData: CreateGoalRequest) => {
|
||||
try {
|
||||
await dispatch(createGoal(goalData)).unwrap();
|
||||
setShowCreateModal(false);
|
||||
|
||||
// 获取用户名
|
||||
const userName = userProfile?.name || '主人';
|
||||
|
||||
// 创建目标成功后,设置定时推送
|
||||
try {
|
||||
if (goalData.hasReminder) {
|
||||
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
|
||||
{
|
||||
title: goalData.title,
|
||||
repeatType: goalData.repeatType,
|
||||
frequency: goalData.frequency,
|
||||
hasReminder: goalData.hasReminder,
|
||||
reminderTime: goalData.reminderTime,
|
||||
customRepeatRule: goalData.customRepeatRule,
|
||||
startTime: goalData.startTime,
|
||||
},
|
||||
userName
|
||||
);
|
||||
console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds);
|
||||
}
|
||||
|
||||
} catch (notificationError) {
|
||||
console.error('创建目标定时推送失败:', notificationError);
|
||||
// 通知创建失败不影响目标创建的成功
|
||||
}
|
||||
|
||||
// 使用确认弹窗显示成功消息
|
||||
showConfirm(
|
||||
{
|
||||
title: '目标创建成功',
|
||||
message: '恭喜!您的目标已成功创建。系统将自动生成相应的任务,帮助您实现目标。',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
icon: 'checkmark-circle',
|
||||
iconColor: '#10B981',
|
||||
},
|
||||
() => {
|
||||
// 用户点击确定后的回调
|
||||
console.log('用户确认了目标创建成功');
|
||||
}
|
||||
);
|
||||
|
||||
// 创建目标后重新加载任务列表
|
||||
loadTasks();
|
||||
} catch (error) {
|
||||
// 错误已在useEffect中处理
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 导航到任务列表页面
|
||||
const handleNavigateToTasks = () => {
|
||||
pushIfAuthedElseLogin('/task-list');
|
||||
};
|
||||
|
||||
// 计算各状态的任务数量
|
||||
const taskCounts = {
|
||||
all: tasks.length,
|
||||
pending: tasks.filter(task => task.status === 'pending').length,
|
||||
completed: tasks.filter(task => task.status === 'completed').length,
|
||||
skipped: tasks.filter(task => task.status === 'skipped').length,
|
||||
};
|
||||
|
||||
// 根据筛选条件过滤任务,并将已完成的任务放到最后
|
||||
const filteredTasks = React.useMemo(() => {
|
||||
let filtered: TaskListItem[] = [];
|
||||
|
||||
switch (selectedFilter) {
|
||||
case 'pending':
|
||||
filtered = tasks.filter(task => task.status === 'pending');
|
||||
break;
|
||||
case 'completed':
|
||||
filtered = tasks.filter(task => task.status === 'completed');
|
||||
break;
|
||||
case 'skipped':
|
||||
filtered = tasks.filter(task => task.status === 'skipped');
|
||||
break;
|
||||
default:
|
||||
filtered = tasks;
|
||||
break;
|
||||
}
|
||||
|
||||
// 对所有筛选结果进行排序:已完成的任务放到最后
|
||||
return [...filtered].sort((a, b) => {
|
||||
// 如果a已完成而b未完成,a排在后面
|
||||
if (a.status === 'completed' && b.status !== 'completed') {
|
||||
return 1;
|
||||
}
|
||||
// 如果b已完成而a未完成,b排在后面
|
||||
if (b.status === 'completed' && a.status !== 'completed') {
|
||||
return -1;
|
||||
}
|
||||
// 如果都已完成或都未完成,保持原有顺序
|
||||
return 0;
|
||||
});
|
||||
}, [tasks, selectedFilter]);
|
||||
|
||||
// 处理筛选变化
|
||||
const handleFilterChange = (filter: TaskFilterType) => {
|
||||
setSelectedFilter(filter);
|
||||
};
|
||||
|
||||
// 处理引导完成
|
||||
const handleGuideComplete = async () => {
|
||||
try {
|
||||
await markGuideCompleted('GOALS_PAGE');
|
||||
setShowGuide(false);
|
||||
} catch (error) {
|
||||
console.error('保存引导状态失败:', error);
|
||||
setShowGuide(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理任务完成
|
||||
const handleTaskCompleted = (completedTask: TaskListItem) => {
|
||||
// 触发震动反馈
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
}
|
||||
|
||||
// 播放庆祝动画
|
||||
celebrationAnimationRef.current?.play();
|
||||
|
||||
console.log(`任务 "${completedTask.title}" 已完成,播放庆祝动画`);
|
||||
};
|
||||
|
||||
// 渲染任务项
|
||||
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
||||
<TaskCard
|
||||
task={item}
|
||||
onTaskCompleted={handleTaskCompleted}
|
||||
/>
|
||||
);
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmptyState = () => {
|
||||
// 未登录状态下的引导
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View style={styles.emptyStateLogin}>
|
||||
<LinearGradient
|
||||
colors={['#F0F9FF', '#FEFEFE', '#F0F9FF']}
|
||||
style={styles.emptyStateLoginBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
|
||||
<View style={styles.emptyStateLoginContent}>
|
||||
{/* 清新的图标设计 */}
|
||||
<View style={styles.emptyStateLoginIconContainer}>
|
||||
<LinearGradient
|
||||
colors={[colorTokens.primary, '#9B8AFB']}
|
||||
style={styles.emptyStateLoginIconGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<MaterialIcons name="person-outline" size={32} color="#FFFFFF" />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
|
||||
{/* 主标题 */}
|
||||
<Text style={[styles.emptyStateLoginTitle, { color: colorTokens.text }]}>
|
||||
开启您的健康之旅
|
||||
</Text>
|
||||
|
||||
{/* 副标题 */}
|
||||
<Text style={[styles.emptyStateLoginSubtitle, { color: colorTokens.textSecondary }]}>
|
||||
登录后即可创建个人目标,让我们一起建立健康的生活习惯
|
||||
</Text>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[styles.emptyStateLoginButton, { backgroundColor: colorTokens.primary }]}
|
||||
onPress={() => pushIfAuthedElseLogin('/goals')}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colorTokens.primary, '#9B8AFB']}
|
||||
style={styles.emptyStateLoginButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Text style={styles.emptyStateLoginButtonText}>立即登录</Text>
|
||||
<MaterialIcons name="arrow-forward" size={18} color="#FFFFFF" />
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 已登录但无任务的状态
|
||||
let title = '暂无任务';
|
||||
let subtitle = '创建目标后,系统会自动生成相应的任务';
|
||||
|
||||
if (selectedFilter === 'pending') {
|
||||
title = '暂无待完成的任务';
|
||||
subtitle = '当前没有待完成的任务';
|
||||
} else if (selectedFilter === 'completed') {
|
||||
title = '暂无已完成的任务';
|
||||
subtitle = '完成一些任务后,它们会显示在这里';
|
||||
} else if (selectedFilter === 'skipped') {
|
||||
title = '暂无已跳过的任务';
|
||||
subtitle = '跳过一些任务后,它们会显示在这里';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Image
|
||||
source={require('@/assets/images/task/ImageEmpty.png')}
|
||||
style={styles.emptyStateImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染加载更多
|
||||
const renderLoadMore = () => {
|
||||
if (!tasksPagination.hasMore) return null;
|
||||
return (
|
||||
<View style={styles.loadMoreContainer}>
|
||||
<Text style={[styles.loadMoreText, { color: colorTokens.textSecondary }]}>
|
||||
{tasksLoading ? '加载中...' : '上拉加载更多'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar
|
||||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['#F0F9FF', '#E0F2FE']}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: '#7A5AF8',
|
||||
height: 233,
|
||||
borderBottomLeftRadius: 24,
|
||||
borderBottomRightRadius: 24,
|
||||
}}>
|
||||
{/* 右下角Lottie动画 */}
|
||||
<Lottie
|
||||
source={require('@/assets/lottie/Goal.json')}
|
||||
style={styles.bottomRightImage}
|
||||
autoPlay
|
||||
loop
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
{/* 标题区域 */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
|
||||
习惯养成
|
||||
</Text>
|
||||
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
|
||||
自律让我更健康
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 任务进度卡片 */}
|
||||
<View >
|
||||
<TaskProgressCard
|
||||
tasks={tasks}
|
||||
headerButtons={
|
||||
<View style={styles.cardHeaderButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.cardGoalsButton, { borderColor: colorTokens.primary }]}
|
||||
onPress={handleNavigateToTasks}
|
||||
>
|
||||
<Text style={[styles.cardGoalsButtonText, { color: colorTokens.primary }]}>
|
||||
历史
|
||||
</Text>
|
||||
<MaterialIcons name="list" size={16} color={colorTokens.primary} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.cardAddButton, { backgroundColor: colorTokens.primary }]}
|
||||
onPress={handleOpenTemplateModal}
|
||||
>
|
||||
<Text style={styles.cardAddButtonText}>+</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 任务筛选标签 */}
|
||||
<TaskFilterTabs
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
taskCounts={taskCounts}
|
||||
/>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<View style={styles.taskListContainer}>
|
||||
<FlatList
|
||||
data={filteredTasks}
|
||||
renderItem={renderTaskItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.taskList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={['#0EA5E9']}
|
||||
tintColor="#0EA5E9"
|
||||
/>
|
||||
}
|
||||
onEndReached={handleLoadMoreTasks}
|
||||
onEndReachedThreshold={0.1}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
ListFooterComponent={renderLoadMore}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 目标模板选择弹窗 */}
|
||||
<GoalTemplateModal
|
||||
visible={showTemplateModal}
|
||||
onClose={() => setShowTemplateModal(false)}
|
||||
onSelectTemplate={handleSelectTemplate}
|
||||
onCreateCustom={handleCreateCustomGoal}
|
||||
/>
|
||||
|
||||
{/* 创建目标弹窗 */}
|
||||
<CreateGoalModal
|
||||
key={modalKey}
|
||||
visible={showCreateModal}
|
||||
onClose={() => {
|
||||
setShowCreateModal(false);
|
||||
setSelectedTemplateData(undefined);
|
||||
}}
|
||||
onSubmit={handleCreateGoal}
|
||||
onSuccess={handleModalSuccess}
|
||||
loading={createLoading}
|
||||
initialData={selectedTemplateData}
|
||||
/>
|
||||
|
||||
{/* 目标页面引导 */}
|
||||
<GoalsPageGuide
|
||||
visible={showGuide}
|
||||
onComplete={handleGuideComplete}
|
||||
tasks={tasks}
|
||||
/>
|
||||
|
||||
{/* 开发测试按钮 */}
|
||||
<GuideTestButton visible={__DEV__} />
|
||||
|
||||
{/* 目标通知测试按钮 */}
|
||||
{__DEV__ && (
|
||||
<TouchableOpacity
|
||||
style={styles.testButton}
|
||||
onPress={() => {
|
||||
// 这里可以导航到测试页面或显示测试弹窗
|
||||
Alert.alert(
|
||||
'目标通知测试',
|
||||
'选择要测试的通知类型',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '每日目标通知',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const userName = userProfile?.name || '';
|
||||
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
|
||||
{
|
||||
title: '每日运动目标',
|
||||
repeatType: 'daily',
|
||||
frequency: 1,
|
||||
hasReminder: true,
|
||||
reminderTime: '09:00',
|
||||
},
|
||||
userName
|
||||
);
|
||||
Alert.alert('成功', `每日目标通知已创建,ID: ${notificationIds.join(', ')}`);
|
||||
} catch (error) {
|
||||
Alert.alert('错误', `创建通知失败: ${error}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '每周目标通知',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const userName = userProfile?.name || '';
|
||||
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
|
||||
{
|
||||
title: '每周运动目标',
|
||||
repeatType: 'weekly',
|
||||
frequency: 1,
|
||||
hasReminder: true,
|
||||
reminderTime: '10:00',
|
||||
customRepeatRule: {
|
||||
weekdays: [1, 3, 5], // 周一、三、五
|
||||
},
|
||||
},
|
||||
userName
|
||||
);
|
||||
Alert.alert('成功', `每周目标通知已创建,ID: ${notificationIds.join(', ')}`);
|
||||
} catch (error) {
|
||||
Alert.alert('错误', `创建通知失败: ${error}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '目标达成通知',
|
||||
onPress: async () => {
|
||||
try {
|
||||
const userName = userProfile?.name || '';
|
||||
await GoalNotificationHelpers.sendGoalAchievementNotification(userName, '每日运动目标');
|
||||
Alert.alert('成功', '目标达成通知已发送');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', `发送通知失败: ${error}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '测试庆祝动画',
|
||||
onPress: () => {
|
||||
celebrationAnimationRef.current?.play();
|
||||
}
|
||||
},
|
||||
]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.testButtonText}>测试通知</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* 庆祝动画组件 */}
|
||||
<CelebrationAnimation ref={celebrationAnimationRef} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
opacity: 0.6,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: 'absolute',
|
||||
top: -20,
|
||||
right: -20,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.1,
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -15,
|
||||
left: -15,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.05,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
headerButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
goalsButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
pageTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
marginBottom: 4,
|
||||
},
|
||||
pageTitle2: {
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
color: '#FFFFFF',
|
||||
lineHeight: 24,
|
||||
},
|
||||
addButton: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
addButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 22,
|
||||
fontWeight: '600',
|
||||
lineHeight: 22,
|
||||
},
|
||||
|
||||
taskListContainer: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
taskList: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20, // 避让底部导航栏 + 额外间距
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyStateImage: {
|
||||
width: 223,
|
||||
height: 59,
|
||||
marginBottom: 20,
|
||||
},
|
||||
emptyStateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyStateSubtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
// 未登录空状态样式
|
||||
emptyStateLogin: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 80,
|
||||
position: 'relative',
|
||||
},
|
||||
emptyStateLoginBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 24,
|
||||
},
|
||||
emptyStateLoginContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
},
|
||||
emptyStateLoginIconContainer: {
|
||||
marginBottom: 24,
|
||||
shadowColor: '#7A5AF8',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 16,
|
||||
elevation: 8,
|
||||
},
|
||||
emptyStateLoginIconGradient: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
emptyStateLoginTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
emptyStateLoginSubtitle: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
textAlign: 'center',
|
||||
marginBottom: 32,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
emptyStateLoginButton: {
|
||||
borderRadius: 28,
|
||||
shadowColor: '#7A5AF8',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 12,
|
||||
elevation: 6,
|
||||
},
|
||||
emptyStateLoginButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 28,
|
||||
gap: 8,
|
||||
},
|
||||
emptyStateLoginButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
loadMoreContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
},
|
||||
loadMoreText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
bottomRightImage: {
|
||||
position: 'absolute',
|
||||
top: 40,
|
||||
right: 36,
|
||||
width: 120,
|
||||
height: 120,
|
||||
},
|
||||
// 任务进度卡片中的按钮样式
|
||||
cardHeaderButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
cardGoalsButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderWidth: 1,
|
||||
},
|
||||
cardGoalsListButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderWidth: 1,
|
||||
},
|
||||
cardGoalsButtonText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
cardAddButton: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cardAddButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
lineHeight: 18,
|
||||
},
|
||||
testButton: {
|
||||
position: 'absolute',
|
||||
top: 100,
|
||||
right: 20,
|
||||
backgroundColor: '#10B981',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
zIndex: 1000,
|
||||
},
|
||||
testButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
@@ -18,13 +18,14 @@ import { store } from '@/store';
|
||||
import { hydrateActiveSchedule, selectActiveFastingSchedule } from '@/store/fastingSlice';
|
||||
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
||||
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
||||
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
||||
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
||||
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||||
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
||||
import { ToastProvider } from '@/contexts/ToastContext';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { STORAGE_KEYS } from '@/services/api';
|
||||
@@ -32,7 +33,6 @@ import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||
import { fetchChallenges } from '@/store/challengesSlice';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
||||
|
||||
|
||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
@@ -242,8 +242,6 @@ export default function RootLayout() {
|
||||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile/edit" />
|
||||
<Stack.Screen name="profile/goals" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="goals-list" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="fasting/[planId]" options={{ headerShown: false }} />
|
||||
|
||||
<Stack.Screen name="ai-posture-assessment" />
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
import { GoalCard } from '@/components/GoalCard';
|
||||
import { CreateGoalModal } from '@/components/model/CreateGoalModal';
|
||||
import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { deleteGoal, fetchGoals, loadMoreGoals, updateGoal } from '@/store/goalsSlice';
|
||||
import { CreateGoalRequest, GoalListItem, UpdateGoalRequest } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, FlatList, RefreshControl, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function GoalsListScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const dispatch = useAppDispatch();
|
||||
const router = useRouter();
|
||||
|
||||
const { showConfirm } = useGlobalDialog();
|
||||
|
||||
// Redux状态
|
||||
const {
|
||||
goals,
|
||||
goalsLoading,
|
||||
goalsError,
|
||||
goalsPagination,
|
||||
updateLoading,
|
||||
updateError,
|
||||
} = useAppSelector((state) => state.goals);
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// 编辑目标相关状态
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingGoal, setEditingGoal] = useState<GoalListItem | null>(null);
|
||||
|
||||
// 页面聚焦时重新加载数据
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
console.log('useFocusEffect - loading goals');
|
||||
loadGoals();
|
||||
}, [dispatch])
|
||||
);
|
||||
|
||||
// 加载目标列表
|
||||
const loadGoals = async () => {
|
||||
try {
|
||||
await dispatch(fetchGoals({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
sortBy: 'createdAt',
|
||||
sortOrder: 'desc',
|
||||
})).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to load goals:', error);
|
||||
// 在开发模式下,如果API调用失败,使用模拟数据
|
||||
if (__DEV__) {
|
||||
console.log('Using mock data for development');
|
||||
// 添加模拟数据用于测试左滑删除功能
|
||||
const mockGoals: GoalListItem[] = [
|
||||
{
|
||||
id: 'mock-1',
|
||||
userId: 'test-user-1',
|
||||
title: '每日运动30分钟',
|
||||
repeatType: 'daily',
|
||||
frequency: 1,
|
||||
status: 'active',
|
||||
completedCount: 5,
|
||||
targetCount: 30,
|
||||
hasReminder: true,
|
||||
reminderTime: '09:00',
|
||||
category: '运动',
|
||||
priority: 5,
|
||||
startDate: '2024-01-01',
|
||||
startTime: 900,
|
||||
endTime: 1800,
|
||||
progressPercentage: 17,
|
||||
},
|
||||
{
|
||||
id: 'mock-2',
|
||||
userId: 'test-user-1',
|
||||
title: '每天喝8杯水',
|
||||
repeatType: 'daily',
|
||||
frequency: 8,
|
||||
status: 'active',
|
||||
completedCount: 6,
|
||||
targetCount: 8,
|
||||
hasReminder: true,
|
||||
reminderTime: '10:00',
|
||||
category: '健康',
|
||||
priority: 8,
|
||||
startDate: '2024-01-01',
|
||||
startTime: 600,
|
||||
endTime: 2200,
|
||||
progressPercentage: 75,
|
||||
},
|
||||
{
|
||||
id: 'mock-3',
|
||||
userId: 'test-user-1',
|
||||
title: '每周读书2小时',
|
||||
repeatType: 'weekly',
|
||||
frequency: 2,
|
||||
status: 'paused',
|
||||
completedCount: 1,
|
||||
targetCount: 2,
|
||||
hasReminder: false,
|
||||
category: '学习',
|
||||
priority: 3,
|
||||
startDate: '2024-01-01',
|
||||
startTime: 800,
|
||||
endTime: 2000,
|
||||
progressPercentage: 50,
|
||||
},
|
||||
];
|
||||
|
||||
// 直接更新 Redux 状态(仅用于开发测试)
|
||||
dispatch({
|
||||
type: 'goals/fetchGoals/fulfilled',
|
||||
payload: {
|
||||
query: { page: 1, pageSize: 20, sortBy: 'createdAt', sortOrder: 'desc' },
|
||||
response: {
|
||||
list: mockGoals,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: mockGoals.length,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await loadGoals();
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多目标
|
||||
const handleLoadMoreGoals = async () => {
|
||||
if (goalsPagination.hasMore && !goalsLoading) {
|
||||
try {
|
||||
await dispatch(loadMoreGoals()).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to load more goals:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除目标
|
||||
const handleDeleteGoal = async (goalId: string) => {
|
||||
try {
|
||||
await dispatch(deleteGoal(goalId)).unwrap();
|
||||
// 删除成功,Redux 会自动更新状态
|
||||
} catch (error) {
|
||||
console.error('Failed to delete goal:', error);
|
||||
Alert.alert('错误', '删除目标失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理错误提示
|
||||
useEffect(() => {
|
||||
if (goalsError) {
|
||||
Alert.alert('错误', goalsError);
|
||||
}
|
||||
if (updateError) {
|
||||
Alert.alert('更新失败', updateError);
|
||||
}
|
||||
}, [goalsError, updateError]);
|
||||
|
||||
|
||||
// 根据筛选条件过滤目标
|
||||
const filteredGoals = useMemo(() => {
|
||||
return goals;
|
||||
}, [goals]);
|
||||
|
||||
|
||||
|
||||
// 处理目标点击
|
||||
const handleGoalPress = (goal: GoalListItem) => {
|
||||
setEditingGoal(goal);
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
// 将 GoalListItem 转换为 CreateGoalRequest 格式
|
||||
const convertGoalToModalData = (goal: GoalListItem): Partial<CreateGoalRequest> => {
|
||||
return {
|
||||
title: goal.title,
|
||||
description: goal.description,
|
||||
repeatType: goal.repeatType,
|
||||
frequency: goal.frequency,
|
||||
category: goal.category,
|
||||
priority: goal.priority,
|
||||
hasReminder: goal.hasReminder,
|
||||
reminderTime: goal.reminderTime,
|
||||
customRepeatRule: goal.customRepeatRule,
|
||||
endDate: goal.endDate,
|
||||
};
|
||||
};
|
||||
|
||||
// 处理更新目标
|
||||
const handleUpdateGoal = async (goalId: string, goalData: UpdateGoalRequest) => {
|
||||
try {
|
||||
await dispatch(updateGoal({ goalId, goalData })).unwrap();
|
||||
setShowEditModal(false);
|
||||
setEditingGoal(null);
|
||||
|
||||
// 使用全局弹窗显示成功消息
|
||||
showConfirm(
|
||||
{
|
||||
title: '目标更新成功',
|
||||
message: '恭喜!您的目标已成功更新。',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
icon: 'checkmark-circle',
|
||||
iconColor: '#10B981',
|
||||
},
|
||||
() => {
|
||||
console.log('用户确认了目标更新成功');
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to update goal:', error);
|
||||
Alert.alert('错误', '更新目标失败,请重试');
|
||||
// 更新失败时不关闭弹窗,保持编辑状态
|
||||
}
|
||||
};
|
||||
|
||||
// 处理编辑弹窗关闭
|
||||
const handleCloseEditModal = () => {
|
||||
setShowEditModal(false);
|
||||
setEditingGoal(null);
|
||||
};
|
||||
|
||||
// 渲染目标项
|
||||
const renderGoalItem = ({ item }: { item: GoalListItem }) => (
|
||||
<GoalCard
|
||||
goal={item}
|
||||
onPress={handleGoalPress}
|
||||
onDelete={handleDeleteGoal}
|
||||
showStatus={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmptyState = () => {
|
||||
let title = '暂无目标';
|
||||
let subtitle = '创建您的第一个目标,开始您的健康之旅';
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<MaterialIcons name="flag" size={64} color="#D1D5DB" />
|
||||
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.createButton, { backgroundColor: colorTokens.primary }]}
|
||||
onPress={() => router.push('/(tabs)/goals')}
|
||||
>
|
||||
<Text style={styles.createButtonText}>创建目标</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 渲染加载更多
|
||||
const renderLoadMore = () => {
|
||||
if (!goalsPagination.hasMore) return null;
|
||||
return (
|
||||
<View style={styles.loadMoreContainer}>
|
||||
<Text style={[styles.loadMoreText, { color: colorTokens.textSecondary }]}>
|
||||
{goalsLoading ? '加载中...' : '上拉加载更多'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar
|
||||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['#F8FAFC', '#F1F5F9']}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
|
||||
<View style={styles.content}>
|
||||
{/* 标题区域 */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={colorTokens.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.pageTitle, { color: colorTokens.text }]}>
|
||||
目标列表
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={() => router.push('/(tabs)/goals')}
|
||||
>
|
||||
<MaterialIcons name="add" size={24} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
||||
{/* 目标列表 */}
|
||||
<View style={styles.goalsListContainer}>
|
||||
<FlatList
|
||||
data={filteredGoals}
|
||||
renderItem={renderGoalItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.goalsList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={['#7A5AF8']}
|
||||
tintColor="#7A5AF8"
|
||||
/>
|
||||
}
|
||||
onEndReached={handleLoadMoreGoals}
|
||||
onEndReachedThreshold={0.1}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
ListFooterComponent={renderLoadMore}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 编辑目标弹窗 */}
|
||||
{editingGoal && (
|
||||
<CreateGoalModal
|
||||
visible={showEditModal}
|
||||
onClose={handleCloseEditModal}
|
||||
onSubmit={() => {}} // 编辑模式下不使用这个回调
|
||||
onUpdate={handleUpdateGoal}
|
||||
loading={updateLoading}
|
||||
initialData={convertGoalToModalData(editingGoal)}
|
||||
editGoalId={editingGoal.id}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
pageTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
},
|
||||
addButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#7A5AF8',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
goalsListContainer: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
goalsList: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 20,
|
||||
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 80,
|
||||
},
|
||||
emptyStateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyStateSubtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
marginBottom: 24,
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
createButton: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 20,
|
||||
},
|
||||
createButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
loadMoreContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
},
|
||||
loadMoreText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
@@ -1,659 +0,0 @@
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -1,289 +0,0 @@
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { TaskCard } from '@/components/TaskCard';
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import { tasksApi } from '@/services/tasksApi';
|
||||
import { TaskListItem } from '@/types/goals';
|
||||
import { getTodayIndexInMonth } from '@/utils/date';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
export default function GoalsDetailScreen() {
|
||||
const safeAreaTop = useSafeAreaTop()
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const router = useRouter();
|
||||
|
||||
// 本地状态管理
|
||||
const [tasks, setTasks] = useState<TaskListItem[]>([]);
|
||||
const [tasksLoading, setTasksLoading] = useState(false);
|
||||
const [tasksError, setTasksError] = useState<string | null>(null);
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// 日期选择器相关状态
|
||||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = async (targetDate?: Date) => {
|
||||
try {
|
||||
setTasksLoading(true);
|
||||
setTasksError(null);
|
||||
|
||||
const dateToUse = targetDate || selectedDate;
|
||||
console.log('Loading tasks for date:', dayjs(dateToUse).format('YYYY-MM-DD'));
|
||||
|
||||
const response = await tasksApi.getTasks({
|
||||
startDate: dayjs(dateToUse).startOf('day').toISOString(),
|
||||
endDate: dayjs(dateToUse).endOf('day').toISOString(),
|
||||
});
|
||||
|
||||
console.log('Tasks API response:', response);
|
||||
setTasks(response.list || []);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load tasks:', error);
|
||||
setTasksError(error.message || '获取任务列表失败');
|
||||
setTasks([]);
|
||||
} finally {
|
||||
setTasksLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面聚焦时重新加载数据
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
console.log('useFocusEffect - loading tasks');
|
||||
loadTasks();
|
||||
}, [])
|
||||
);
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await loadTasks();
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理错误提示
|
||||
useEffect(() => {
|
||||
if (tasksError) {
|
||||
Alert.alert('错误', tasksError);
|
||||
setTasksError(null);
|
||||
}
|
||||
}, [tasksError]);
|
||||
|
||||
|
||||
|
||||
// 日期选择处理
|
||||
const onSelectDate = async (index: number, date: Date) => {
|
||||
console.log('Date selected:', dayjs(date).format('YYYY-MM-DD'));
|
||||
setSelectedIndex(index);
|
||||
setSelectedDate(date);
|
||||
// 重新加载对应日期的任务数据
|
||||
await loadTasks(date);
|
||||
};
|
||||
|
||||
// 根据选中日期筛选任务,并将已完成的任务放到最后
|
||||
const filteredTasks = useMemo(() => {
|
||||
const selected = dayjs(selectedDate);
|
||||
const filtered = tasks.filter(task => {
|
||||
if (task.status === 'skipped') return false;
|
||||
const taskDate = dayjs(task.startDate);
|
||||
return taskDate.isSame(selected, 'day');
|
||||
});
|
||||
|
||||
// 对筛选结果进行排序:已完成的任务放到最后
|
||||
return [...filtered].sort((a, b) => {
|
||||
const aCompleted = a.status === 'completed';
|
||||
const bCompleted = b.status === 'completed';
|
||||
|
||||
// 如果a已完成而b未完成,a排在后面
|
||||
if (aCompleted && !bCompleted) {
|
||||
return 1;
|
||||
}
|
||||
// 如果b已完成而a未完成,b排在后面
|
||||
if (bCompleted && !aCompleted) {
|
||||
return -1;
|
||||
}
|
||||
// 如果都已完成或都未完成,保持原有顺序
|
||||
return 0;
|
||||
});
|
||||
}, [selectedDate, tasks]);
|
||||
|
||||
const handleBackPress = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 渲染任务项
|
||||
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
||||
<TaskCard
|
||||
task={item}
|
||||
/>
|
||||
);
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmptyState = () => {
|
||||
const selectedDateStr = dayjs(selectedDate).format('YYYY年M月D日');
|
||||
|
||||
if (tasksLoading) {
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
|
||||
加载中...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
|
||||
暂无任务
|
||||
</Text>
|
||||
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
|
||||
{selectedDateStr} 没有任务安排
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
{/* 标题区域 */}
|
||||
<HeaderBar
|
||||
title="任务列表"
|
||||
onBack={handleBackPress}
|
||||
transparent={true}
|
||||
withSafeTop={false}
|
||||
/>
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['#F0F9FF', '#E0F2FE']}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* 装饰性圆圈 */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
|
||||
|
||||
<View style={{
|
||||
paddingTop: safeAreaTop
|
||||
}} />
|
||||
|
||||
<View style={styles.content}>
|
||||
|
||||
|
||||
{/* 日期选择器 */}
|
||||
<View style={styles.dateSelector}>
|
||||
<DateSelector
|
||||
selectedIndex={selectedIndex}
|
||||
onDateSelect={onSelectDate}
|
||||
showMonthTitle={true}
|
||||
disableFutureDates={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<View style={styles.taskListContainer}>
|
||||
<FlatList
|
||||
data={filteredTasks}
|
||||
renderItem={renderTaskItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.taskList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={['#0EA5E9']}
|
||||
tintColor="#0EA5E9"
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
opacity: 0.6,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: 'absolute',
|
||||
top: -20,
|
||||
right: -20,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.1,
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -15,
|
||||
left: -15,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.05,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
// 日期选择器样式
|
||||
dateSelector: {
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
taskListContainer: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
taskList: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyStateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyStateSubtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user