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> = {
|
const TAB_CONFIGS: Record<string, TabConfig> = {
|
||||||
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
statistics: { icon: 'chart.pie.fill', title: '健康' },
|
||||||
fasting: { icon: 'timer', title: '断食' },
|
fasting: { icon: 'timer', title: '断食' },
|
||||||
goals: { icon: 'flag.fill', title: '习惯' },
|
|
||||||
challenges: { icon: 'trophy.fill', title: '挑战' },
|
challenges: { icon: 'trophy.fill', title: '挑战' },
|
||||||
personal: { icon: 'person.fill', title: '个人' },
|
personal: { icon: 'person.fill', title: '个人' },
|
||||||
};
|
};
|
||||||
@@ -38,7 +37,6 @@ export default function TabLayout() {
|
|||||||
const routeMap: Record<string, string> = {
|
const routeMap: Record<string, string> = {
|
||||||
statistics: ROUTES.TAB_STATISTICS,
|
statistics: ROUTES.TAB_STATISTICS,
|
||||||
fasting: ROUTES.TAB_FASTING,
|
fasting: ROUTES.TAB_FASTING,
|
||||||
goals: ROUTES.TAB_GOALS,
|
|
||||||
challenges: ROUTES.TAB_CHALLENGES,
|
challenges: ROUTES.TAB_CHALLENGES,
|
||||||
personal: ROUTES.TAB_PERSONAL,
|
personal: ROUTES.TAB_PERSONAL,
|
||||||
};
|
};
|
||||||
@@ -182,10 +180,6 @@ export default function TabLayout() {
|
|||||||
<Icon sf="timer" drawable="custom_android_drawable" />
|
<Icon sf="timer" drawable="custom_android_drawable" />
|
||||||
<Label>断食</Label>
|
<Label>断食</Label>
|
||||||
</NativeTabs.Trigger>
|
</NativeTabs.Trigger>
|
||||||
<NativeTabs.Trigger name="goals">
|
|
||||||
<Icon sf="flag.fill" drawable="custom_settings_drawable" />
|
|
||||||
<Label>习惯</Label>
|
|
||||||
</NativeTabs.Trigger>
|
|
||||||
<NativeTabs.Trigger name="challenges">
|
<NativeTabs.Trigger name="challenges">
|
||||||
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
<Icon sf="trophy.fill" drawable="custom_android_drawable" />
|
||||||
<Label>挑战</Label>
|
<Label>挑战</Label>
|
||||||
@@ -205,7 +199,6 @@ export default function TabLayout() {
|
|||||||
|
|
||||||
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
<Tabs.Screen name="statistics" options={{ title: '健康' }} />
|
||||||
<Tabs.Screen name="fasting" options={{ title: '断食' }} />
|
<Tabs.Screen name="fasting" options={{ title: '断食' }} />
|
||||||
<Tabs.Screen name="goals" options={{ title: '习惯' }} />
|
|
||||||
<Tabs.Screen name="challenges" options={{ title: '挑战' }} />
|
<Tabs.Screen name="challenges" options={{ title: '挑战' }} />
|
||||||
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
<Tabs.Screen name="personal" options={{ title: '个人' }} />
|
||||||
</Tabs>
|
</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 { hydrateActiveSchedule, selectActiveFastingSchedule } from '@/store/fastingSlice';
|
||||||
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import { createWaterRecordAction } from '@/store/waterSlice';
|
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||||
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
|
||||||
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
||||||
|
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
||||||
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||||||
|
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
||||||
import { ToastProvider } from '@/contexts/ToastContext';
|
import { ToastProvider } from '@/contexts/ToastContext';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { STORAGE_KEYS } from '@/services/api';
|
import { STORAGE_KEYS } from '@/services/api';
|
||||||
@@ -32,7 +33,6 @@ import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
|||||||
import { fetchChallenges } from '@/store/challengesSlice';
|
import { fetchChallenges } from '@/store/challengesSlice';
|
||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
|
||||||
|
|
||||||
|
|
||||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
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="training-plan" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="profile/edit" />
|
<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="fasting/[planId]" options={{ headerShown: false }} />
|
||||||
|
|
||||||
<Stack.Screen name="ai-posture-assessment" />
|
<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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import { GoalListItem } from '@/types/goals';
|
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
||||||
import { Swipeable } from 'react-native-gesture-handler';
|
|
||||||
|
|
||||||
interface GoalCardProps {
|
|
||||||
goal: GoalListItem;
|
|
||||||
onPress?: (goal: GoalListItem) => void;
|
|
||||||
onDelete?: (goalId: string) => void;
|
|
||||||
showStatus?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GoalCard: React.FC<GoalCardProps> = ({
|
|
||||||
goal,
|
|
||||||
onPress,
|
|
||||||
onDelete,
|
|
||||||
showStatus = true
|
|
||||||
}) => {
|
|
||||||
const swipeableRef = useRef<Swipeable>(null);
|
|
||||||
|
|
||||||
// 获取重复类型显示文本
|
|
||||||
const getRepeatTypeText = (goal: GoalListItem) => {
|
|
||||||
switch (goal.repeatType) {
|
|
||||||
case 'daily':
|
|
||||||
return '每日';
|
|
||||||
case 'weekly':
|
|
||||||
return '每周';
|
|
||||||
case 'monthly':
|
|
||||||
return '每月';
|
|
||||||
default:
|
|
||||||
return '每日';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取目标状态显示文本
|
|
||||||
const getStatusText = (goal: GoalListItem) => {
|
|
||||||
switch (goal.status) {
|
|
||||||
case 'active':
|
|
||||||
return '进行中';
|
|
||||||
case 'paused':
|
|
||||||
return '已暂停';
|
|
||||||
case 'completed':
|
|
||||||
return '已完成';
|
|
||||||
case 'cancelled':
|
|
||||||
return '已取消';
|
|
||||||
default:
|
|
||||||
return '进行中';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取目标状态颜色
|
|
||||||
const getStatusColor = (goal: GoalListItem) => {
|
|
||||||
switch (goal.status) {
|
|
||||||
case 'active':
|
|
||||||
return '#10B981';
|
|
||||||
case 'paused':
|
|
||||||
return '#F59E0B';
|
|
||||||
case 'completed':
|
|
||||||
return '#3B82F6';
|
|
||||||
case 'cancelled':
|
|
||||||
return '#EF4444';
|
|
||||||
default:
|
|
||||||
return '#10B981';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取目标图标
|
|
||||||
const getGoalIcon = (goal: GoalListItem) => {
|
|
||||||
// 根据目标类别或标题返回不同的图标
|
|
||||||
const title = goal.title.toLowerCase();
|
|
||||||
const category = goal.category?.toLowerCase();
|
|
||||||
|
|
||||||
if (title.includes('运动') || title.includes('健身') || title.includes('跑步')) {
|
|
||||||
return 'fitness-center';
|
|
||||||
} else if (title.includes('喝水') || title.includes('饮水')) {
|
|
||||||
return 'local-drink';
|
|
||||||
} else if (title.includes('睡眠') || title.includes('睡觉')) {
|
|
||||||
return 'bedtime';
|
|
||||||
} else if (title.includes('学习') || title.includes('读书')) {
|
|
||||||
return 'school';
|
|
||||||
} else if (title.includes('冥想') || title.includes('放松')) {
|
|
||||||
return 'self-improvement';
|
|
||||||
} else if (title.includes('早餐') || title.includes('午餐') || title.includes('晚餐')) {
|
|
||||||
return 'restaurant';
|
|
||||||
} else {
|
|
||||||
return 'flag';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理删除操作
|
|
||||||
const handleDelete = () => {
|
|
||||||
Alert.alert(
|
|
||||||
'确认删除',
|
|
||||||
`确定要删除目标"${goal.title}"吗?此操作无法撤销。`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: '取消',
|
|
||||||
style: 'cancel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '删除',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: () => {
|
|
||||||
onDelete?.(goal.id);
|
|
||||||
swipeableRef.current?.close();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染删除按钮
|
|
||||||
const renderRightActions = () => {
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.deleteButton}
|
|
||||||
onPress={handleDelete}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<MaterialIcons style={{
|
|
||||||
marginBottom: 10
|
|
||||||
}} name="delete" size={24} color="#EF4444" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Swipeable
|
|
||||||
ref={swipeableRef}
|
|
||||||
renderRightActions={renderRightActions}
|
|
||||||
rightThreshold={40}
|
|
||||||
overshootRight={false}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.goalCard}
|
|
||||||
onPress={() => onPress?.(goal)}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
{/* 左侧图标 */}
|
|
||||||
<View style={styles.goalIcon}>
|
|
||||||
<MaterialIcons name={getGoalIcon(goal)} size={20} color="#7A5AF8" />
|
|
||||||
<View style={styles.iconStars}>
|
|
||||||
<View style={styles.star} />
|
|
||||||
<View style={styles.star} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 中间内容 */}
|
|
||||||
<View style={styles.goalContent}>
|
|
||||||
<Text style={styles.goalTitle} numberOfLines={1}>
|
|
||||||
{goal.title}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* 底部信息行 */}
|
|
||||||
<View style={styles.goalInfo}>
|
|
||||||
{/* 积分 */}
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<Text style={styles.infoText}>+1</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 目标数量 */}
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<Text style={styles.infoText}>
|
|
||||||
{goal.targetCount || goal.frequency}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 提醒图标(如果有提醒) */}
|
|
||||||
{goal.hasReminder && (
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<MaterialIcons name="notifications" size={12} color="#9CA3AF" />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 提醒时间(如果有提醒) */}
|
|
||||||
{goal.hasReminder && goal.reminderTime && (
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<Text style={styles.infoText}>{goal.reminderTime}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 重复图标 */}
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<MaterialIcons name="loop" size={12} color="#9CA3AF" />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 重复类型 */}
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<Text style={styles.infoText}>{getRepeatTypeText(goal)}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 结束日期 */}
|
|
||||||
{goal.endDate && (
|
|
||||||
<View style={styles.infoItem}>
|
|
||||||
<MaterialIcons
|
|
||||||
name="calendar-month"
|
|
||||||
size={12}
|
|
||||||
color="#9CA3AF"
|
|
||||||
style={{ marginRight: 4 }}
|
|
||||||
/>
|
|
||||||
<Text style={styles.infoText}>{goal.endDate}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧状态指示器 */}
|
|
||||||
{showStatus && (
|
|
||||||
<View style={[styles.statusIndicator, { backgroundColor: getStatusColor(goal) }]}>
|
|
||||||
<Text style={styles.statusText}>{getStatusText(goal)}</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</Swipeable>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
goalCard: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 16,
|
|
||||||
padding: 16,
|
|
||||||
marginBottom: 12,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.05,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 2,
|
|
||||||
},
|
|
||||||
goalIcon: {
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: 12,
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
iconStars: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: -2,
|
|
||||||
right: -2,
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 1,
|
|
||||||
},
|
|
||||||
star: {
|
|
||||||
width: 4,
|
|
||||||
height: 4,
|
|
||||||
borderRadius: 2,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
},
|
|
||||||
goalContent: {
|
|
||||||
flex: 1,
|
|
||||||
marginRight: 12,
|
|
||||||
},
|
|
||||||
goalTitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#1F2937',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
goalInfo: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
infoItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
infoText: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: '#9CA3AF',
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
statusIndicator: {
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
minWidth: 60,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: 10,
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
deleteButton: {
|
|
||||||
width: 60,
|
|
||||||
height: '100%',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
deleteButtonText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Colors } from '@/constants/Colors';
|
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import { Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
||||||
import { GoalCard, GoalItem } from './GoalCard';
|
|
||||||
|
|
||||||
interface GoalCarouselProps {
|
|
||||||
goals: GoalItem[];
|
|
||||||
onGoalPress?: (item: GoalItem) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
|
||||||
|
|
||||||
export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) {
|
|
||||||
const theme = useColorScheme() ?? 'light';
|
|
||||||
const colorTokens = Colors[theme];
|
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
|
||||||
|
|
||||||
if (!goals || goals.length === 0) {
|
|
||||||
return (
|
|
||||||
<View style={styles.emptyContainer}>
|
|
||||||
<Text style={[styles.emptyText, { color: colorTokens.textMuted }]}>
|
|
||||||
今天暂无目标
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<ScrollView
|
|
||||||
ref={scrollViewRef}
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
snapToInterval={(screenWidth - 60) * 0.65 + 16} // 卡片宽度 + 间距
|
|
||||||
snapToAlignment="start"
|
|
||||||
decelerationRate="fast"
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
style={styles.scrollView}
|
|
||||||
>
|
|
||||||
{goals.map((item, index) => (
|
|
||||||
<GoalCard
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
onPress={onGoalPress}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{/* 占位符,确保最后一张卡片有足够的滑动空间 */}
|
|
||||||
<View style={{ width: 20 }} />
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* 底部指示器 */}
|
|
||||||
{/* <View style={styles.indicatorContainer}>
|
|
||||||
{goals.map((_, index) => (
|
|
||||||
<View
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
styles.indicator,
|
|
||||||
{ backgroundColor: index === 0 ? colorTokens.primary : colorTokens.border }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View> */}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
},
|
|
||||||
scrollView: {
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
emptyContainer: {
|
|
||||||
height: 140,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginHorizontal: 20,
|
|
||||||
borderRadius: 20,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#E5E7EB',
|
|
||||||
borderStyle: 'dashed',
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
indicatorContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
indicator: {
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
import { TaskListItem } from '@/types/goals';
|
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Animated,
|
|
||||||
Dimensions,
|
|
||||||
Modal,
|
|
||||||
StatusBar,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native';
|
|
||||||
|
|
||||||
interface GoalsPageGuideProps {
|
|
||||||
visible: boolean;
|
|
||||||
onComplete: () => void;
|
|
||||||
tasks?: TaskListItem[]; // 添加任务数据,用于智能引导
|
|
||||||
}
|
|
||||||
|
|
||||||
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
||||||
|
|
||||||
// 计算精确的高亮位置
|
|
||||||
const calculateHighlightPosition = (stepIndex: number, hasTasks: boolean) => {
|
|
||||||
const baseTop = 120; // 状态栏 + 标题区域高度
|
|
||||||
const cardHeight = 180; // 任务进度卡片高度
|
|
||||||
const filterHeight = 60; // 筛选标签高度
|
|
||||||
const listHeight = 300; // 任务列表高度
|
|
||||||
|
|
||||||
switch (stepIndex) {
|
|
||||||
case 0: // 欢迎标题
|
|
||||||
return {
|
|
||||||
top: baseTop - 40,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
height: 60,
|
|
||||||
borderRadius: 12,
|
|
||||||
};
|
|
||||||
case 1: // 任务进度卡片
|
|
||||||
return {
|
|
||||||
top: baseTop + 20,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
height: cardHeight,
|
|
||||||
borderRadius: 16,
|
|
||||||
};
|
|
||||||
case 2: // 目标管理按钮(有任务时)
|
|
||||||
if (hasTasks) {
|
|
||||||
return {
|
|
||||||
top: baseTop + 40,
|
|
||||||
right: 60,
|
|
||||||
width: 40,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
top: baseTop + 40,
|
|
||||||
right: 20,
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 3: // 创建新目标按钮(有任务时)
|
|
||||||
if (hasTasks) {
|
|
||||||
return {
|
|
||||||
top: baseTop + 40,
|
|
||||||
right: 20,
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null; // 没有这一步
|
|
||||||
}
|
|
||||||
case 4: // 任务筛选标签
|
|
||||||
return {
|
|
||||||
top: baseTop + cardHeight + 40,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
height: filterHeight,
|
|
||||||
borderRadius: 24,
|
|
||||||
};
|
|
||||||
case 5: // 任务列表
|
|
||||||
return {
|
|
||||||
top: baseTop + cardHeight + filterHeight + 60,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
height: listHeight,
|
|
||||||
borderRadius: 24,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GoalsPageGuide: React.FC<GoalsPageGuideProps> = ({
|
|
||||||
visible,
|
|
||||||
onComplete,
|
|
||||||
tasks = [],
|
|
||||||
}) => {
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
|
||||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
||||||
const scaleAnim = useRef(new Animated.Value(0.8)).current;
|
|
||||||
|
|
||||||
// 根据任务数据智能生成引导步骤
|
|
||||||
const generateSteps = () => {
|
|
||||||
const hasTasks = tasks.length > 0;
|
|
||||||
const hasCompletedTasks = tasks.some(task => task.status === 'completed');
|
|
||||||
const hasPendingTasks = tasks.some(task => task.status === 'pending');
|
|
||||||
|
|
||||||
const baseSteps = [
|
|
||||||
{
|
|
||||||
title: '欢迎来到目标页面',
|
|
||||||
description: '这里是您的目标管理中心,让我们一起来了解各个功能。',
|
|
||||||
icon: 'flag',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '任务进度统计',
|
|
||||||
description: '这里显示您当天的任务完成情况,包括待完成、已完成和已跳过的任务数量。',
|
|
||||||
icon: 'analytics',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 根据任务状态添加不同的引导内容
|
|
||||||
if (!hasTasks) {
|
|
||||||
baseSteps.push({
|
|
||||||
title: '创建您的第一个目标',
|
|
||||||
description: '点击加号按钮,创建您的第一个目标,系统会自动生成相应的任务。',
|
|
||||||
icon: 'add',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
baseSteps.push(
|
|
||||||
{
|
|
||||||
title: '目标管理',
|
|
||||||
description: '点击右上角的目标按钮,可以查看和管理您的所有目标。',
|
|
||||||
icon: 'flag',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建新目标',
|
|
||||||
description: '点击加号按钮,可以快速创建新的目标。',
|
|
||||||
icon: 'add',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
baseSteps.push({
|
|
||||||
title: '任务筛选',
|
|
||||||
description: '使用这些标签可以筛选查看不同状态的任务。',
|
|
||||||
icon: 'filter-list',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 根据任务状态调整任务列表的引导内容
|
|
||||||
if (!hasTasks) {
|
|
||||||
baseSteps.push({
|
|
||||||
title: '任务列表',
|
|
||||||
description: '创建目标后,您的任务将显示在这里。',
|
|
||||||
icon: 'list',
|
|
||||||
});
|
|
||||||
} else if (!hasPendingTasks && hasCompletedTasks) {
|
|
||||||
baseSteps.push({
|
|
||||||
title: '任务列表',
|
|
||||||
description: '您已完成所有任务!可以创建新目标或查看历史记录。',
|
|
||||||
icon: 'check-circle',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
baseSteps.push({
|
|
||||||
title: '任务列表',
|
|
||||||
description: '这里显示您的所有任务,可以标记完成或跳过。',
|
|
||||||
icon: 'list',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseSteps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const steps = generateSteps();
|
|
||||||
const hasTasks = tasks.length > 0;
|
|
||||||
const currentHighlightPosition = calculateHighlightPosition(currentStep, hasTasks);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(fadeAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 300,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(scaleAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 300,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
} else {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(fadeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(scaleAnim, {
|
|
||||||
toValue: 0.8,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start();
|
|
||||||
}
|
|
||||||
}, [visible, fadeAnim, scaleAnim]);
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
if (currentStep < steps.length - 1) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
} else {
|
|
||||||
handleComplete();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevious = () => {
|
|
||||||
if (currentStep > 0) {
|
|
||||||
setCurrentStep(currentStep - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComplete = () => {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(fadeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(scaleAnim, {
|
|
||||||
toValue: 0.8,
|
|
||||||
duration: 200,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
]).start(() => {
|
|
||||||
setCurrentStep(0);
|
|
||||||
onComplete();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
|
||||||
handleComplete();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!visible || !currentHighlightPosition) return null;
|
|
||||||
|
|
||||||
const currentStepData = steps[currentStep];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
transparent
|
|
||||||
animationType="none"
|
|
||||||
statusBarTranslucent
|
|
||||||
>
|
|
||||||
<StatusBar backgroundColor="rgba(0, 0, 0, 0.8)" barStyle="light-content" />
|
|
||||||
|
|
||||||
{/* 背景遮罩 */}
|
|
||||||
<View style={styles.overlay}>
|
|
||||||
{/* 高亮区域 */}
|
|
||||||
<View style={[styles.highlightArea, currentHighlightPosition]}>
|
|
||||||
<View style={[styles.highlightBorder, { borderRadius: currentHighlightPosition.borderRadius }]} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 引导内容 */}
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.guideContainer,
|
|
||||||
{
|
|
||||||
opacity: fadeAnim,
|
|
||||||
transform: [{ scale: scaleAnim }],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{/* 步骤指示器 */}
|
|
||||||
<View style={styles.stepIndicator}>
|
|
||||||
{steps.map((_, index) => (
|
|
||||||
<View
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
styles.stepDot,
|
|
||||||
index === currentStep && styles.stepDotActive,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 图标 */}
|
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<MaterialIcons
|
|
||||||
name={currentStepData.icon as any}
|
|
||||||
size={48}
|
|
||||||
color="#7A5AF8"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 标题 */}
|
|
||||||
<Text style={styles.title}>{currentStepData.title}</Text>
|
|
||||||
|
|
||||||
{/* 描述 */}
|
|
||||||
<Text style={styles.description}>{currentStepData.description}</Text>
|
|
||||||
|
|
||||||
{/* 按钮区域 */}
|
|
||||||
<View style={styles.buttonContainer}>
|
|
||||||
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
|
|
||||||
<Text style={styles.skipButtonText}>跳过</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.navigationButtons}>
|
|
||||||
{currentStep > 0 && (
|
|
||||||
<TouchableOpacity style={styles.previousButton} onPress={handlePrevious}>
|
|
||||||
<Text style={styles.previousButtonText}>回顾</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TouchableOpacity style={styles.nextButton} onPress={handleNext}>
|
|
||||||
<Text style={styles.nextButtonText}>
|
|
||||||
{currentStep === steps.length - 1 ? '完成' : '下一步'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
overlay: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
highlightArea: {
|
|
||||||
position: 'absolute',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
highlightBorder: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#7A5AF8',
|
|
||||||
backgroundColor: 'rgba(122, 90, 248, 0.08)',
|
|
||||||
shadowColor: '#7A5AF8',
|
|
||||||
shadowOffset: { width: 0, height: 0 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
guideContainer: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 120,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 20,
|
|
||||||
padding: 24,
|
|
||||||
alignItems: 'center',
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: {
|
|
||||||
width: 0,
|
|
||||||
height: 4,
|
|
||||||
},
|
|
||||||
shadowOpacity: 0.25,
|
|
||||||
shadowRadius: 12,
|
|
||||||
elevation: 8,
|
|
||||||
},
|
|
||||||
stepIndicator: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
stepDot: {
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: '#E5E7EB',
|
|
||||||
marginHorizontal: 4,
|
|
||||||
},
|
|
||||||
stepDotActive: {
|
|
||||||
backgroundColor: '#7A5AF8',
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
borderRadius: 40,
|
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#1F2937',
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#6B7280',
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 24,
|
|
||||||
marginBottom: 24,
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
navigationButtons: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
skipButton: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#E5E7EB',
|
|
||||||
},
|
|
||||||
skipButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#6B7280',
|
|
||||||
},
|
|
||||||
previousButton: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderRadius: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#7A5AF8',
|
|
||||||
backgroundColor: 'rgba(122, 90, 248, 0.1)',
|
|
||||||
},
|
|
||||||
previousButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#7A5AF8',
|
|
||||||
},
|
|
||||||
nextButton: {
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
paddingVertical: 12,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#7A5AF8',
|
|
||||||
},
|
|
||||||
nextButtonText: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
StyleSheet,
|
|
||||||
Alert,
|
Alert,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNotifications } from '../hooks/useNotifications';
|
import { useNotifications } from '../hooks/useNotifications';
|
||||||
import { ThemedText } from './ThemedText';
|
import { ThemedText } from './ThemedText';
|
||||||
import { ThemedView } from './ThemedView';
|
import { ThemedView } from './ThemedView';
|
||||||
import { DailySummaryTest } from './DailySummaryTest';
|
|
||||||
|
|
||||||
export const NotificationTest: React.FC = () => {
|
export const NotificationTest: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -23,9 +21,7 @@ export const NotificationTest: React.FC = () => {
|
|||||||
cancelAllNotifications,
|
cancelAllNotifications,
|
||||||
getAllScheduledNotifications,
|
getAllScheduledNotifications,
|
||||||
sendWorkoutReminder,
|
sendWorkoutReminder,
|
||||||
sendGoalAchievement,
|
|
||||||
sendMoodCheckinReminder,
|
sendMoodCheckinReminder,
|
||||||
debugNotificationStatus,
|
|
||||||
} = useNotifications();
|
} = useNotifications();
|
||||||
|
|
||||||
const [scheduledNotifications, setScheduledNotifications] = useState<any[]>([]);
|
const [scheduledNotifications, setScheduledNotifications] = useState<any[]>([]);
|
||||||
@@ -97,15 +93,6 @@ export const NotificationTest: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendGoalAchievement = async () => {
|
|
||||||
try {
|
|
||||||
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
|
||||||
Alert.alert('成功', '目标达成通知已发送');
|
|
||||||
} catch (error) {
|
|
||||||
Alert.alert('错误', '发送目标达成通知失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendMoodCheckinReminder = async () => {
|
const handleSendMoodCheckinReminder = async () => {
|
||||||
try {
|
try {
|
||||||
await sendMoodCheckinReminder('心情打卡', '记得记录今天的心情状态哦');
|
await sendMoodCheckinReminder('心情打卡', '记得记录今天的心情状态哦');
|
||||||
@@ -134,20 +121,11 @@ export const NotificationTest: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDebugNotificationStatus = async () => {
|
|
||||||
try {
|
|
||||||
await debugNotificationStatus();
|
|
||||||
Alert.alert('调试完成', '请查看控制台输出');
|
|
||||||
} catch (error) {
|
|
||||||
Alert.alert('错误', '调试失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
<ThemedText style={styles.title}>推送通知测试</ThemedText>
|
<ThemedText style={styles.title}>推送通知测试</ThemedText>
|
||||||
|
|
||||||
<View style={styles.statusContainer}>
|
<View style={styles.statusContainer}>
|
||||||
<ThemedText style={styles.statusText}>
|
<ThemedText style={styles.statusText}>
|
||||||
初始化状态: {isInitialized ? '已初始化' : '未初始化'}
|
初始化状态: {isInitialized ? '已初始化' : '未初始化'}
|
||||||
@@ -193,13 +171,6 @@ export const NotificationTest: React.FC = () => {
|
|||||||
<ThemedText style={styles.buttonText}>发送运动提醒</ThemedText>
|
<ThemedText style={styles.buttonText}>发送运动提醒</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.button}
|
|
||||||
onPress={handleSendGoalAchievement}
|
|
||||||
>
|
|
||||||
<ThemedText style={styles.buttonText}>发送目标达成通知</ThemedText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={handleSendMoodCheckinReminder}
|
onPress={handleSendMoodCheckinReminder}
|
||||||
@@ -214,13 +185,6 @@ export const NotificationTest: React.FC = () => {
|
|||||||
<ThemedText style={styles.buttonText}>获取已安排通知</ThemedText>
|
<ThemedText style={styles.buttonText}>获取已安排通知</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, styles.debugButton]}
|
|
||||||
onPress={handleDebugNotificationStatus}
|
|
||||||
>
|
|
||||||
<ThemedText style={styles.buttonText}>调试通知状态</ThemedText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.dangerButton]}
|
style={[styles.button, styles.dangerButton]}
|
||||||
onPress={handleCancelAllNotifications}
|
onPress={handleCancelAllNotifications}
|
||||||
@@ -247,9 +211,6 @@ export const NotificationTest: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 每日总结推送测试 */}
|
|
||||||
<DailySummaryTest userName="测试用户" />
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,501 +0,0 @@
|
|||||||
import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
|
||||||
import { Colors } from '@/constants/Colors';
|
|
||||||
import { useAppDispatch } from '@/hooks/redux';
|
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
||||||
import { completeTask, skipTask } from '@/store/tasksSlice';
|
|
||||||
import { TaskListItem } from '@/types/goals';
|
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import React from 'react';
|
|
||||||
import { Alert, Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
||||||
|
|
||||||
interface TaskCardProps {
|
|
||||||
task: TaskListItem;
|
|
||||||
onTaskCompleted?: (task: TaskListItem) => void; // 任务完成回调
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TaskCard: React.FC<TaskCardProps> = ({
|
|
||||||
task,
|
|
||||||
onTaskCompleted,
|
|
||||||
}) => {
|
|
||||||
const theme = useColorScheme() ?? 'light';
|
|
||||||
const colorTokens = Colors[theme];
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { showConfirm } = useGlobalDialog();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 创建进度条动画值
|
|
||||||
const progressAnimation = React.useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
// 当任务进度变化时,启动动画
|
|
||||||
React.useEffect(() => {
|
|
||||||
const targetProgress = task.progressPercentage > 0 ? Math.min(task.progressPercentage, 100) : 6;
|
|
||||||
|
|
||||||
Animated.timing(progressAnimation, {
|
|
||||||
toValue: targetProgress,
|
|
||||||
duration: 800, // 动画持续时间800毫秒
|
|
||||||
useNativeDriver: false, // 因为我们要动画width属性,所以不能使用原生驱动
|
|
||||||
}).start();
|
|
||||||
}, [task.progressPercentage, progressAnimation]);
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return '已完成';
|
|
||||||
case 'in_progress':
|
|
||||||
return '进行中';
|
|
||||||
case 'overdue':
|
|
||||||
return '已过期';
|
|
||||||
case 'skipped':
|
|
||||||
return '已跳过';
|
|
||||||
default:
|
|
||||||
return '待开始';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'overdue':
|
|
||||||
return '#EF4444'; // High - 过期任务
|
|
||||||
case 'in_progress':
|
|
||||||
return '#F59E0B'; // Medium - 进行中
|
|
||||||
case 'completed':
|
|
||||||
return '#10B981'; // Low - 已完成
|
|
||||||
default:
|
|
||||||
return '#6B7280'; // Default - 待开始
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityText = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'overdue':
|
|
||||||
return '高';
|
|
||||||
case 'in_progress':
|
|
||||||
return '中';
|
|
||||||
case 'completed':
|
|
||||||
return '低';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
const month = date.toLocaleDateString('zh-CN', { month: 'short' });
|
|
||||||
const day = date.getDate();
|
|
||||||
return `${day} ${month}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompleteTask = async () => {
|
|
||||||
// 如果任务已经完成,不执行任何操作
|
|
||||||
if (task.status === 'completed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 调用完成任务 API
|
|
||||||
await dispatch(completeTask({
|
|
||||||
taskId: task.id,
|
|
||||||
completionData: {
|
|
||||||
count: 1,
|
|
||||||
notes: '通过任务卡片完成'
|
|
||||||
}
|
|
||||||
})).unwrap();
|
|
||||||
|
|
||||||
// 触发任务完成回调
|
|
||||||
onTaskCompleted?.(task);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Alert.alert('错误', '完成任务失败,请重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSkipTask = async () => {
|
|
||||||
// 如果任务已经完成或已跳过,不执行任何操作
|
|
||||||
if (task.status === 'completed' || task.status === 'skipped') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示确认弹窗
|
|
||||||
showConfirm(
|
|
||||||
{
|
|
||||||
title: '确认跳过任务',
|
|
||||||
message: `确定要跳过任务"${task.title}"吗?\n\n跳过后的任务将不会显示在任务列表中,且无法恢复。`,
|
|
||||||
confirmText: '跳过',
|
|
||||||
cancelText: '取消',
|
|
||||||
destructive: true,
|
|
||||||
icon: 'warning',
|
|
||||||
iconColor: '#F59E0B',
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
// 调用跳过任务 API
|
|
||||||
await dispatch(skipTask({
|
|
||||||
taskId: task.id,
|
|
||||||
skipData: {
|
|
||||||
reason: '用户主动跳过'
|
|
||||||
}
|
|
||||||
})).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
Alert.alert('错误', '跳过任务失败,请重试');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTaskPress = () => {
|
|
||||||
router.push(`/task-detail?taskId=${task.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderActionIcons = () => {
|
|
||||||
if (task.status === 'completed' || task.status === 'overdue' || task.status === 'skipped') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.actionIconsContainer}>
|
|
||||||
{/* 完成任务图标 */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.iconContainer}
|
|
||||||
onPress={handleCompleteTask}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/task/icon-complete-gradient.png')}
|
|
||||||
style={styles.taskIcon}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* 跳过任务图标 - 仅对进行中的任务显示 */}
|
|
||||||
{task.status === 'pending' && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.skipIconContainer}
|
|
||||||
onPress={handleSkipTask}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/task/icon-skip.png')}
|
|
||||||
style={styles.taskIcon}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.container,
|
|
||||||
{
|
|
||||||
backgroundColor: task.status === 'completed'
|
|
||||||
? 'rgba(248, 250, 252, 0.8)' // 已完成任务使用稍微透明的背景色
|
|
||||||
: colorTokens.background
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
onPress={handleTaskPress}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<View style={styles.cardContent}>
|
|
||||||
{/* 左侧图标区域 */}
|
|
||||||
<View style={styles.iconSection}>
|
|
||||||
<View style={[
|
|
||||||
styles.iconCircle,
|
|
||||||
{
|
|
||||||
backgroundColor: task.status === 'completed'
|
|
||||||
? '#EDE9FE' // 完成状态使用更深的紫色背景
|
|
||||||
: '#F3E8FF',
|
|
||||||
}
|
|
||||||
]}>
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/task/icon-copy.png')}
|
|
||||||
style={styles.taskIcon}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧信息区域 */}
|
|
||||||
<View style={styles.infoSection}>
|
|
||||||
{/* 任务标题 */}
|
|
||||||
<Text style={[
|
|
||||||
styles.title,
|
|
||||||
{
|
|
||||||
color: task.status === 'completed'
|
|
||||||
? '#6B7280'
|
|
||||||
: colorTokens.text,
|
|
||||||
}
|
|
||||||
]} numberOfLines={1}>
|
|
||||||
{task.title}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* 进度条 */}
|
|
||||||
<View style={styles.progressContainer}>
|
|
||||||
{/* 背景进度条 */}
|
|
||||||
<View style={styles.progressBackground} />
|
|
||||||
|
|
||||||
{/* 实际进度条 */}
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.progressBar,
|
|
||||||
{
|
|
||||||
width: progressAnimation.interpolate({
|
|
||||||
inputRange: [0, 100],
|
|
||||||
outputRange: ['6%', '100%'], // 最小显示6%确保可见
|
|
||||||
}),
|
|
||||||
backgroundColor: task.status === 'completed'
|
|
||||||
? '#8B5CF6' // 完成状态也使用紫色
|
|
||||||
: task.progressPercentage > 0
|
|
||||||
? '#8B5CF6'
|
|
||||||
: '#C7D2FE', // 浅紫色,表示待开始
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 进度文字 */}
|
|
||||||
<Text style={[
|
|
||||||
styles.progressText,
|
|
||||||
{
|
|
||||||
color: task.progressPercentage > 20 || task.status === 'completed'
|
|
||||||
? '#FFFFFF'
|
|
||||||
: '#374151', // 进度较少时使用深色文字
|
|
||||||
}
|
|
||||||
]}>
|
|
||||||
{task.currentCount}/{task.targetCount} 次
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
{renderActionIcons()}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
padding: 14,
|
|
||||||
borderRadius: 30,
|
|
||||||
marginBottom: 12,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
cardContent: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
iconSection: {
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
iconCircle: {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: 18,
|
|
||||||
backgroundColor: '#F3E8FF', // 浅紫色背景
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
infoSection: {
|
|
||||||
flex: 1,
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: '600',
|
|
||||||
lineHeight: 20,
|
|
||||||
color: '#1F2937', // 深蓝紫色文字
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
progressContainer: {
|
|
||||||
position: 'relative',
|
|
||||||
height: 14,
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginTop: 4,
|
|
||||||
},
|
|
||||||
progressBackground: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: 14,
|
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
borderRadius: 10,
|
|
||||||
},
|
|
||||||
progressBar: {
|
|
||||||
height: 14,
|
|
||||||
borderRadius: 10,
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
minWidth: '6%', // 确保最小宽度可见
|
|
||||||
},
|
|
||||||
progressText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#FFFFFF',
|
|
||||||
textAlign: 'center',
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
actionIconsContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
},
|
|
||||||
skipIconContainer: {
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 14,
|
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
taskIcon: {
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
},
|
|
||||||
// 保留其他样式以备后用
|
|
||||||
header: {
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
titleSection: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
tagsContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
statusTag: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#F3F4F6',
|
|
||||||
},
|
|
||||||
statusTagText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#374151',
|
|
||||||
},
|
|
||||||
priorityTag: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
priorityTagText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
|
||||||
progressBarOld: {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
teamSection: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatars: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 24,
|
|
||||||
marginRight: -8,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#FFFFFF',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
avatarImage: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
infoTag: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
infoTagText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#374151',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
||||||
|
|
||||||
export type TaskFilterType = 'all' | 'pending' | 'completed' | 'skipped';
|
|
||||||
|
|
||||||
interface TaskFilterTabsProps {
|
|
||||||
selectedFilter: TaskFilterType;
|
|
||||||
onFilterChange: (filter: TaskFilterType) => void;
|
|
||||||
taskCounts: {
|
|
||||||
all: number;
|
|
||||||
pending: number;
|
|
||||||
completed: number;
|
|
||||||
skipped: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TaskFilterTabs: React.FC<TaskFilterTabsProps> = ({
|
|
||||||
selectedFilter,
|
|
||||||
onFilterChange,
|
|
||||||
taskCounts,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<View style={styles.tabContainer}>
|
|
||||||
{/* 全部 Tab */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.tab,
|
|
||||||
selectedFilter === 'all' && styles.activeTab
|
|
||||||
]}
|
|
||||||
onPress={() => onFilterChange('all')}
|
|
||||||
>
|
|
||||||
<Text style={[
|
|
||||||
styles.tabText,
|
|
||||||
selectedFilter === 'all' && styles.activeTabText
|
|
||||||
]}>
|
|
||||||
全部
|
|
||||||
</Text>
|
|
||||||
<View style={[
|
|
||||||
styles.badge,
|
|
||||||
selectedFilter === 'all' ? styles.activeBadge : styles.inactiveBadge
|
|
||||||
]}>
|
|
||||||
<Text style={[
|
|
||||||
styles.badgeText,
|
|
||||||
selectedFilter === 'all' && styles.activeBadgeText
|
|
||||||
]}>
|
|
||||||
{taskCounts.all}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* 待完成 Tab */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.tab,
|
|
||||||
selectedFilter === 'pending' && styles.activeTab
|
|
||||||
]}
|
|
||||||
onPress={() => onFilterChange('pending')}
|
|
||||||
>
|
|
||||||
<Text style={[
|
|
||||||
styles.tabText,
|
|
||||||
selectedFilter === 'pending' && styles.activeTabText
|
|
||||||
]}>
|
|
||||||
待完成
|
|
||||||
</Text>
|
|
||||||
<View style={[
|
|
||||||
styles.badge,
|
|
||||||
selectedFilter === 'pending' ? styles.activeBadge : styles.inactiveBadge
|
|
||||||
]}>
|
|
||||||
<Text style={[
|
|
||||||
styles.badgeText,
|
|
||||||
selectedFilter === 'pending' && styles.activeBadgeText
|
|
||||||
]}>
|
|
||||||
{taskCounts.pending}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* 已完成 Tab */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.tab,
|
|
||||||
selectedFilter === 'completed' && styles.activeTab
|
|
||||||
]}
|
|
||||||
onPress={() => onFilterChange('completed')}
|
|
||||||
>
|
|
||||||
<Text style={[
|
|
||||||
styles.tabText,
|
|
||||||
selectedFilter === 'completed' && styles.activeTabText
|
|
||||||
]}>
|
|
||||||
已完成
|
|
||||||
</Text>
|
|
||||||
<View style={[
|
|
||||||
styles.badge,
|
|
||||||
selectedFilter === 'completed' ? styles.activeBadge : styles.inactiveBadge
|
|
||||||
]}>
|
|
||||||
<Text style={[
|
|
||||||
styles.badgeText,
|
|
||||||
selectedFilter === 'completed' && styles.activeBadgeText
|
|
||||||
]}>
|
|
||||||
{taskCounts.completed}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{/* 已跳过 Tab */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.tab,
|
|
||||||
selectedFilter === 'skipped' && styles.activeTab
|
|
||||||
]}
|
|
||||||
onPress={() => onFilterChange('skipped')}
|
|
||||||
>
|
|
||||||
<Text style={[
|
|
||||||
styles.tabText,
|
|
||||||
selectedFilter === 'skipped' && styles.activeTabText
|
|
||||||
]}>
|
|
||||||
已跳过
|
|
||||||
</Text>
|
|
||||||
<View style={[
|
|
||||||
styles.badge,
|
|
||||||
selectedFilter === 'skipped' ? styles.activeBadge : styles.inactiveBadge
|
|
||||||
]}>
|
|
||||||
<Text style={[
|
|
||||||
styles.badgeText,
|
|
||||||
selectedFilter === 'skipped' && styles.activeBadgeText
|
|
||||||
]}>
|
|
||||||
{taskCounts.skipped}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
},
|
|
||||||
tabContainer: {
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 24,
|
|
||||||
padding: 4,
|
|
||||||
flexDirection: 'row',
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
shadowOpacity: 0.05,
|
|
||||||
shadowRadius: 2,
|
|
||||||
elevation: 1,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#E5E7EB',
|
|
||||||
},
|
|
||||||
tab: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
borderRadius: 20,
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
activeTab: {
|
|
||||||
backgroundColor: '#7A5AF8',
|
|
||||||
shadowColor: '#7A5AF8',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.2,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 3,
|
|
||||||
},
|
|
||||||
tabText: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#374151',
|
|
||||||
},
|
|
||||||
activeTabText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
badge: {
|
|
||||||
minWidth: 20,
|
|
||||||
height: 20,
|
|
||||||
borderRadius: 10,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
},
|
|
||||||
inactiveBadge: {
|
|
||||||
backgroundColor: '#E5E7EB',
|
|
||||||
},
|
|
||||||
activeBadge: {
|
|
||||||
backgroundColor: '#EF4444',
|
|
||||||
},
|
|
||||||
badgeText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#374151',
|
|
||||||
},
|
|
||||||
activeBadgeText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import { TaskListItem } from '@/types/goals';
|
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|
||||||
import { Image } from 'expo-image';
|
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
||||||
|
|
||||||
interface TaskProgressCardProps {
|
|
||||||
tasks: TaskListItem[];
|
|
||||||
headerButtons?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
|
|
||||||
tasks,
|
|
||||||
headerButtons,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 计算各状态的任务数量
|
|
||||||
const pendingTasks = tasks.filter(task => task.status === 'pending');
|
|
||||||
const completedTasks = tasks.filter(task => task.status === 'completed');
|
|
||||||
const skippedTasks = tasks.filter(task => task.status === 'skipped');
|
|
||||||
|
|
||||||
// 处理跳转到目标列表
|
|
||||||
const handleNavigateToGoals = () => {
|
|
||||||
router.push('/goals-list');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
{/* 标题区域 */}
|
|
||||||
<View style={styles.header}>
|
|
||||||
<View style={styles.titleContainer}>
|
|
||||||
<Text style={styles.title}>今日</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.goalsIconButton}
|
|
||||||
onPress={handleNavigateToGoals}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-goal-edit.png' }}
|
|
||||||
style={{ width: 18, height: 18 }}
|
|
||||||
cachePolicy="memory-disk"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
<View style={styles.headerActions}>
|
|
||||||
|
|
||||||
{headerButtons && (
|
|
||||||
<View style={styles.headerButtons}>
|
|
||||||
{headerButtons}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 状态卡片区域 */}
|
|
||||||
<View style={styles.statusCards}>
|
|
||||||
{/* 待完成 卡片 */}
|
|
||||||
<View style={styles.statusCard}>
|
|
||||||
<View style={styles.cardHeader}>
|
|
||||||
<MaterialIcons name="pending" size={16} color="#7A5AF8" />
|
|
||||||
<Text style={styles.cardLabel} numberOfLines={1}>待完成</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.cardCount}>{pendingTasks.length}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 已完成 卡片 */}
|
|
||||||
<View style={styles.statusCard}>
|
|
||||||
<View style={styles.cardHeader}>
|
|
||||||
<MaterialIcons name="check-circle" size={16} color="#10B981" />
|
|
||||||
<Text style={styles.cardLabel} numberOfLines={1}>已完成</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.cardCount}>{completedTasks.length}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 已跳过 卡片 */}
|
|
||||||
<View style={styles.statusCard}>
|
|
||||||
<View style={styles.cardHeader}>
|
|
||||||
<MaterialIcons name="skip-next" size={16} color="#F59E0B" />
|
|
||||||
<Text style={styles.cardLabel} numberOfLines={1}>已跳过</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.cardCount}>{skippedTasks.length}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 16,
|
|
||||||
padding: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
marginBottom: 20,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 8,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
marginBottom: 20,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
titleContainer: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
headerButtons: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
headerActions: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
goalsIconButton: {
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: '700',
|
|
||||||
lineHeight: 24,
|
|
||||||
height: 24,
|
|
||||||
color: '#1F2937',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: '#6B7280',
|
|
||||||
fontWeight: '400',
|
|
||||||
},
|
|
||||||
statusCards: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
statusCard: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#E5E7EB',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
minHeight: 80,
|
|
||||||
},
|
|
||||||
cardHeader: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
gap: 6,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
},
|
|
||||||
cardLabel: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#1F2937',
|
|
||||||
lineHeight: 14,
|
|
||||||
},
|
|
||||||
cardCount: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '700',
|
|
||||||
color: '#1F2937',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ export const ROUTES = {
|
|||||||
// Tab路由
|
// Tab路由
|
||||||
TAB_EXPLORE: '/explore',
|
TAB_EXPLORE: '/explore',
|
||||||
TAB_COACH: '/coach',
|
TAB_COACH: '/coach',
|
||||||
TAB_GOALS: '/goals',
|
|
||||||
TAB_STATISTICS: '/statistics',
|
TAB_STATISTICS: '/statistics',
|
||||||
TAB_CHALLENGES: '/challenges',
|
TAB_CHALLENGES: '/challenges',
|
||||||
TAB_PERSONAL: '/personal',
|
TAB_PERSONAL: '/personal',
|
||||||
@@ -30,7 +29,6 @@ export const ROUTES = {
|
|||||||
// 用户相关路由
|
// 用户相关路由
|
||||||
AUTH_LOGIN: '/auth/login',
|
AUTH_LOGIN: '/auth/login',
|
||||||
PROFILE_EDIT: '/profile/edit',
|
PROFILE_EDIT: '/profile/edit',
|
||||||
PROFILE_GOALS: '/profile/goals',
|
|
||||||
|
|
||||||
// 法律相关路由
|
// 法律相关路由
|
||||||
LEGAL_USER_AGREEMENT: '/legal/user-agreement',
|
LEGAL_USER_AGREEMENT: '/legal/user-agreement',
|
||||||
@@ -59,8 +57,6 @@ export const ROUTES = {
|
|||||||
// 轻断食相关
|
// 轻断食相关
|
||||||
FASTING_PLAN_DETAIL: '/fasting',
|
FASTING_PLAN_DETAIL: '/fasting',
|
||||||
|
|
||||||
// 任务相关路由
|
|
||||||
TASK_DETAIL: '/task-detail',
|
|
||||||
|
|
||||||
// 目标管理路由 (已移至tab中)
|
// 目标管理路由 (已移至tab中)
|
||||||
// GOAL_MANAGEMENT: '/goal-management',
|
// GOAL_MANAGEMENT: '/goal-management',
|
||||||
@@ -85,8 +81,7 @@ export const ROUTE_PARAMS = {
|
|||||||
// 文章参数
|
// 文章参数
|
||||||
ARTICLE_ID: 'id',
|
ARTICLE_ID: 'id',
|
||||||
|
|
||||||
// 任务参数
|
// 断食计划参数
|
||||||
TASK_ID: 'taskId',
|
|
||||||
FASTING_PLAN_ID: 'planId',
|
FASTING_PLAN_ID: 'planId',
|
||||||
|
|
||||||
// 重定向参数
|
// 重定向参数
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
import { CreateGoalRequest } from '@/types/goals';
|
|
||||||
|
|
||||||
export interface GoalTemplate {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon: string;
|
|
||||||
iconColor: string;
|
|
||||||
backgroundColor: string;
|
|
||||||
category: 'recommended' | 'health' | 'lifestyle' | 'exercise';
|
|
||||||
data: Partial<CreateGoalRequest>;
|
|
||||||
isRecommended?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GoalCategory {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon?: string;
|
|
||||||
isRecommended?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const goalTemplates: GoalTemplate[] = [
|
|
||||||
// 改善睡眠分类的模板
|
|
||||||
{
|
|
||||||
id: 'afternoon-nap',
|
|
||||||
title: '睡一会午觉',
|
|
||||||
icon: 'hotel',
|
|
||||||
iconColor: '#22D3EE',
|
|
||||||
backgroundColor: '#E0F2FE',
|
|
||||||
category: 'health',
|
|
||||||
data: {
|
|
||||||
title: '午休时间',
|
|
||||||
description: '每天午休30分钟,恢复精力',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '13:00',
|
|
||||||
},
|
|
||||||
isRecommended: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'regular-bedtime',
|
|
||||||
title: '规律作息',
|
|
||||||
icon: 'access-time',
|
|
||||||
iconColor: '#8B5CF6',
|
|
||||||
backgroundColor: '#F3E8FF',
|
|
||||||
category: 'health',
|
|
||||||
data: {
|
|
||||||
title: '保持规律作息',
|
|
||||||
description: '每天固定时间睡觉起床,建立健康的生物钟',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '22:30',
|
|
||||||
},
|
|
||||||
isRecommended: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// 生活习惯分类的模板
|
|
||||||
{
|
|
||||||
id: 'eat-breakfast',
|
|
||||||
title: '坚持吃早餐',
|
|
||||||
icon: 'restaurant',
|
|
||||||
iconColor: '#F97316',
|
|
||||||
backgroundColor: '#FFF7ED',
|
|
||||||
category: 'lifestyle',
|
|
||||||
data: {
|
|
||||||
title: '坚持吃早餐',
|
|
||||||
description: '每天按时吃早餐,保持营养均衡',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '07:30',
|
|
||||||
},
|
|
||||||
isRecommended: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'drink-water',
|
|
||||||
title: '喝八杯水',
|
|
||||||
icon: 'local-drink',
|
|
||||||
iconColor: '#06B6D4',
|
|
||||||
backgroundColor: '#E0F2FE',
|
|
||||||
category: 'lifestyle',
|
|
||||||
data: {
|
|
||||||
title: '每日饮水目标',
|
|
||||||
description: '每天喝足够的水,保持身体水分',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 8,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '09:00',
|
|
||||||
},
|
|
||||||
isRecommended: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'read-book',
|
|
||||||
title: '看一本新书',
|
|
||||||
icon: 'book',
|
|
||||||
iconColor: '#8B5CF6',
|
|
||||||
backgroundColor: '#F3E8FF',
|
|
||||||
category: 'lifestyle',
|
|
||||||
data: {
|
|
||||||
title: '阅读新书',
|
|
||||||
description: '每天阅读30分钟,丰富知识',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '20:00',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'housework',
|
|
||||||
title: '做一会家务',
|
|
||||||
icon: 'home',
|
|
||||||
iconColor: '#F97316',
|
|
||||||
backgroundColor: '#FFF7ED',
|
|
||||||
category: 'lifestyle',
|
|
||||||
data: {
|
|
||||||
title: '日常家务',
|
|
||||||
description: '每天做一些家务,保持家居整洁',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '19:00',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mindfulness',
|
|
||||||
title: '练习一次正念',
|
|
||||||
icon: 'self-improvement',
|
|
||||||
iconColor: '#10B981',
|
|
||||||
backgroundColor: '#ECFDF5',
|
|
||||||
category: 'lifestyle',
|
|
||||||
data: {
|
|
||||||
title: '正念练习',
|
|
||||||
description: '每天练习10分钟正念冥想',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '21:00',
|
|
||||||
},
|
|
||||||
isRecommended: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// 运动锻炼分类的模板
|
|
||||||
{
|
|
||||||
id: 'exercise-duration',
|
|
||||||
title: '锻炼时长达标',
|
|
||||||
icon: 'timer',
|
|
||||||
iconColor: '#7C3AED',
|
|
||||||
backgroundColor: '#F3E8FF',
|
|
||||||
category: 'exercise',
|
|
||||||
data: {
|
|
||||||
title: '每日锻炼时长',
|
|
||||||
description: '每天至少锻炼30分钟',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '17:00',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'morning-run',
|
|
||||||
title: '晨跑',
|
|
||||||
icon: 'directions-run',
|
|
||||||
iconColor: '#EF4444',
|
|
||||||
backgroundColor: '#FEF2F2',
|
|
||||||
category: 'exercise',
|
|
||||||
data: {
|
|
||||||
title: '每日晨跑',
|
|
||||||
description: '每天早上跑步30分钟,保持活力',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '06:30',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yoga-practice',
|
|
||||||
title: '瑜伽练习',
|
|
||||||
icon: 'self-improvement',
|
|
||||||
iconColor: '#10B981',
|
|
||||||
backgroundColor: '#ECFDF5',
|
|
||||||
category: 'exercise',
|
|
||||||
data: {
|
|
||||||
title: '每日瑜伽',
|
|
||||||
description: '每天练习瑜伽,提升身体柔韧性',
|
|
||||||
repeatType: 'daily',
|
|
||||||
frequency: 1,
|
|
||||||
hasReminder: true,
|
|
||||||
reminderTime: '19:30',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 分类定义
|
|
||||||
export const goalCategories: GoalCategory[] = [
|
|
||||||
{
|
|
||||||
id: 'recommended',
|
|
||||||
title: '推荐',
|
|
||||||
isRecommended: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'health',
|
|
||||||
title: '改善睡眠'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lifestyle',
|
|
||||||
title: '生活习惯'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'exercise',
|
|
||||||
title: '运动锻炼'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 按类别分组的模板
|
|
||||||
export const getTemplatesByCategory = (category: string) => {
|
|
||||||
if (category === 'recommended') {
|
|
||||||
// 推荐分类显示所有分类中的精选模板
|
|
||||||
return goalTemplates.filter(template => template.isRecommended);
|
|
||||||
}
|
|
||||||
return goalTemplates.filter(template => template.category === category);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取所有分类
|
|
||||||
export const getAllCategories = () => {
|
|
||||||
return goalCategories;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
|
||||||
import { notificationService, NotificationData, NotificationTypes } from '../services/notifications';
|
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { NotificationData, notificationService, NotificationTypes } from '../services/notifications';
|
||||||
|
|
||||||
export interface UseNotificationsReturn {
|
export interface UseNotificationsReturn {
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
@@ -25,9 +25,7 @@ export interface UseNotificationsReturn {
|
|||||||
cancelAllNotifications: () => Promise<void>;
|
cancelAllNotifications: () => Promise<void>;
|
||||||
getAllScheduledNotifications: () => Promise<Notifications.NotificationRequest[]>;
|
getAllScheduledNotifications: () => Promise<Notifications.NotificationRequest[]>;
|
||||||
sendWorkoutReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
sendWorkoutReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
||||||
sendGoalAchievement: (title: string, body: string) => Promise<string>;
|
|
||||||
sendMoodCheckinReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
sendMoodCheckinReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
||||||
debugNotificationStatus: () => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotifications = (): UseNotificationsReturn => {
|
export const useNotifications = (): UseNotificationsReturn => {
|
||||||
@@ -99,7 +97,7 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
sound: true,
|
sound: true,
|
||||||
priority: 'high',
|
priority: 'high',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
return notificationService.scheduleNotificationAtDate(notification, date);
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
} else {
|
} else {
|
||||||
@@ -107,17 +105,7 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sendGoalAchievement = useCallback(async (title: string, body: string) => {
|
// sendGoalAchievement 函数已删除,因为目标功能已移除
|
||||||
const notification: NotificationData = {
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
|
|
||||||
sound: true,
|
|
||||||
priority: 'high',
|
|
||||||
};
|
|
||||||
|
|
||||||
return notificationService.sendImmediateNotification(notification);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sendMoodCheckinReminder = useCallback(async (title: string, body: string, date?: Date) => {
|
const sendMoodCheckinReminder = useCallback(async (title: string, body: string, date?: Date) => {
|
||||||
const notification: NotificationData = {
|
const notification: NotificationData = {
|
||||||
@@ -127,7 +115,7 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
sound: true,
|
sound: true,
|
||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
return notificationService.scheduleNotificationAtDate(notification, date);
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
} else {
|
} else {
|
||||||
@@ -135,10 +123,6 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const debugNotificationStatus = useCallback(async () => {
|
|
||||||
return notificationService.debugNotificationStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 组件挂载时自动初始化
|
// 组件挂载时自动初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialize();
|
initialize();
|
||||||
@@ -156,8 +140,6 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
cancelAllNotifications,
|
cancelAllNotifications,
|
||||||
getAllScheduledNotifications,
|
getAllScheduledNotifications,
|
||||||
sendWorkoutReminder,
|
sendWorkoutReminder,
|
||||||
sendGoalAchievement,
|
|
||||||
sendMoodCheckinReminder,
|
sendMoodCheckinReminder,
|
||||||
debugNotificationStatus,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.20</string>
|
<string>1.0.21</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
620
ios/Podfile.lock
620
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
1045
package-lock.json
generated
1045
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -24,35 +24,35 @@
|
|||||||
"@sentry/react-native": "~7.2.0",
|
"@sentry/react-native": "~7.2.0",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"expo": "^54.0.13",
|
"expo": "54.0.21",
|
||||||
"expo-apple-authentication": "~8.0.7",
|
"expo-apple-authentication": "~8.0.7",
|
||||||
"expo-background-task": "~1.0.8",
|
"expo-background-task": "~1.0.8",
|
||||||
"expo-blur": "~15.0.7",
|
"expo-blur": "~15.0.7",
|
||||||
"expo-camera": "~17.0.8",
|
"expo-camera": "~17.0.8",
|
||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
"expo-glass-effect": "^0.1.4",
|
"expo-glass-effect": "~0.1.5",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-image": "~3.0.9",
|
"expo-image": "~3.0.10",
|
||||||
"expo-image-picker": "~17.0.8",
|
"expo-image-picker": "~17.0.8",
|
||||||
"expo-linear-gradient": "~15.0.7",
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
"expo-media-library": "^18.2.0",
|
"expo-media-library": "^18.2.0",
|
||||||
"expo-notifications": "~0.32.12",
|
"expo-notifications": "~0.32.12",
|
||||||
"expo-quick-actions": "^6.0.0",
|
"expo-quick-actions": "^6.0.0",
|
||||||
"expo-router": "~6.0.12",
|
"expo-router": "~6.0.14",
|
||||||
"expo-splash-screen": "~31.0.10",
|
"expo-splash-screen": "~31.0.10",
|
||||||
"expo-sqlite": "^16.0.8",
|
"expo-sqlite": "^16.0.8",
|
||||||
"expo-status-bar": "~3.0.8",
|
"expo-status-bar": "~3.0.8",
|
||||||
"expo-symbols": "~1.0.7",
|
"expo-symbols": "~1.0.7",
|
||||||
"expo-system-ui": "~6.0.7",
|
"expo-system-ui": "~6.0.8",
|
||||||
"expo-task-manager": "~14.0.7",
|
"expo-task-manager": "~14.0.8",
|
||||||
"expo-web-browser": "~15.0.7",
|
"expo-web-browser": "~15.0.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lottie-react-native": "^7.3.4",
|
"lottie-react-native": "^7.3.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.4",
|
"react-native": "0.81.5",
|
||||||
"react-native-chart-kit": "^6.12.0",
|
"react-native-chart-kit": "^6.12.0",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^14.0.4",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import {
|
|
||||||
ApiResponse,
|
|
||||||
BatchGoalOperationRequest,
|
|
||||||
BatchGoalOperationResult,
|
|
||||||
CompleteGoalRequest,
|
|
||||||
CreateGoalRequest,
|
|
||||||
GetGoalCompletionsQuery,
|
|
||||||
GetGoalsQuery,
|
|
||||||
Goal,
|
|
||||||
GoalCompletion,
|
|
||||||
GoalDetailResponse,
|
|
||||||
GoalListItem,
|
|
||||||
GoalStats,
|
|
||||||
PaginatedResponse,
|
|
||||||
UpdateGoalRequest,
|
|
||||||
} from '@/types/goals';
|
|
||||||
import { api } from './api';
|
|
||||||
|
|
||||||
// 目标管理API服务
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建目标
|
|
||||||
*/
|
|
||||||
export const createGoal = async (goalData: CreateGoalRequest): Promise<Goal> => {
|
|
||||||
return api.post<Goal>('/goals', goalData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标列表
|
|
||||||
*/
|
|
||||||
export const getGoals = async (query: GetGoalsQuery = {}): Promise<PaginatedResponse<GoalListItem>> => {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
Object.entries(query).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
searchParams.append(key, String(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryString = searchParams.toString();
|
|
||||||
const path = queryString ? `/goals?${queryString}` : '/goals';
|
|
||||||
|
|
||||||
return api.get<PaginatedResponse<GoalListItem>>(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标详情
|
|
||||||
*/
|
|
||||||
export const getGoalById = async (goalId: string): Promise<ApiResponse<GoalDetailResponse>> => {
|
|
||||||
return api.get<ApiResponse<GoalDetailResponse>>(`/goals/${goalId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新目标
|
|
||||||
*/
|
|
||||||
export const updateGoal = async (goalId: string, goalData: UpdateGoalRequest): Promise<Goal> => {
|
|
||||||
return api.put<Goal>(`/goals/${goalId}`, goalData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除目标
|
|
||||||
*/
|
|
||||||
export const deleteGoal = async (goalId: string): Promise<ApiResponse<boolean>> => {
|
|
||||||
return api.delete<ApiResponse<boolean>>(`/goals/${goalId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录目标完成
|
|
||||||
*/
|
|
||||||
export const completeGoal = async (goalId: string, completionData: CompleteGoalRequest = {}): Promise<ApiResponse<GoalCompletion>> => {
|
|
||||||
return api.post<ApiResponse<GoalCompletion>>(`/goals/${goalId}/complete`, completionData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标完成记录
|
|
||||||
*/
|
|
||||||
export const getGoalCompletions = async (
|
|
||||||
goalId: string,
|
|
||||||
query: GetGoalCompletionsQuery = {}
|
|
||||||
): Promise<ApiResponse<PaginatedResponse<GoalCompletion>>> => {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
Object.entries(query).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
searchParams.append(key, String(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryString = searchParams.toString();
|
|
||||||
const path = queryString ? `/goals/${goalId}/completions?${queryString}` : `/goals/${goalId}/completions`;
|
|
||||||
|
|
||||||
return api.get<ApiResponse<PaginatedResponse<GoalCompletion>>>(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标统计信息
|
|
||||||
*/
|
|
||||||
export const getGoalStats = async (): Promise<ApiResponse<GoalStats>> => {
|
|
||||||
return api.get<ApiResponse<GoalStats>>('/goals/stats/overview');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量操作目标
|
|
||||||
*/
|
|
||||||
export const batchOperateGoals = async (operationData: BatchGoalOperationRequest): Promise<ApiResponse<BatchGoalOperationResult[]>> => {
|
|
||||||
return api.post<ApiResponse<BatchGoalOperationResult[]>>('/goals/batch', operationData);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 导出所有API方法
|
|
||||||
export const goalsApi = {
|
|
||||||
createGoal,
|
|
||||||
getGoals,
|
|
||||||
getGoalById,
|
|
||||||
updateGoal,
|
|
||||||
deleteGoal,
|
|
||||||
completeGoal,
|
|
||||||
getGoalCompletions,
|
|
||||||
getGoalStats,
|
|
||||||
batchOperateGoals,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default goalsApi;
|
|
||||||
@@ -526,7 +526,6 @@ export const notificationService = NotificationService.getInstance();
|
|||||||
// 预定义的推送通知类型
|
// 预定义的推送通知类型
|
||||||
export const NotificationTypes = {
|
export const NotificationTypes = {
|
||||||
WORKOUT_REMINDER: 'workout_reminder',
|
WORKOUT_REMINDER: 'workout_reminder',
|
||||||
GOAL_ACHIEVEMENT: 'goal_achievement',
|
|
||||||
MOOD_CHECKIN: 'mood_checkin',
|
MOOD_CHECKIN: 'mood_checkin',
|
||||||
NUTRITION_REMINDER: 'nutrition_reminder',
|
NUTRITION_REMINDER: 'nutrition_reminder',
|
||||||
PROGRESS_UPDATE: 'progress_update',
|
PROGRESS_UPDATE: 'progress_update',
|
||||||
@@ -558,17 +557,7 @@ export const sendWorkoutReminder = (title: string, body: string, date?: Date) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendGoalAchievement = (title: string, body: string) => {
|
// sendGoalAchievement 函数已删除,因为目标功能已移除
|
||||||
const notification: NotificationData = {
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
|
|
||||||
sound: true,
|
|
||||||
priority: 'high',
|
|
||||||
};
|
|
||||||
|
|
||||||
return notificationService.sendImmediateNotification(notification);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendMoodCheckinReminder = (title: string, body: string, date?: Date) => {
|
export const sendMoodCheckinReminder = (title: string, body: string, date?: Date) => {
|
||||||
const notification: NotificationData = {
|
const notification: NotificationData = {
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import {
|
|
||||||
ApiResponse,
|
|
||||||
CompleteTaskRequest,
|
|
||||||
GetTasksQuery,
|
|
||||||
PaginatedResponse,
|
|
||||||
SkipTaskRequest,
|
|
||||||
Task,
|
|
||||||
TaskListItem,
|
|
||||||
TaskStats,
|
|
||||||
} from '@/types/goals';
|
|
||||||
import { api } from './api';
|
|
||||||
|
|
||||||
// 任务管理API服务
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取任务列表
|
|
||||||
*/
|
|
||||||
export const getTasks = async (query: GetTasksQuery = {}): Promise<PaginatedResponse<TaskListItem>> => {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
Object.entries(query).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
searchParams.append(key, String(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryString = searchParams.toString();
|
|
||||||
const path = queryString ? `/goals/tasks?${queryString}` : '/goals/tasks';
|
|
||||||
|
|
||||||
return api.get<PaginatedResponse<TaskListItem>>(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取特定目标的任务列表
|
|
||||||
*/
|
|
||||||
export const getTasksByGoalId = async (goalId: string, query: GetTasksQuery = {}): Promise<PaginatedResponse<TaskListItem>> => {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
Object.entries(query).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
searchParams.append(key, String(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryString = searchParams.toString();
|
|
||||||
const path = queryString ? `/goals/${goalId}/tasks?${queryString}` : `/goals/${goalId}/tasks`;
|
|
||||||
|
|
||||||
return api.get<PaginatedResponse<TaskListItem>>(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 完成任务
|
|
||||||
*/
|
|
||||||
export const completeTask = async (taskId: string, completionData: CompleteTaskRequest = {}): Promise<Task> => {
|
|
||||||
return api.post<Task>(`/goals/tasks/${taskId}/complete`, completionData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 跳过任务
|
|
||||||
*/
|
|
||||||
export const skipTask = async (taskId: string, skipData: SkipTaskRequest = {}): Promise<Task> => {
|
|
||||||
return api.post<Task>(`/goals/tasks/${taskId}/skip`, skipData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取任务统计
|
|
||||||
*/
|
|
||||||
export const getTaskStats = async (goalId?: string): Promise<TaskStats> => {
|
|
||||||
const path = goalId ? `/goals/tasks/stats/overview?goalId=${goalId}` : '/goals/tasks/stats/overview';
|
|
||||||
const response = await api.get<ApiResponse<TaskStats>>(path);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导出所有API方法
|
|
||||||
export const tasksApi = {
|
|
||||||
getTasks,
|
|
||||||
getTasksByGoalId,
|
|
||||||
completeTask,
|
|
||||||
skipTask,
|
|
||||||
getTaskStats,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tasksApi;
|
|
||||||
@@ -1,603 +0,0 @@
|
|||||||
import { goalsApi } from '@/services/goalsApi';
|
|
||||||
import {
|
|
||||||
BatchGoalOperationRequest,
|
|
||||||
CompleteGoalRequest,
|
|
||||||
CreateGoalRequest,
|
|
||||||
GetGoalCompletionsQuery,
|
|
||||||
GetGoalsQuery,
|
|
||||||
GoalCompletion,
|
|
||||||
GoalDetailResponse,
|
|
||||||
GoalListItem,
|
|
||||||
GoalStats,
|
|
||||||
GoalStatus,
|
|
||||||
UpdateGoalRequest
|
|
||||||
} from '@/types/goals';
|
|
||||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
// 目标管理状态类型
|
|
||||||
export interface GoalsState {
|
|
||||||
// 目标列表
|
|
||||||
goals: GoalListItem[];
|
|
||||||
goalsLoading: boolean;
|
|
||||||
goalsError: string | null;
|
|
||||||
goalsPagination: {
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 当前查看的目标详情
|
|
||||||
currentGoal: GoalDetailResponse | null;
|
|
||||||
currentGoalLoading: boolean;
|
|
||||||
currentGoalError: string | null;
|
|
||||||
|
|
||||||
// 目标完成记录
|
|
||||||
completions: GoalCompletion[];
|
|
||||||
completionsLoading: boolean;
|
|
||||||
completionsError: string | null;
|
|
||||||
completionsPagination: {
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 目标统计
|
|
||||||
stats: GoalStats | null;
|
|
||||||
statsLoading: boolean;
|
|
||||||
statsError: string | null;
|
|
||||||
|
|
||||||
// 创建/更新目标
|
|
||||||
createLoading: boolean;
|
|
||||||
createError: string | null;
|
|
||||||
updateLoading: boolean;
|
|
||||||
updateError: string | null;
|
|
||||||
|
|
||||||
// 批量操作
|
|
||||||
batchLoading: boolean;
|
|
||||||
batchError: string | null;
|
|
||||||
|
|
||||||
// 筛选和搜索
|
|
||||||
filters: GetGoalsQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: GoalsState = {
|
|
||||||
goals: [],
|
|
||||||
goalsLoading: false,
|
|
||||||
goalsError: null,
|
|
||||||
goalsPagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
total: 0,
|
|
||||||
hasMore: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
currentGoal: null,
|
|
||||||
currentGoalLoading: false,
|
|
||||||
currentGoalError: null,
|
|
||||||
|
|
||||||
completions: [],
|
|
||||||
completionsLoading: false,
|
|
||||||
completionsError: null,
|
|
||||||
completionsPagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
total: 0,
|
|
||||||
hasMore: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
stats: null,
|
|
||||||
statsLoading: false,
|
|
||||||
statsError: null,
|
|
||||||
|
|
||||||
createLoading: false,
|
|
||||||
createError: null,
|
|
||||||
updateLoading: false,
|
|
||||||
updateError: null,
|
|
||||||
|
|
||||||
batchLoading: false,
|
|
||||||
batchError: null,
|
|
||||||
|
|
||||||
filters: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
sortBy: 'createdAt',
|
|
||||||
sortOrder: 'desc',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 异步操作
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标列表
|
|
||||||
*/
|
|
||||||
export const fetchGoals = createAsyncThunk(
|
|
||||||
'goals/fetchGoals',
|
|
||||||
async (query: GetGoalsQuery = {}, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.getGoals(query);
|
|
||||||
|
|
||||||
console.log('fetchGoals response', response);
|
|
||||||
return { query, response };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '获取目标列表失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载更多目标
|
|
||||||
*/
|
|
||||||
export const loadMoreGoals = createAsyncThunk(
|
|
||||||
'goals/loadMoreGoals',
|
|
||||||
async (_, { getState, rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const state = getState() as { goals: GoalsState };
|
|
||||||
const { filters, goalsPagination } = state.goals;
|
|
||||||
|
|
||||||
if (!goalsPagination.hasMore) {
|
|
||||||
return { goals: [], pagination: goalsPagination };
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
...filters,
|
|
||||||
page: goalsPagination.page + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await goalsApi.getGoals(query);
|
|
||||||
|
|
||||||
console.log('response', response);
|
|
||||||
|
|
||||||
return { query, response };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '加载更多目标失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标详情
|
|
||||||
*/
|
|
||||||
export const fetchGoalDetail = createAsyncThunk(
|
|
||||||
'goals/fetchGoalDetail',
|
|
||||||
async (goalId: string, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.getGoalById(goalId);
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '获取目标详情失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建目标
|
|
||||||
*/
|
|
||||||
export const createGoal = createAsyncThunk(
|
|
||||||
'goals/createGoal',
|
|
||||||
async (goalData: CreateGoalRequest, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.createGoal(goalData);
|
|
||||||
console.log('createGoal response', response);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '创建目标失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新目标
|
|
||||||
*/
|
|
||||||
export const updateGoal = createAsyncThunk(
|
|
||||||
'goals/updateGoal',
|
|
||||||
async ({ goalId, goalData }: { goalId: string; goalData: UpdateGoalRequest }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.updateGoal(goalId, goalData);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '更新目标失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除目标
|
|
||||||
*/
|
|
||||||
export const deleteGoal = createAsyncThunk(
|
|
||||||
'goals/deleteGoal',
|
|
||||||
async (goalId: string, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
await goalsApi.deleteGoal(goalId);
|
|
||||||
return goalId;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '删除目标失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录目标完成
|
|
||||||
*/
|
|
||||||
export const completeGoal = createAsyncThunk(
|
|
||||||
'goals/completeGoal',
|
|
||||||
async ({ goalId, completionData }: { goalId: string; completionData?: CompleteGoalRequest }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.completeGoal(goalId, completionData);
|
|
||||||
return { goalId, completion: response.data };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '记录目标完成失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标完成记录
|
|
||||||
*/
|
|
||||||
export const fetchGoalCompletions = createAsyncThunk(
|
|
||||||
'goals/fetchGoalCompletions',
|
|
||||||
async ({ goalId, query }: { goalId: string; query?: GetGoalCompletionsQuery }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.getGoalCompletions(goalId, query);
|
|
||||||
return { query, response: response.data };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '获取完成记录失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取目标统计
|
|
||||||
*/
|
|
||||||
export const fetchGoalStats = createAsyncThunk(
|
|
||||||
'goals/fetchGoalStats',
|
|
||||||
async (_, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.getGoalStats();
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '获取目标统计失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量操作目标
|
|
||||||
*/
|
|
||||||
export const batchOperateGoals = createAsyncThunk(
|
|
||||||
'goals/batchOperateGoals',
|
|
||||||
async (operationData: BatchGoalOperationRequest, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await goalsApi.batchOperateGoals(operationData);
|
|
||||||
return { operation: operationData, results: response.data };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '批量操作失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Redux Slice
|
|
||||||
const goalsSlice = createSlice({
|
|
||||||
name: 'goals',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
// 设置筛选条件
|
|
||||||
setFilters: (state, action: PayloadAction<Partial<GetGoalsQuery>>) => {
|
|
||||||
state.filters = { ...state.filters, ...action.payload };
|
|
||||||
},
|
|
||||||
|
|
||||||
// 重置筛选条件
|
|
||||||
resetFilters: (state) => {
|
|
||||||
state.filters = {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
sortBy: 'createdAt',
|
|
||||||
sortOrder: 'desc',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// 清除错误
|
|
||||||
clearErrors: (state) => {
|
|
||||||
state.goalsError = null;
|
|
||||||
state.currentGoalError = null;
|
|
||||||
state.completionsError = null;
|
|
||||||
state.statsError = null;
|
|
||||||
state.createError = null;
|
|
||||||
state.updateError = null;
|
|
||||||
state.batchError = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 清除当前目标详情
|
|
||||||
clearCurrentGoal: (state) => {
|
|
||||||
state.currentGoal = null;
|
|
||||||
state.currentGoalError = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 本地更新目标状态(用于乐观更新)
|
|
||||||
updateGoalStatus: (state, action: PayloadAction<{ goalId: string; status: GoalStatus }>) => {
|
|
||||||
const { goalId, status } = action.payload;
|
|
||||||
|
|
||||||
// 更新目标列表中的状态
|
|
||||||
const goalIndex = state.goals.findIndex(goal => goal.id === goalId);
|
|
||||||
if (goalIndex !== -1) {
|
|
||||||
state.goals[goalIndex].status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前目标详情中的状态
|
|
||||||
if (state.currentGoal && state.currentGoal.id === goalId) {
|
|
||||||
state.currentGoal.status = status;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 本地增加完成次数(用于乐观更新)
|
|
||||||
incrementGoalCompletion: (state, action: PayloadAction<{ goalId: string; count?: number }>) => {
|
|
||||||
const { goalId, count = 1 } = action.payload;
|
|
||||||
|
|
||||||
// 更新目标列表中的完成次数
|
|
||||||
const goalIndex = state.goals.findIndex(goal => goal.id === goalId);
|
|
||||||
if (goalIndex !== -1) {
|
|
||||||
state.goals[goalIndex].completedCount += count;
|
|
||||||
// 重新计算进度百分比
|
|
||||||
if (state.goals[goalIndex].targetCount && state.goals[goalIndex].targetCount > 0) {
|
|
||||||
state.goals[goalIndex].progressPercentage = Math.round(
|
|
||||||
(state.goals[goalIndex].completedCount / state.goals[goalIndex].targetCount) * 100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前目标详情中的完成次数
|
|
||||||
if (state.currentGoal && state.currentGoal.id === goalId) {
|
|
||||||
state.currentGoal.completedCount += count;
|
|
||||||
if (state.currentGoal.targetCount && state.currentGoal.targetCount > 0) {
|
|
||||||
state.currentGoal.progressPercentage = Math.round(
|
|
||||||
(state.currentGoal.completedCount / state.currentGoal.targetCount) * 100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder
|
|
||||||
// 获取目标列表
|
|
||||||
.addCase(fetchGoals.pending, (state) => {
|
|
||||||
state.goalsLoading = true;
|
|
||||||
state.goalsError = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchGoals.fulfilled, (state, action) => {
|
|
||||||
state.goalsLoading = false;
|
|
||||||
const { query, response } = action.payload;
|
|
||||||
|
|
||||||
// 如果是第一页,替换数据;否则追加数据
|
|
||||||
if (query.page === 1) {
|
|
||||||
state.goals = response.list;
|
|
||||||
} else {
|
|
||||||
state.goals = [...state.goals, ...response.list];
|
|
||||||
}
|
|
||||||
|
|
||||||
state.goalsPagination = {
|
|
||||||
page: response.page,
|
|
||||||
pageSize: response.pageSize,
|
|
||||||
total: response.total,
|
|
||||||
hasMore: response.page * response.pageSize < response.total,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.addCase(fetchGoals.rejected, (state, action) => {
|
|
||||||
state.goalsLoading = false;
|
|
||||||
state.goalsError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载更多目标
|
|
||||||
.addCase(loadMoreGoals.pending, (state) => {
|
|
||||||
state.goalsLoading = true;
|
|
||||||
})
|
|
||||||
.addCase(loadMoreGoals.fulfilled, (state, action) => {
|
|
||||||
state.goalsLoading = false;
|
|
||||||
const { response } = action.payload;
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.goals = [...state.goals, ...response.list];
|
|
||||||
state.goalsPagination = {
|
|
||||||
page: response.page,
|
|
||||||
pageSize: response.pageSize,
|
|
||||||
total: response.total,
|
|
||||||
hasMore: response.page * response.pageSize < response.total,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.addCase(loadMoreGoals.rejected, (state, action) => {
|
|
||||||
state.goalsLoading = false;
|
|
||||||
state.goalsError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取目标详情
|
|
||||||
.addCase(fetchGoalDetail.pending, (state) => {
|
|
||||||
state.currentGoalLoading = true;
|
|
||||||
state.currentGoalError = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchGoalDetail.fulfilled, (state, action) => {
|
|
||||||
state.currentGoalLoading = false;
|
|
||||||
state.currentGoal = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(fetchGoalDetail.rejected, (state, action) => {
|
|
||||||
state.currentGoalLoading = false;
|
|
||||||
state.currentGoalError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建目标
|
|
||||||
.addCase(createGoal.pending, (state) => {
|
|
||||||
state.createLoading = true;
|
|
||||||
state.createError = null;
|
|
||||||
})
|
|
||||||
.addCase(createGoal.fulfilled, (state, action) => {
|
|
||||||
state.createLoading = false;
|
|
||||||
// 将新目标添加到列表开头
|
|
||||||
const newGoal: GoalListItem = {
|
|
||||||
...action.payload,
|
|
||||||
progressPercentage: action.payload.targetCount && action.payload.targetCount > 0
|
|
||||||
? Math.round((action.payload.completedCount / action.payload.targetCount) * 100)
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
state.goals.unshift(newGoal);
|
|
||||||
state.goalsPagination.total += 1;
|
|
||||||
})
|
|
||||||
.addCase(createGoal.rejected, (state, action) => {
|
|
||||||
state.createLoading = false;
|
|
||||||
state.createError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新目标
|
|
||||||
.addCase(updateGoal.pending, (state) => {
|
|
||||||
state.updateLoading = true;
|
|
||||||
state.updateError = null;
|
|
||||||
})
|
|
||||||
.addCase(updateGoal.fulfilled, (state, action) => {
|
|
||||||
state.updateLoading = false;
|
|
||||||
const updatedGoal = action.payload;
|
|
||||||
|
|
||||||
console.log('updateGoal.fulfilled', updatedGoal);
|
|
||||||
|
|
||||||
// 更新目标列表中的目标
|
|
||||||
const goalIndex = state.goals.findIndex(goal => goal.id === updatedGoal.id);
|
|
||||||
if (goalIndex !== -1) {
|
|
||||||
state.goals[goalIndex] = {
|
|
||||||
...state.goals[goalIndex],
|
|
||||||
...updatedGoal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前目标详情
|
|
||||||
if (state.currentGoal && state.currentGoal.id === updatedGoal.id) {
|
|
||||||
state.currentGoal = {
|
|
||||||
...state.currentGoal,
|
|
||||||
...updatedGoal,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addCase(updateGoal.rejected, (state, action) => {
|
|
||||||
state.updateLoading = false;
|
|
||||||
state.updateError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 删除目标
|
|
||||||
.addCase(deleteGoal.fulfilled, (state, action) => {
|
|
||||||
const goalId = action.payload;
|
|
||||||
|
|
||||||
// 从目标列表中移除
|
|
||||||
state.goals = state.goals.filter(goal => goal.id !== goalId);
|
|
||||||
state.goalsPagination.total -= 1;
|
|
||||||
|
|
||||||
// 如果删除的是当前查看的目标,清除详情
|
|
||||||
if (state.currentGoal && state.currentGoal.id === goalId) {
|
|
||||||
state.currentGoal = null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 记录目标完成
|
|
||||||
.addCase(completeGoal.fulfilled, (state, action) => {
|
|
||||||
const { goalId, completion } = action.payload;
|
|
||||||
|
|
||||||
// 增加完成次数
|
|
||||||
goalsSlice.caseReducers.incrementGoalCompletion(state, {
|
|
||||||
type: 'goals/incrementGoalCompletion',
|
|
||||||
payload: { goalId, count: completion.completionCount },
|
|
||||||
});
|
|
||||||
|
|
||||||
// 将完成记录添加到列表开头
|
|
||||||
state.completions.unshift(completion);
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取完成记录
|
|
||||||
.addCase(fetchGoalCompletions.pending, (state) => {
|
|
||||||
state.completionsLoading = true;
|
|
||||||
state.completionsError = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchGoalCompletions.fulfilled, (state, action) => {
|
|
||||||
state.completionsLoading = false;
|
|
||||||
const { query, response } = action.payload;
|
|
||||||
|
|
||||||
// 如果是第一页,替换数据;否则追加数据
|
|
||||||
if (query?.page === 1) {
|
|
||||||
state.completions = response.list;
|
|
||||||
} else {
|
|
||||||
state.completions = [...state.completions, ...response.list];
|
|
||||||
}
|
|
||||||
|
|
||||||
state.completionsPagination = {
|
|
||||||
page: response.page,
|
|
||||||
pageSize: response.pageSize,
|
|
||||||
total: response.total,
|
|
||||||
hasMore: response.page * response.pageSize < response.total,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.addCase(fetchGoalCompletions.rejected, (state, action) => {
|
|
||||||
state.completionsLoading = false;
|
|
||||||
state.completionsError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取目标统计
|
|
||||||
.addCase(fetchGoalStats.pending, (state) => {
|
|
||||||
state.statsLoading = true;
|
|
||||||
state.statsError = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchGoalStats.fulfilled, (state, action) => {
|
|
||||||
state.statsLoading = false;
|
|
||||||
state.stats = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(fetchGoalStats.rejected, (state, action) => {
|
|
||||||
state.statsLoading = false;
|
|
||||||
state.statsError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 批量操作
|
|
||||||
.addCase(batchOperateGoals.pending, (state) => {
|
|
||||||
state.batchLoading = true;
|
|
||||||
state.batchError = null;
|
|
||||||
})
|
|
||||||
.addCase(batchOperateGoals.fulfilled, (state, action) => {
|
|
||||||
state.batchLoading = false;
|
|
||||||
const { operation, results } = action.payload;
|
|
||||||
|
|
||||||
// 根据操作类型更新状态
|
|
||||||
results.forEach(result => {
|
|
||||||
if (result.success) {
|
|
||||||
const goalIndex = state.goals.findIndex(goal => goal.id === result.goalId);
|
|
||||||
if (goalIndex !== -1) {
|
|
||||||
switch (operation.action) {
|
|
||||||
case 'pause':
|
|
||||||
state.goals[goalIndex].status = 'paused';
|
|
||||||
break;
|
|
||||||
case 'resume':
|
|
||||||
state.goals[goalIndex].status = 'active';
|
|
||||||
break;
|
|
||||||
case 'complete':
|
|
||||||
state.goals[goalIndex].status = 'completed';
|
|
||||||
break;
|
|
||||||
case 'delete':
|
|
||||||
state.goals = state.goals.filter(goal => goal.id !== result.goalId);
|
|
||||||
state.goalsPagination.total -= 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.addCase(batchOperateGoals.rejected, (state, action) => {
|
|
||||||
state.batchLoading = false;
|
|
||||||
state.batchError = action.payload as string;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
setFilters,
|
|
||||||
resetFilters,
|
|
||||||
clearErrors,
|
|
||||||
clearCurrentGoal,
|
|
||||||
updateGoalStatus,
|
|
||||||
incrementGoalCompletion,
|
|
||||||
} = goalsSlice.actions;
|
|
||||||
|
|
||||||
export default goalsSlice.reducer;
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
|
import { persistActiveFastingSchedule } from '@/utils/fasting';
|
||||||
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
|
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
|
||||||
import challengesReducer from './challengesSlice';
|
import challengesReducer from './challengesSlice';
|
||||||
import checkinReducer, { addExercise, autoSyncCheckin, removeExercise, replaceExercises, setNote, toggleExerciseCompleted } from './checkinSlice';
|
import checkinReducer, { addExercise, autoSyncCheckin, removeExercise, replaceExercises, setNote, toggleExerciseCompleted } from './checkinSlice';
|
||||||
import circumferenceReducer from './circumferenceSlice';
|
import circumferenceReducer from './circumferenceSlice';
|
||||||
import exerciseLibraryReducer from './exerciseLibrarySlice';
|
import exerciseLibraryReducer from './exerciseLibrarySlice';
|
||||||
import foodLibraryReducer from './foodLibrarySlice';
|
|
||||||
import foodRecognitionReducer from './foodRecognitionSlice';
|
|
||||||
import membershipReducer from './membershipSlice';
|
|
||||||
import goalsReducer from './goalsSlice';
|
|
||||||
import healthReducer from './healthSlice';
|
|
||||||
import fastingReducer, {
|
import fastingReducer, {
|
||||||
clearActiveSchedule,
|
clearActiveSchedule,
|
||||||
completeActiveSchedule,
|
completeActiveSchedule,
|
||||||
@@ -15,15 +11,17 @@ import fastingReducer, {
|
|||||||
scheduleFastingPlan,
|
scheduleFastingPlan,
|
||||||
setRecommendedSchedule,
|
setRecommendedSchedule,
|
||||||
} from './fastingSlice';
|
} from './fastingSlice';
|
||||||
|
import foodLibraryReducer from './foodLibrarySlice';
|
||||||
|
import foodRecognitionReducer from './foodRecognitionSlice';
|
||||||
|
import healthReducer from './healthSlice';
|
||||||
|
import membershipReducer from './membershipSlice';
|
||||||
import moodReducer from './moodSlice';
|
import moodReducer from './moodSlice';
|
||||||
import nutritionReducer from './nutritionSlice';
|
import nutritionReducer from './nutritionSlice';
|
||||||
import scheduleExerciseReducer from './scheduleExerciseSlice';
|
import scheduleExerciseReducer from './scheduleExerciseSlice';
|
||||||
import tasksReducer from './tasksSlice';
|
|
||||||
import trainingPlanReducer from './trainingPlanSlice';
|
import trainingPlanReducer from './trainingPlanSlice';
|
||||||
import userReducer from './userSlice';
|
import userReducer from './userSlice';
|
||||||
import waterReducer from './waterSlice';
|
import waterReducer from './waterSlice';
|
||||||
import workoutReducer from './workoutSlice';
|
import workoutReducer from './workoutSlice';
|
||||||
import { persistActiveFastingSchedule } from '@/utils/fasting';
|
|
||||||
|
|
||||||
// 创建监听器中间件来处理自动同步
|
// 创建监听器中间件来处理自动同步
|
||||||
const listenerMiddleware = createListenerMiddleware();
|
const listenerMiddleware = createListenerMiddleware();
|
||||||
@@ -99,11 +97,9 @@ export const store = configureStore({
|
|||||||
challenges: challengesReducer,
|
challenges: challengesReducer,
|
||||||
checkin: checkinReducer,
|
checkin: checkinReducer,
|
||||||
circumference: circumferenceReducer,
|
circumference: circumferenceReducer,
|
||||||
goals: goalsReducer,
|
|
||||||
health: healthReducer,
|
health: healthReducer,
|
||||||
mood: moodReducer,
|
mood: moodReducer,
|
||||||
nutrition: nutritionReducer,
|
nutrition: nutritionReducer,
|
||||||
tasks: tasksReducer,
|
|
||||||
trainingPlan: trainingPlanReducer,
|
trainingPlan: trainingPlanReducer,
|
||||||
scheduleExercise: scheduleExerciseReducer,
|
scheduleExercise: scheduleExerciseReducer,
|
||||||
exerciseLibrary: exerciseLibraryReducer,
|
exerciseLibrary: exerciseLibraryReducer,
|
||||||
|
|||||||
@@ -1,322 +0,0 @@
|
|||||||
import { tasksApi } from '@/services/tasksApi';
|
|
||||||
import {
|
|
||||||
CompleteTaskRequest,
|
|
||||||
GetTasksQuery,
|
|
||||||
SkipTaskRequest,
|
|
||||||
TaskListItem,
|
|
||||||
TaskStats,
|
|
||||||
} from '@/types/goals';
|
|
||||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
// 任务管理状态类型
|
|
||||||
export interface TasksState {
|
|
||||||
// 任务列表
|
|
||||||
tasks: TaskListItem[];
|
|
||||||
tasksLoading: boolean;
|
|
||||||
tasksError: string | null;
|
|
||||||
tasksPagination: {
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 任务统计
|
|
||||||
stats: TaskStats | null;
|
|
||||||
statsLoading: boolean;
|
|
||||||
statsError: string | null;
|
|
||||||
|
|
||||||
// 完成任务
|
|
||||||
completeLoading: boolean;
|
|
||||||
completeError: string | null;
|
|
||||||
|
|
||||||
// 跳过任务
|
|
||||||
skipLoading: boolean;
|
|
||||||
skipError: string | null;
|
|
||||||
|
|
||||||
// 筛选和搜索
|
|
||||||
filters: GetTasksQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: TasksState = {
|
|
||||||
tasks: [],
|
|
||||||
tasksLoading: false,
|
|
||||||
tasksError: null,
|
|
||||||
tasksPagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
total: 0,
|
|
||||||
hasMore: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
stats: null,
|
|
||||||
statsLoading: false,
|
|
||||||
statsError: null,
|
|
||||||
|
|
||||||
completeLoading: false,
|
|
||||||
completeError: null,
|
|
||||||
|
|
||||||
skipLoading: false,
|
|
||||||
skipError: null,
|
|
||||||
|
|
||||||
filters: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 异步操作
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取任务列表
|
|
||||||
*/
|
|
||||||
export const fetchTasks = createAsyncThunk(
|
|
||||||
'tasks/fetchTasks',
|
|
||||||
async (query: GetTasksQuery = {}, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
console.log('fetchTasks', fetchTasks);
|
|
||||||
|
|
||||||
const response = await tasksApi.getTasks(query);
|
|
||||||
console.log('fetchTasks response', response);
|
|
||||||
return { query, response };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '获取任务列表失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载更多任务
|
|
||||||
*/
|
|
||||||
export const loadMoreTasks = createAsyncThunk(
|
|
||||||
'tasks/loadMoreTasks',
|
|
||||||
async (_, { getState, rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const state = getState() as { tasks: TasksState };
|
|
||||||
const { filters, tasksPagination } = state.tasks;
|
|
||||||
|
|
||||||
if (!tasksPagination.hasMore) {
|
|
||||||
return { tasks: [], pagination: tasksPagination };
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
...filters,
|
|
||||||
page: tasksPagination.page + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await tasksApi.getTasks(query);
|
|
||||||
console.log('loadMoreTasks response', response);
|
|
||||||
|
|
||||||
return { query, response };
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '加载更多任务失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 完成任务
|
|
||||||
*/
|
|
||||||
export const completeTask = createAsyncThunk(
|
|
||||||
'tasks/completeTask',
|
|
||||||
async ({ taskId, completionData }: { taskId: string; completionData?: CompleteTaskRequest }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await tasksApi.completeTask(taskId, completionData);
|
|
||||||
console.log('completeTask response', response);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '完成任务失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 跳过任务
|
|
||||||
*/
|
|
||||||
export const skipTask = createAsyncThunk(
|
|
||||||
'tasks/skipTask',
|
|
||||||
async ({ taskId, skipData }: { taskId: string; skipData?: SkipTaskRequest }, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await tasksApi.skipTask(taskId, skipData);
|
|
||||||
console.log('skipTask response', response);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '跳过任务失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取任务统计
|
|
||||||
*/
|
|
||||||
export const fetchTaskStats = createAsyncThunk(
|
|
||||||
'tasks/fetchTaskStats',
|
|
||||||
async (goalId: string, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
const response = await tasksApi.getTaskStats(goalId);
|
|
||||||
console.log('fetchTaskStats response', response);
|
|
||||||
return response;
|
|
||||||
} catch (error: any) {
|
|
||||||
return rejectWithValue(error.message || '获取任务统计失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Redux Slice
|
|
||||||
const tasksSlice = createSlice({
|
|
||||||
name: 'tasks',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
// 清除错误
|
|
||||||
clearErrors: (state) => {
|
|
||||||
state.tasksError = null;
|
|
||||||
state.completeError = null;
|
|
||||||
state.skipError = null;
|
|
||||||
state.statsError = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 更新筛选条件
|
|
||||||
updateFilters: (state, action: PayloadAction<Partial<GetTasksQuery>>) => {
|
|
||||||
state.filters = { ...state.filters, ...action.payload };
|
|
||||||
},
|
|
||||||
|
|
||||||
// 重置筛选条件
|
|
||||||
resetFilters: (state) => {
|
|
||||||
state.filters = {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// 乐观更新任务完成状态
|
|
||||||
optimisticCompleteTask: (state, action: PayloadAction<{ taskId: string; count?: number }>) => {
|
|
||||||
const { taskId, count = 1 } = action.payload;
|
|
||||||
const task = state.tasks.find(t => t.id === taskId);
|
|
||||||
if (task) {
|
|
||||||
const newCount = Math.min(task.currentCount + count, task.targetCount);
|
|
||||||
task.currentCount = newCount;
|
|
||||||
task.progressPercentage = Math.round((newCount / task.targetCount) * 100);
|
|
||||||
|
|
||||||
if (newCount >= task.targetCount) {
|
|
||||||
task.status = 'completed';
|
|
||||||
task.completedAt = new Date().toISOString();
|
|
||||||
} else if (newCount > 0) {
|
|
||||||
task.status = 'in_progress';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder
|
|
||||||
// 获取任务列表
|
|
||||||
.addCase(fetchTasks.pending, (state) => {
|
|
||||||
state.tasksLoading = true;
|
|
||||||
state.tasksError = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchTasks.fulfilled, (state, action) => {
|
|
||||||
state.tasksLoading = false;
|
|
||||||
const { query, response } = action.payload;
|
|
||||||
|
|
||||||
// 如果是第一页,替换数据;否则追加数据
|
|
||||||
state.tasks = response.list;
|
|
||||||
console.log('state.tasks', state.tasks);
|
|
||||||
|
|
||||||
state.tasksPagination = {
|
|
||||||
page: response.page,
|
|
||||||
pageSize: response.pageSize,
|
|
||||||
total: response.total,
|
|
||||||
hasMore: response.page * response.pageSize < response.total,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.addCase(fetchTasks.rejected, (state, action) => {
|
|
||||||
state.tasksLoading = false;
|
|
||||||
state.tasksError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载更多任务
|
|
||||||
.addCase(loadMoreTasks.pending, (state) => {
|
|
||||||
state.tasksLoading = true;
|
|
||||||
})
|
|
||||||
.addCase(loadMoreTasks.fulfilled, (state, action) => {
|
|
||||||
state.tasksLoading = false;
|
|
||||||
const { response } = action.payload;
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.tasks = [...state.tasks, ...response.list];
|
|
||||||
state.tasksPagination = {
|
|
||||||
page: response.page,
|
|
||||||
pageSize: response.pageSize,
|
|
||||||
total: response.total,
|
|
||||||
hasMore: response.page * response.pageSize < response.total,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.addCase(loadMoreTasks.rejected, (state, action) => {
|
|
||||||
state.tasksLoading = false;
|
|
||||||
state.tasksError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 完成任务
|
|
||||||
.addCase(completeTask.pending, (state) => {
|
|
||||||
state.completeLoading = true;
|
|
||||||
state.completeError = null;
|
|
||||||
})
|
|
||||||
.addCase(completeTask.fulfilled, (state, action) => {
|
|
||||||
state.completeLoading = false;
|
|
||||||
// 更新任务列表中的对应任务
|
|
||||||
const updatedTask = action.payload;
|
|
||||||
const index = state.tasks.findIndex(t => t.id === updatedTask.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
state.tasks[index] = updatedTask;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addCase(completeTask.rejected, (state, action) => {
|
|
||||||
state.completeLoading = false;
|
|
||||||
state.completeError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 跳过任务
|
|
||||||
.addCase(skipTask.pending, (state) => {
|
|
||||||
state.skipLoading = true;
|
|
||||||
state.skipError = null;
|
|
||||||
})
|
|
||||||
.addCase(skipTask.fulfilled, (state, action) => {
|
|
||||||
state.skipLoading = false;
|
|
||||||
// 更新任务列表中的对应任务
|
|
||||||
const updatedTask = action.payload;
|
|
||||||
const index = state.tasks.findIndex(t => t.id === updatedTask.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
state.tasks[index] = updatedTask;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addCase(skipTask.rejected, (state, action) => {
|
|
||||||
state.skipLoading = false;
|
|
||||||
state.skipError = action.payload as string;
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取任务统计
|
|
||||||
.addCase(fetchTaskStats.pending, (state) => {
|
|
||||||
state.statsLoading = true;
|
|
||||||
state.statsError = null;
|
|
||||||
})
|
|
||||||
.addCase(fetchTaskStats.fulfilled, (state, action) => {
|
|
||||||
state.statsLoading = false;
|
|
||||||
state.stats = action.payload;
|
|
||||||
})
|
|
||||||
.addCase(fetchTaskStats.rejected, (state, action) => {
|
|
||||||
state.statsLoading = false;
|
|
||||||
state.statsError = action.payload as string;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
clearErrors,
|
|
||||||
updateFilters,
|
|
||||||
resetFilters,
|
|
||||||
optimisticCompleteTask,
|
|
||||||
} = tasksSlice.actions;
|
|
||||||
|
|
||||||
export default tasksSlice.reducer;
|
|
||||||
251
types/goals.ts
251
types/goals.ts
@@ -1,251 +0,0 @@
|
|||||||
// 目标管理相关类型定义
|
|
||||||
|
|
||||||
export type RepeatType = 'daily' | 'weekly' | 'monthly';
|
|
||||||
|
|
||||||
export type GoalStatus = 'active' | 'paused' | 'completed' | 'cancelled';
|
|
||||||
|
|
||||||
export type GoalPriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
|
||||||
|
|
||||||
// 自定义重复规则
|
|
||||||
export interface CustomRepeatRule {
|
|
||||||
weekdays?: number[]; // 0-6,0为周日
|
|
||||||
dayOfMonth?: number[]; // 1-31
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提醒设置
|
|
||||||
export interface ReminderSettings {
|
|
||||||
enabled: boolean;
|
|
||||||
weekdays?: number[]; // 0-6,0为周日
|
|
||||||
monthDays?: number[]; // 1-31,每月几号
|
|
||||||
sound?: string;
|
|
||||||
vibration?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标数据结构
|
|
||||||
export interface Goal {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
repeatType: RepeatType;
|
|
||||||
frequency: number;
|
|
||||||
customRepeatRule?: CustomRepeatRule;
|
|
||||||
startDate: string; // ISO date string
|
|
||||||
endDate?: string; // ISO date string
|
|
||||||
startTime: number; // HH:mm format
|
|
||||||
endTime: number; // HH:mm format
|
|
||||||
status: GoalStatus;
|
|
||||||
completedCount: number;
|
|
||||||
targetCount?: number;
|
|
||||||
category?: string;
|
|
||||||
priority: GoalPriority;
|
|
||||||
hasReminder: boolean;
|
|
||||||
reminderTime?: string; // HH:mm format
|
|
||||||
reminderSettings?: ReminderSettings;
|
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标完成记录
|
|
||||||
export interface GoalCompletion {
|
|
||||||
id: string;
|
|
||||||
goalId: string;
|
|
||||||
userId: string;
|
|
||||||
completedAt: string; // ISO datetime string
|
|
||||||
completionCount: number;
|
|
||||||
notes?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建目标的请求数据
|
|
||||||
export interface CreateGoalRequest {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
repeatType: RepeatType;
|
|
||||||
frequency: number;
|
|
||||||
customRepeatRule?: CustomRepeatRule;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
startTime?: number; // 单位:分钟
|
|
||||||
endTime?: number; // 单位:分钟
|
|
||||||
targetCount?: number;
|
|
||||||
category?: string;
|
|
||||||
priority: GoalPriority;
|
|
||||||
hasReminder: boolean;
|
|
||||||
reminderTime?: string;
|
|
||||||
reminderSettings?: ReminderSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新目标的请求数据
|
|
||||||
export interface UpdateGoalRequest {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
repeatType?: RepeatType;
|
|
||||||
frequency?: number;
|
|
||||||
customRepeatRule?: CustomRepeatRule;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
targetCount?: number;
|
|
||||||
category?: string;
|
|
||||||
priority?: GoalPriority;
|
|
||||||
hasReminder?: boolean;
|
|
||||||
reminderTime?: string;
|
|
||||||
reminderSettings?: ReminderSettings;
|
|
||||||
status?: GoalStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录目标完成的请求数据
|
|
||||||
export interface CompleteGoalRequest {
|
|
||||||
completionCount?: number;
|
|
||||||
notes?: string;
|
|
||||||
completedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取目标列表的查询参数
|
|
||||||
export interface GetGoalsQuery {
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
status?: GoalStatus;
|
|
||||||
repeatType?: RepeatType;
|
|
||||||
category?: string;
|
|
||||||
search?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
sortBy?: 'createdAt' | 'updatedAt' | 'priority' | 'title' | 'startDate';
|
|
||||||
sortOrder?: 'asc' | 'desc';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取目标完成记录的查询参数
|
|
||||||
export interface GetGoalCompletionsQuery {
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量操作目标的请求数据
|
|
||||||
export interface BatchGoalOperationRequest {
|
|
||||||
goalIds: string[];
|
|
||||||
action: 'pause' | 'resume' | 'complete' | 'delete';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量操作结果
|
|
||||||
export interface BatchGoalOperationResult {
|
|
||||||
goalId: string;
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标统计信息
|
|
||||||
export interface GoalStats {
|
|
||||||
total: number;
|
|
||||||
active: number;
|
|
||||||
completed: number;
|
|
||||||
paused: number;
|
|
||||||
cancelled: number;
|
|
||||||
byCategory: Record<string, number>;
|
|
||||||
byRepeatType: Record<RepeatType, number>;
|
|
||||||
totalCompletions: number;
|
|
||||||
thisWeekCompletions: number;
|
|
||||||
thisMonthCompletions: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API响应格式
|
|
||||||
export interface ApiResponse<T> {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页响应格式
|
|
||||||
export interface PaginatedResponse<T> {
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
list: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标详情响应(包含完成记录)
|
|
||||||
export interface GoalDetailResponse extends Goal {
|
|
||||||
progressPercentage: number;
|
|
||||||
daysRemaining?: number;
|
|
||||||
completions: GoalCompletion[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标列表项响应
|
|
||||||
export interface GoalListItem extends Goal {
|
|
||||||
progressPercentage: number;
|
|
||||||
daysRemaining?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任务相关类型定义
|
|
||||||
|
|
||||||
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'overdue' | 'skipped';
|
|
||||||
|
|
||||||
// 任务数据结构
|
|
||||||
export interface Task {
|
|
||||||
id: string;
|
|
||||||
goalId: string;
|
|
||||||
userId: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
startDate: string; // ISO date string
|
|
||||||
endDate: string; // ISO date string
|
|
||||||
targetCount: number;
|
|
||||||
currentCount: number;
|
|
||||||
status: TaskStatus;
|
|
||||||
progressPercentage: number;
|
|
||||||
completedAt?: string; // ISO datetime string
|
|
||||||
notes?: string;
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
daysRemaining: number;
|
|
||||||
isToday: boolean;
|
|
||||||
goal?: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
repeatType: RepeatType;
|
|
||||||
frequency: number;
|
|
||||||
category?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取任务列表的查询参数
|
|
||||||
export interface GetTasksQuery {
|
|
||||||
goalId?: string;
|
|
||||||
status?: TaskStatus;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完成任务的请求数据
|
|
||||||
export interface CompleteTaskRequest {
|
|
||||||
count?: number;
|
|
||||||
notes?: string;
|
|
||||||
completedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳过任务的请求数据
|
|
||||||
export interface SkipTaskRequest {
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任务统计信息
|
|
||||||
export interface TaskStats {
|
|
||||||
total: number;
|
|
||||||
pending: number;
|
|
||||||
inProgress: number;
|
|
||||||
completed: number;
|
|
||||||
overdue: number;
|
|
||||||
skipped: number;
|
|
||||||
totalProgress: number;
|
|
||||||
todayTasks: number;
|
|
||||||
weekTasks: number;
|
|
||||||
monthTasks: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任务列表项响应
|
|
||||||
export interface TaskListItem extends Task {
|
|
||||||
// 继承Task的所有属性
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
|
|
||||||
// 引导状态存储键
|
// 引导状态存储键
|
||||||
const GUIDE_KEYS = {
|
const GUIDE_KEYS = {} as const;
|
||||||
GOALS_PAGE: '@guide_goals_page_completed',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查用户是否已经完成特定引导
|
* 检查用户是否已经完成特定引导
|
||||||
* @param guideKey 引导键名
|
* @param guideKey 引导键名
|
||||||
* @returns Promise<boolean> 是否已完成
|
* @returns Promise<boolean> 是否已完成
|
||||||
*/
|
*/
|
||||||
export const checkGuideCompleted = async (guideKey: keyof typeof GUIDE_KEYS): Promise<boolean> => {
|
export const checkGuideCompleted = async (guideKey: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const completed = await AsyncStorage.getItem(GUIDE_KEYS[guideKey]);
|
const completed = await AsyncStorage.getItem(guideKey);
|
||||||
return completed === 'true';
|
return completed === 'true';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查引导状态失败:', error);
|
console.error('检查引导状态失败:', error);
|
||||||
@@ -24,9 +22,9 @@ export const checkGuideCompleted = async (guideKey: keyof typeof GUIDE_KEYS): Pr
|
|||||||
* 标记引导为已完成
|
* 标记引导为已完成
|
||||||
* @param guideKey 引导键名
|
* @param guideKey 引导键名
|
||||||
*/
|
*/
|
||||||
export const markGuideCompleted = async (guideKey: keyof typeof GUIDE_KEYS): Promise<void> => {
|
export const markGuideCompleted = async (guideKey: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(GUIDE_KEYS[guideKey], 'true');
|
await AsyncStorage.setItem(guideKey, 'true');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存引导状态失败:', error);
|
console.error('保存引导状态失败:', error);
|
||||||
}
|
}
|
||||||
@@ -37,9 +35,8 @@ export const markGuideCompleted = async (guideKey: keyof typeof GUIDE_KEYS): Pro
|
|||||||
*/
|
*/
|
||||||
export const resetAllGuides = async (): Promise<void> => {
|
export const resetAllGuides = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const keys = Object.values(GUIDE_KEYS);
|
// 由于没有引导键,这个函数现在什么都不做
|
||||||
await AsyncStorage.multiRemove(keys);
|
console.log('所有引导状态已重置(无引导键)');
|
||||||
console.log('所有引导状态已重置');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重置引导状态失败:', error);
|
console.error('重置引导状态失败:', error);
|
||||||
}
|
}
|
||||||
@@ -51,15 +48,8 @@ export const resetAllGuides = async (): Promise<void> => {
|
|||||||
*/
|
*/
|
||||||
export const getAllGuideStatus = async (): Promise<Record<string, boolean>> => {
|
export const getAllGuideStatus = async (): Promise<Record<string, boolean>> => {
|
||||||
try {
|
try {
|
||||||
const result: Record<string, boolean> = {};
|
// 由于没有引导键,返回空对象
|
||||||
const keys = Object.values(GUIDE_KEYS);
|
return {};
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const completed = await AsyncStorage.getItem(key);
|
|
||||||
result[key] = completed === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取引导状态失败:', error);
|
console.error('获取引导状态失败:', error);
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -78,214 +78,7 @@ export class WorkoutNotificationHelpers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// GoalNotificationHelpers 类已删除,因为目标功能已移除
|
||||||
* 目标相关的通知辅助函数
|
|
||||||
*/
|
|
||||||
export class GoalNotificationHelpers {
|
|
||||||
/**
|
|
||||||
* 发送目标达成通知
|
|
||||||
*/
|
|
||||||
static async sendGoalAchievementNotification(userName: string, goalName: string) {
|
|
||||||
return notificationService.sendImmediateNotification({
|
|
||||||
title: '目标达成',
|
|
||||||
body: `${userName},恭喜您达成了目标:${goalName}!`,
|
|
||||||
data: { type: 'goal_achievement', goalName },
|
|
||||||
sound: true,
|
|
||||||
priority: 'high',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送目标进度更新通知
|
|
||||||
*/
|
|
||||||
static async sendGoalProgressNotification(userName: string, goalName: string, progress: number) {
|
|
||||||
return notificationService.sendImmediateNotification({
|
|
||||||
title: '目标进度',
|
|
||||||
body: `${userName},您的目标"${goalName}"已完成${progress}%!`,
|
|
||||||
data: { type: 'goal_progress', goalName, progress },
|
|
||||||
sound: true,
|
|
||||||
priority: 'normal',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安排目标提醒
|
|
||||||
*/
|
|
||||||
static async scheduleGoalReminder(userName: string, goalName: string, deadline: Date) {
|
|
||||||
// 在截止日期前一天发送提醒
|
|
||||||
const reminderDate = new Date(deadline);
|
|
||||||
reminderDate.setDate(reminderDate.getDate() - 1);
|
|
||||||
|
|
||||||
return notificationService.scheduleNotificationAtDate(
|
|
||||||
{
|
|
||||||
title: '目标截止提醒',
|
|
||||||
body: `${userName},您的目标"${goalName}"明天就要截止了,加油!`,
|
|
||||||
data: { type: 'goal_deadline_reminder', goalName },
|
|
||||||
sound: true,
|
|
||||||
priority: 'high',
|
|
||||||
},
|
|
||||||
reminderDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据目标设置创建定时推送
|
|
||||||
* @param goalData 目标数据
|
|
||||||
* @param userName 用户名
|
|
||||||
* @returns 通知ID数组
|
|
||||||
*/
|
|
||||||
static async scheduleGoalNotifications(
|
|
||||||
goalData: {
|
|
||||||
title: string;
|
|
||||||
repeatType: 'daily' | 'weekly' | 'monthly';
|
|
||||||
frequency: number;
|
|
||||||
hasReminder: boolean;
|
|
||||||
reminderTime?: string;
|
|
||||||
customRepeatRule?: {
|
|
||||||
weekdays?: number[];
|
|
||||||
dayOfMonth?: number[];
|
|
||||||
};
|
|
||||||
startTime?: number;
|
|
||||||
},
|
|
||||||
userName: string
|
|
||||||
): Promise<string[]> {
|
|
||||||
const notificationIds: string[] = [];
|
|
||||||
|
|
||||||
// 如果没有开启提醒,直接返回
|
|
||||||
if (!goalData.hasReminder || !goalData.reminderTime) {
|
|
||||||
console.log('目标未开启提醒或未设置提醒时间');
|
|
||||||
return notificationIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 解析提醒时间
|
|
||||||
const [hours, minutes] = goalData.reminderTime.split(':').map(Number);
|
|
||||||
|
|
||||||
// 创建通知内容
|
|
||||||
const notification: NotificationData = {
|
|
||||||
title: '目标提醒',
|
|
||||||
body: `${userName},该完成您的目标"${goalData.title}"了!`,
|
|
||||||
data: {
|
|
||||||
type: 'goal_reminder',
|
|
||||||
goalTitle: goalData.title,
|
|
||||||
repeatType: goalData.repeatType,
|
|
||||||
frequency: goalData.frequency
|
|
||||||
},
|
|
||||||
sound: true,
|
|
||||||
priority: 'high',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据重复类型创建不同的通知
|
|
||||||
switch (goalData.repeatType) {
|
|
||||||
case 'daily':
|
|
||||||
// 每日重复 - 使用日历重复通知
|
|
||||||
const dailyId = await notificationService.scheduleCalendarRepeatingNotification(
|
|
||||||
notification,
|
|
||||||
{
|
|
||||||
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
|
||||||
hour: hours,
|
|
||||||
minute: minutes,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
notificationIds.push(dailyId);
|
|
||||||
console.log(`已安排每日目标提醒,通知ID:${dailyId}`);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'weekly':
|
|
||||||
// 每周重复 - 为每个选中的星期几创建单独的通知
|
|
||||||
if (goalData.customRepeatRule?.weekdays && goalData.customRepeatRule.weekdays.length > 0) {
|
|
||||||
for (const weekday of goalData.customRepeatRule.weekdays) {
|
|
||||||
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
|
|
||||||
notification,
|
|
||||||
{
|
|
||||||
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
|
|
||||||
hour: hours,
|
|
||||||
minute: minutes,
|
|
||||||
weekdays: [weekday],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
notificationIds.push(weeklyId);
|
|
||||||
console.log(`已安排每周目标提醒,星期${weekday},通知ID:${weeklyId}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 默认每周重复
|
|
||||||
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
|
|
||||||
notification,
|
|
||||||
{
|
|
||||||
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
|
|
||||||
hour: hours,
|
|
||||||
minute: minutes,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
notificationIds.push(weeklyId);
|
|
||||||
console.log(`已安排每周目标提醒,通知ID:${weeklyId}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'monthly':
|
|
||||||
// 每月重复 - 为每个选中的日期创建单独的通知
|
|
||||||
if (goalData.customRepeatRule?.dayOfMonth && goalData.customRepeatRule.dayOfMonth.length > 0) {
|
|
||||||
for (const dayOfMonth of goalData.customRepeatRule.dayOfMonth) {
|
|
||||||
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
|
|
||||||
notification,
|
|
||||||
{
|
|
||||||
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
|
|
||||||
hour: hours,
|
|
||||||
minute: minutes,
|
|
||||||
dayOfMonth: dayOfMonth,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
notificationIds.push(monthlyId);
|
|
||||||
console.log(`已安排每月目标提醒,${dayOfMonth}号,通知ID:${monthlyId}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 默认每月重复
|
|
||||||
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
|
|
||||||
notification,
|
|
||||||
{
|
|
||||||
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
|
|
||||||
hour: hours,
|
|
||||||
minute: minutes,
|
|
||||||
dayOfMonth: 1,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
notificationIds.push(monthlyId);
|
|
||||||
console.log(`已安排每月目标提醒,通知ID:${monthlyId}`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`目标"${goalData.title}"的定时推送已创建完成,共${notificationIds.length}个通知`);
|
|
||||||
return notificationIds;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建目标定时推送失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消特定目标的所有通知
|
|
||||||
*/
|
|
||||||
static async cancelGoalNotifications(goalTitle: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const notifications = await notificationService.getAllScheduledNotifications();
|
|
||||||
|
|
||||||
for (const notification of notifications) {
|
|
||||||
if (notification.content.data?.type === 'goal_reminder' &&
|
|
||||||
notification.content.data?.goalTitle === goalTitle) {
|
|
||||||
await notificationService.cancelNotification(notification.identifier);
|
|
||||||
console.log(`已取消目标"${goalTitle}"的通知:${notification.identifier}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消目标通知失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChallengeNotificationHelpers {
|
export class ChallengeNotificationHelpers {
|
||||||
static buildChallengesTabUrl(): string {
|
static buildChallengesTabUrl(): string {
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export function generateWelcomeMessage(params: GenerateWelcomeMessageParams): We
|
|||||||
content: `你好,${name}!🐳\n\n我是你的小海豹Seal!发现你还没有完善健康档案呢~让我帮你开启个性化的健康管理之旅吧!\n\n完善档案后,我就能为你量身定制专属的营养方案、运动计划和生活建议啦!`,
|
content: `你好,${name}!🐳\n\n我是你的小海豹Seal!发现你还没有完善健康档案呢~让我帮你开启个性化的健康管理之旅吧!\n\n完善档案后,我就能为你量身定制专属的营养方案、运动计划和生活建议啦!`,
|
||||||
choices: [
|
choices: [
|
||||||
{ id: 'profile_setup', label: '完善我的健康档案', value: '我想要完善健康档案,建立个人健康数据', emoji: '📋', recommended: true },
|
{ id: 'profile_setup', label: '完善我的健康档案', value: '我想要完善健康档案,建立个人健康数据', emoji: '📋', recommended: true },
|
||||||
{ id: 'health_goals', label: '了解健康目标设定', value: '我想了解如何设定合理的健康目标', emoji: '🎯' },
|
|
||||||
{ id: 'nutrition_basics', label: '营养基础知识科普', value: '我想了解一些基础的营养知识', emoji: '🥗' },
|
{ id: 'nutrition_basics', label: '营养基础知识科普', value: '我想了解一些基础的营养知识', emoji: '🥗' },
|
||||||
{ id: 'quick_start', label: '快速开始体验', value: '我想直接体验一下你的功能', emoji: '🚀' }
|
{ id: 'quick_start', label: '快速开始体验', value: '我想直接体验一下你的功能', emoji: '🚀' }
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user