diff --git a/.kiro/steering/chinese-language.md b/.kiro/steering/chinese-language.md deleted file mode 100644 index 61b697f..0000000 --- a/.kiro/steering/chinese-language.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -inclusion: always ---- - -# 中文回复规则 - -请始终使用中文进行回复和编写文档。包括: - -- 所有对话回复都使用中文 -- 代码注释使用中文 -- 文档和说明使用中文 -- 错误信息和提示使用中文 -- 变量名和函数名可以使用英文,但注释和文档说明必须是中文 - -这个规则适用于所有交互,除非用户明确要求使用其他语言。 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index d9da12d..747c09e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,35 +11,38 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Reset project**: `npm run reset-project` ## Architecture -- **Framework**: React Native (Expo) with TypeScript. -- **Navigation**: Expo Router for file-based routing (`app/` directory). -- **State Management**: Redux Toolkit with slices for different domains (user, training plans, workouts, challenges, etc.). -- **UI**: Themed components (`ThemedText`, `ThemedView`) and reusable UI elements (`Collapsible`, `ParallaxScrollView`). -- **API Layer**: Service files for communicating with backend APIs (`services/` directory). -- **Data Persistence**: AsyncStorage for local data storage. -- **Platform-Specific**: Android (`android/`) and iOS (`ios/`) configurations with native modules. -- **Hooks**: Custom hooks for color scheme (`useColorScheme`), theme management (`useThemeColor`), and Redux integration (`useRedux`). -- **Dependencies**: React Navigation for tab-based navigation, Expo modules for native features (haptics, blur, image picking, etc.), and third-party libraries for specific functionality. +- **Framework**: React Native (Expo) with TypeScript using Expo Router for file-based navigation +- **State Management**: Redux Toolkit with domain-specific slices (`store/`) and typed hooks (`hooks/redux.ts`) +- **Authentication**: Custom auth guard system with `useAuthGuard` hook for protected navigation +- **Navigation**: + - File-based routing in `app/` directory with nested layouts + - Tab-based navigation with custom styling and haptic feedback + - Route constants defined in `constants/Routes.ts` +- **UI System**: + - Themed components (`ThemedText`, `ThemedView`) with color scheme support + - Custom icon system with `IconSymbol` component for iOS symbols + - Reusable UI components in `components/ui/` +- **Data Layer**: + - API services in `services/` directory with centralized API client + - AsyncStorage for local persistence + - Background task management for sync operations +- **Native Integration**: + - Health data integration with HealthKit + - Apple Authentication + - Camera and photo library access for posture assessment + - Push notifications with background task support + - Haptic feedback integration -## Key Features -- **Authentication**: Login flow with Apple authentication support. -- **Training Plans**: Creation and management of personalized pilates training plans. -- **Workouts**: Daily workout tracking and session management. -- **AI Features**: AI coach chat and posture assessment capabilities. -- **Health Integration**: Integration with health data tracking. -- **Content Management**: Article reading and educational content. -- **Challenge System**: Challenge participation and progress tracking. -- **User Profiles**: Personal information management and goal setting. +## Key Architecture Patterns +- **Redux Auto-sync**: Listener middleware automatically syncs checkin data changes to backend +- **Type-safe Navigation**: Uses Expo Router with TypeScript for route type safety +- **Authentication Flow**: `pushIfAuthedElseLogin` function handles auth-protected navigation +- **Theme System**: Dynamic theming with light/dark mode support and color tokens +- **Service Layer**: Centralized API client with interceptors and error handling -## Directory Structure -- `app/`: Main application screens and routing -- `components/`: Reusable UI components -- `constants/`: Application constants and configuration -- `hooks/`: Custom React hooks -- `services/`: API service layer -- `store/`: Redux store and slices -- `types/`: TypeScript type definitions - - -## rules -- 路由跳转使用 pushIfAuthedElseLogin \ No newline at end of file +## Development Conventions +- Use absolute imports with `@/` prefix for all internal imports +- Follow existing Redux slice patterns for state management +- Implement auth guards using `useAuthGuard` hook for protected features +- Use themed components for consistent styling +- Follow established navigation patterns with typed routes \ No newline at end of file diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index a81bbe4..95898ca 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -44,9 +44,9 @@ export default function TabLayout() { case 'explore': return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const; case 'coach': - return { icon: 'person.3.fill', title: 'Seal' } as const; + return { icon: 'message.fill', title: 'AI' } as const; case 'goals': - return { icon: 'flag.fill', title: '目标' } as const; + return { icon: 'flag.fill', title: '习惯' } as const; case 'statistics': return { icon: 'chart.pie.fill', title: '统计' } as const; case 'personal': diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index 455c6ea..97189f5 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -118,8 +118,17 @@ const CardType = { type CardType = typeof CardType[keyof typeof CardType]; +// 定义路由参数类型 +type CoachScreenParams = { + name?: string; + action?: 'diet' | 'weight' | 'mood' | 'workout'; + subAction?: 'record' | 'photo' | 'text' | 'card'; + meal?: 'breakfast' | 'lunch' | 'dinner' | 'snack'; + message?: string; +}; + export default function CoachScreen() { - const params = useLocalSearchParams<{ name?: string }>(); + const params = useLocalSearchParams(); const insets = useSafeAreaInsets(); const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); @@ -389,6 +398,69 @@ export default function CoachScreen() { }; }, [insets.bottom]); + // 处理路由参数动作 + useEffect(() => { + // 确保用户已登录且消息已加载 + if (!isLoggedIn || messages.length === 0) return; + + // 检查是否有动作参数 + if (params.action) { + const executeAction = async () => { + try { + switch (params.action) { + case 'diet': + if (params.subAction === 'card') { + // 插入饮食记录卡片 + insertDietInputCard(); + } else if (params.subAction === 'record' && params.message) { + // 直接发送预设的饮食记录消息 + const mealPrefix = params.meal ? `${getMealDisplayName(params.meal)}` : ''; + const message = `#记饮食:${mealPrefix}${decodeURIComponent(params.message)}`; + await sendStream(message); + } + break; + + case 'weight': + if (params.subAction === 'card') { + // 插入体重记录卡片 + insertWeightInputCard(); + } else if (params.subAction === 'record' && params.message) { + // 直接发送预设的体重记录消息 + const message = `#记体重:${decodeURIComponent(params.message)}`; + await sendStream(message); + } + break; + + case 'mood': + // 跳转到心情记录页面 + pushIfAuthedElseLogin('/mood/calendar'); + break; + + default: + console.warn('未知的动作类型:', params.action); + } + } catch (error) { + console.error('执行路由动作失败:', error); + } + }; + + // 延迟执行,确保页面已完全加载 + const timer = setTimeout(executeAction, 500); + return () => clearTimeout(timer); + } + }, [params.action, params.subAction, params.meal, params.message, isLoggedIn, messages.length]); + + // 获取餐次显示名称 + const getMealDisplayName = (meal: string): string => { + const mealNames: Record = { + breakfast: '早餐', + lunch: '午餐', + dinner: '晚餐', + snack: '加餐' + }; + return mealNames[meal] || ''; + }; + const streamAbortRef = useRef<{ abort: () => void } | null>(null); // 组件卸载时清理流式请求和定时器 diff --git a/app/(tabs)/goals.tsx b/app/(tabs)/goals.tsx index db74077..41a647b 100644 --- a/app/(tabs)/goals.tsx +++ b/app/(tabs)/goals.tsx @@ -1,4 +1,5 @@ -import CreateGoalModal from '@/components/CreateGoalModal'; +import { CreateGoalModal } from '@/components/CreateGoalModal'; +import GoalTemplateModal from '@/components/GoalTemplateModal'; import { GoalsPageGuide } from '@/components/GoalsPageGuide'; import { GuideTestButton } from '@/components/GuideTestButton'; import { TaskCard } from '@/components/TaskCard'; @@ -7,6 +8,7 @@ import { TaskProgressCard } from '@/components/TaskProgressCard'; 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'; @@ -37,9 +39,7 @@ export default function GoalsScreen() { tasksLoading, tasksError, tasksPagination, - completeLoading, completeError, - skipLoading, skipError, } = useAppSelector((state) => state.tasks); @@ -50,11 +50,13 @@ export default function GoalsScreen() { 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('all'); const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗 const [showGuide, setShowGuide] = useState(false); // 控制引导显示 + const [selectedTemplateData, setSelectedTemplateData] = useState | undefined>(); // 页面聚焦时重新加载数据 useFocusEffect( @@ -141,6 +143,29 @@ export default function GoalsScreen() { 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); }; // 创建目标处理函数 @@ -353,10 +378,10 @@ export default function GoalsScreen() { - 今日目标 + 习惯养成 - 让我们检查你的目标! + 自律让我更健康 @@ -378,10 +403,7 @@ export default function GoalsScreen() { { - setModalKey(prev => prev + 1); // 每次打开弹窗时使用新的 key - setShowCreateModal(true); - }} + onPress={handleOpenTemplateModal} > + @@ -420,14 +442,26 @@ export default function GoalsScreen() { /> + {/* 目标模板选择弹窗 */} + setShowTemplateModal(false)} + onSelectTemplate={handleSelectTemplate} + onCreateCustom={handleCreateCustomGoal} + /> + {/* 创建目标弹窗 */} setShowCreateModal(false)} + onClose={() => { + setShowCreateModal(false); + setSelectedTemplateData(undefined); + }} onSubmit={handleCreateGoal} onSuccess={handleModalSuccess} loading={createLoading} + initialData={selectedTemplateData} /> {/* 目标页面引导 */} diff --git a/assets/images/icons/icon-recommend.png b/assets/images/icons/icon-recommend.png new file mode 100644 index 0000000..4fd50dd Binary files /dev/null and b/assets/images/icons/icon-recommend.png differ diff --git a/components/CreateGoalModal.tsx b/components/CreateGoalModal.tsx index 43541e4..1efc825 100644 --- a/components/CreateGoalModal.tsx +++ b/components/CreateGoalModal.tsx @@ -4,7 +4,7 @@ import { CreateGoalRequest, GoalPriority, RepeatType } from '@/types/goals'; import { Ionicons } from '@expo/vector-icons'; import DateTimePicker from '@react-native-community/datetimepicker'; import { LinearGradient } from 'expo-linear-gradient'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Alert, Image, @@ -26,6 +26,7 @@ interface CreateGoalModalProps { onSubmit: (goalData: CreateGoalRequest) => void; onSuccess?: () => void; loading?: boolean; + initialData?: Partial; } const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [ @@ -42,28 +43,49 @@ export const CreateGoalModal: React.FC = ({ onSubmit, onSuccess, loading = false, + initialData, }) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; // 表单状态 - const [title, setTitle] = useState(''); - const [description, setDescription] = useState(''); - const [repeatType, setRepeatType] = useState('daily'); - const [frequency, setFrequency] = useState(1); - const [hasReminder, setHasReminder] = useState(false); + const [title, setTitle] = useState(initialData?.title || ''); + const [description, setDescription] = useState(initialData?.description || ''); + const [repeatType, setRepeatType] = useState(initialData?.repeatType || 'daily'); + const [frequency, setFrequency] = useState(initialData?.frequency || 1); + const [hasReminder, setHasReminder] = useState(initialData?.hasReminder || false); const [showFrequencyPicker, setShowFrequencyPicker] = useState(false); const [showRepeatTypePicker, setShowRepeatTypePicker] = useState(false); - const [reminderTime, setReminderTime] = useState('20:00'); - const [category, setCategory] = useState(''); - const [priority, setPriority] = useState(5); + const [reminderTime, setReminderTime] = useState(initialData?.reminderTime || '20:00'); + const [category, setCategory] = useState(initialData?.category || ''); + const [priority, setPriority] = useState(initialData?.priority || 5); const [showTimePicker, setShowTimePicker] = useState(false); const [tempSelectedTime, setTempSelectedTime] = useState(null); // 周几选择状态 - const [selectedWeekdays, setSelectedWeekdays] = useState([1, 2, 3, 4, 5]); // 默认周一到周五 + const [selectedWeekdays, setSelectedWeekdays] = useState( + initialData?.customRepeatRule?.weekdays || [1, 2, 3, 4, 5] + ); // 默认周一到周五 // 每月日期选择状态 - const [selectedMonthDays, setSelectedMonthDays] = useState([1, 15]); // 默认1号和15号 + const [selectedMonthDays, setSelectedMonthDays] = useState( + initialData?.customRepeatRule?.dayOfMonth || [1, 15] + ); // 默认1号和15号 + + // 当 initialData 变化时更新表单状态 + useEffect(() => { + if (initialData) { + setTitle(initialData.title || ''); + setDescription(initialData.description || ''); + setRepeatType(initialData.repeatType || 'daily'); + setFrequency(initialData.frequency || 1); + setHasReminder(initialData.hasReminder || false); + setReminderTime(initialData.reminderTime || '20:00'); + setCategory(initialData.category || ''); + setPriority(initialData.priority || 5); + setSelectedWeekdays(initialData.customRepeatRule?.weekdays || [1, 2, 3, 4, 5]); + setSelectedMonthDays(initialData.customRepeatRule?.dayOfMonth || [1, 15]); + } + }, [initialData]); // 重置表单 const resetForm = () => { diff --git a/components/GoalTemplateModal.tsx b/components/GoalTemplateModal.tsx new file mode 100644 index 0000000..0938058 --- /dev/null +++ b/components/GoalTemplateModal.tsx @@ -0,0 +1,295 @@ +import { Colors } from '@/constants/Colors'; +import { getTemplatesByCategory, goalCategories, GoalTemplate } from '@/constants/goalTemplates'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import React, { useState } from 'react'; +import { + Dimensions, + Image, + Modal, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View +} from 'react-native'; + +const { width: screenWidth } = Dimensions.get('window'); + +interface GoalTemplateModalProps { + visible: boolean; + onClose: () => void; + onSelectTemplate: (template: GoalTemplate) => void; + onCreateCustom: () => void; +} + +export default function GoalTemplateModal({ + visible, + onClose, + onSelectTemplate, + onCreateCustom +}: GoalTemplateModalProps) { + const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; + const colorTokens = Colors[theme]; + + const [selectedCategory, setSelectedCategory] = useState('recommended'); + const currentTemplates = getTemplatesByCategory(selectedCategory); + + // 渲染分类标签 + const renderCategoryTab = (category: any) => { + const isSelected = selectedCategory === category.id; + return ( + setSelectedCategory(category.id)} + activeOpacity={0.8} + > + {category.isRecommended && ( + + )} + + {category.title} + + + ); + }; + + // 渲染模板项 + const renderTemplateItem = (template: GoalTemplate) => ( + onSelectTemplate(template)} + activeOpacity={0.8} + > + + + + + {template.title} + + + ); + + return ( + + + + + {/* 头部 */} + + + + + + 创建新目标 + + + + + + {/* 创建自定义目标 */} + + + + + 创建自定义目标 + + + {/* 分类选择器 */} + + + {goalCategories.map(renderCategoryTab)} + + + + {/* 当前分类的模板 */} + + + {currentTemplates.map(renderTemplateItem)} + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F9FAFB', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingVertical: 16, + backgroundColor: '#FFFFFF', + borderBottomWidth: 1, + borderBottomColor: '#E5E7EB', + }, + closeButton: { + padding: 4, + }, + title: { + fontSize: 18, + fontWeight: '600', + color: '#111827', + textAlign: 'center', + }, + content: { + flex: 1, + }, + scrollContent: { + paddingBottom: 40, + }, + customGoalButton: { + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: 20, + marginTop: 20, + padding: 16, + backgroundColor: '#FFFFFF', + borderRadius: 16, + borderWidth: 1, + borderColor: '#E5E7EB', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 1, + }, + customGoalIcon: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#F3E8FF', + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + customGoalText: { + fontSize: 16, + fontWeight: '600', + color: '#374151', + }, + categorySection: { + marginTop: 24, + }, + categoryScrollView: { + paddingLeft: 20, + }, + categoryScrollContent: { + paddingRight: 20, + }, + categoryTab: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 10, + marginRight: 12, + borderRadius: 20, + backgroundColor: '#F3F4F6', + }, + categoryTabSelected: { + backgroundColor: '#7C3AED', + }, + categoryTabText: { + fontSize: 14, + fontWeight: '600', + color: '#374151', + }, + categoryTabTextSelected: { + color: '#FFFFFF', + }, + avatarGroup: { + flexDirection: 'row', + marginRight: 8, + }, + avatar: { + width: 16, + height: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: '#FFFFFF', + }, + templatesSection: { + marginTop: 24, + }, + templatesGrid: { + paddingHorizontal: 20, + flexDirection: 'row', + flexWrap: 'wrap', + gap: 12, + }, + templateItem: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#FFFFFF', + minWidth: (screenWidth - 60) / 2, // 2列布局,考虑间距和padding + maxWidth: (screenWidth - 60) / 2, + aspectRatio: 2.2, + borderRadius: 16, + padding: 16, + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 2, + }, + templateIcon: { + width: 24, + height: 24, + alignItems: 'center', + justifyContent: 'center', + }, + templateTitle: { + fontSize: 14, + color: '#374151', + fontWeight: '600', + lineHeight: 20, + marginLeft: 12, + }, +}); \ No newline at end of file diff --git a/components/TaskProgressCard.tsx b/components/TaskProgressCard.tsx index 35dd1ce..ad27bd0 100644 --- a/components/TaskProgressCard.tsx +++ b/components/TaskProgressCard.tsx @@ -30,7 +30,7 @@ export const TaskProgressCard: React.FC = ({ {/* 标题区域 */} - 统计 + 今日 = ({ - + {headerButtons && ( {headerButtons} diff --git a/components/ui/IconSymbol.tsx b/components/ui/IconSymbol.tsx index c2de0cf..161964c 100644 --- a/components/ui/IconSymbol.tsx +++ b/components/ui/IconSymbol.tsx @@ -20,6 +20,7 @@ const MAPPING = { 'chevron.right': 'chevron-right', 'person.fill': 'person', 'person.3.fill': 'people', + 'message.fill': 'message', } as IconMapping; /** diff --git a/constants/goalTemplates.ts b/constants/goalTemplates.ts new file mode 100644 index 0000000..cc8ca22 --- /dev/null +++ b/constants/goalTemplates.ts @@ -0,0 +1,227 @@ +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; + 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; +}; \ No newline at end of file diff --git a/utils/health.ts b/utils/health.ts index 509e2de..f69265a 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -2,6 +2,12 @@ import dayjs from 'dayjs'; import type { HealthActivitySummary, HealthKitPermissions } from 'react-native-health'; import AppleHealthKit from 'react-native-health'; +type HealthDataOptions = { + startDate: string; + endDate: string; +}; + + const PERMISSIONS: HealthKitPermissions = { permissions: { read: [ @@ -61,271 +67,163 @@ export async function ensureHealthPermissions(): Promise { }); } -export async function fetchHealthDataForDate(date: Date): Promise { - console.log('开始获取指定日期健康数据...', date); - - const start = dayjs(date).startOf('day').toDate(); - const end = dayjs(date).endOf('day').toDate(); - - const options = { - startDate: start.toISOString(), - endDate: end.toISOString() - } as any; - - const activitySummaryOptions = { - startDate: start.toISOString(), - endDate: end.toISOString() - }; - - console.log('查询选项:', options); - - // 并行获取所有健康数据,包括ActivitySummary、血氧饱和度和心率 - const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate] = await Promise.all([ - // 获取步数 - new Promise((resolve) => { - AppleHealthKit.getStepCount({ - date: dayjs(date).toISOString() - }, (err, res) => { - if (err) { - console.error('获取步数失败:', err); - return resolve(0); - } - if (!res) { - console.warn('步数数据为空'); - return resolve(0); - } - console.log('步数数据:', res); - resolve(res.value || 0); - }); - }), - - // 获取消耗卡路里 - new Promise((resolve) => { - AppleHealthKit.getActiveEnergyBurned(options, (err, res) => { - if (err) { - console.error('获取消耗卡路里失败:', err); - return resolve(0); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('卡路里数据为空或格式错误'); - return resolve(0); - } - console.log('卡路里数据:', res); - // 求和该日内的所有记录(单位:千卡) - const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); - resolve(total); - }); - }), - - // 获取基础代谢率 - new Promise((resolve) => { - AppleHealthKit.getBasalEnergyBurned(options, (err, res) => { - if (err) { - console.error('获取基础代谢失败:', err); - return resolve(0); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('基础代谢数据为空或格式错误'); - return resolve(0); - } - console.log('基础代谢数据:', res); - // 求和该日内的所有记录(单位:千卡) - const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); - resolve(total); - }); - }), - - // 获取睡眠时长 - new Promise((resolve) => { - AppleHealthKit.getSleepSamples(options, (err, res) => { - if (err) { - console.error('获取睡眠数据失败:', err); - return resolve(0); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('睡眠数据为空或格式错误'); - return resolve(0); - } - console.log('睡眠数据:', res); - - // 计算总睡眠时间(单位:分钟) - let totalSleepDuration = 0; - res.forEach((sample: any) => { - if (sample && sample.startDate && sample.endDate) { - const startTime = new Date(sample.startDate).getTime(); - const endTime = new Date(sample.endDate).getTime(); - const durationMinutes = (endTime - startTime) / (1000 * 60); - totalSleepDuration += durationMinutes; - } - }); - - resolve(totalSleepDuration); - }); - }), - - // 获取HRV数据 - new Promise((resolve) => { - AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { - if (err) { - console.error('获取HRV数据失败:', err); - return resolve(null); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('HRV数据为空或格式错误'); - return resolve(null); - } - console.log('HRV数据:', res); - - // 获取最新的HRV值 - const latestHrv = res[res.length - 1]; - if (latestHrv && latestHrv.value) { - resolve(Math.round(latestHrv.value * 1000)); - } else { - resolve(null); - } - }); - }), - - // 获取ActivitySummary数据(健身圆环数据) - new Promise((resolve) => { - AppleHealthKit.getActivitySummary( - activitySummaryOptions, - (err: Object, results: HealthActivitySummary[]) => { - if (err) { - console.error('获取ActivitySummary失败:', err); - return resolve(null); - } - if (!results || results.length === 0) { - console.warn('ActivitySummary数据为空'); - return resolve(null); - } - console.log('ActivitySummary数据:', results[0]); - resolve(results[0]); - }, - ); - }), - - // 获取血氧饱和度数据 - new Promise((resolve) => { - AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { - if (err) { - console.error('获取血氧饱和度失败:', err); - return resolve(null); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('血氧饱和度数据为空或格式错误'); - return resolve(null); - } - console.log('血氧饱和度数据:', res); - // 获取最新的血氧饱和度值 - const latestOxygen = res[res.length - 1]; - if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) { - let value = Number(latestOxygen.value); - - // 检查数据格式:如果值小于1,可能是小数形式(0.0-1.0),需要转换为百分比 - if (value > 0 && value < 1) { - value = value * 100; - console.log('血氧饱和度数据从小数转换为百分比:', latestOxygen.value, '->', value); - } - - // 血氧饱和度通常在0-100之间,验证数据有效性 - if (value >= 0 && value <= 100) { - resolve(Number(value.toFixed(1))); - } else { - console.warn('血氧饱和度数据异常:', value); - resolve(null); - } - } else { - resolve(null); - } - }); - }), - - // 获取心率数据 - new Promise((resolve) => { - AppleHealthKit.getHeartRateSamples(options, (err, res) => { - if (err) { - console.error('获取心率失败:', err); - return resolve(null); - } - if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('心率数据为空或格式错误'); - return resolve(null); - } - console.log('心率数据:', res); - // 获取最新的心率值 - const latestHeartRate = res[res.length - 1]; - if (latestHeartRate && latestHeartRate.value !== undefined && latestHeartRate.value !== null) { - // 心率通常在30-200之间,验证数据有效性 - const value = Number(latestHeartRate.value); - if (value >= 30 && value <= 200) { - resolve(Math.round(value)); - } else { - console.warn('心率数据异常:', value); - resolve(null); - } - } else { - resolve(null); - } - }); - }) - ]); - - console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate }); - +// 日期工具函数 +function createDateRange(date: Date): HealthDataOptions { return { - steps, - activeEnergyBurned: calories, - basalEnergyBurned: basalMetabolism, - sleepDuration, - hrv, - // 健身圆环数据 - activeCalories: Math.round(activitySummary?.activeEnergyBurned || 0), - activeCaloriesGoal: Math.round(activitySummary?.activeEnergyBurnedGoal || 350), - exerciseMinutes: Math.round(activitySummary?.appleExerciseTime || 0), - exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30), - standHours: Math.round(activitySummary?.appleStandHours || 0), - standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12), - // 血氧饱和度和心率数据 - oxygenSaturation, - heartRate + startDate: dayjs(date).startOf('day').toDate().toISOString(), + endDate: dayjs(date).endOf('day').toDate().toISOString() }; } -export async function fetchTodayHealthData(): Promise { - return fetchHealthDataForDate(new Date()); +// 睡眠时长计算 +function calculateSleepDuration(samples: any[]): number { + return samples.reduce((total, sample) => { + if (sample && sample.startDate && sample.endDate) { + const startTime = dayjs(sample.startDate).valueOf(); + const endTime = dayjs(sample.endDate).valueOf(); + const durationMinutes = (endTime - startTime) / (1000 * 60); + return total + durationMinutes; + } + return total; + }, 0); } -// 新增:专门获取HRV数据的函数 -export async function fetchHRVForDate(date: Date): Promise { - console.log('开始获取指定日期HRV数据...', date); +// 通用错误处理 +function logError(operation: string, error: any): void { + console.error(`获取${operation}失败:`, error); +} - const start = new Date(date); - start.setHours(0, 0, 0, 0); - const end = new Date(date); - end.setHours(23, 59, 59, 999); +function logWarning(operation: string, message: string): void { + console.warn(`${operation}数据${message}`); +} - const options = { - startDate: start.toISOString(), - endDate: end.toISOString() - } as any; +function logSuccess(operation: string, data: any): void { + console.log(`${operation}数据:`, data); +} - return new Promise((resolve) => { +// 数值验证和转换 +function validateOxygenSaturation(value: any): number | null { + if (value === undefined || value === null) return null; + + let numValue = Number(value); + + // 如果值小于1,可能是小数形式(0.0-1.0),需要转换为百分比 + if (numValue > 0 && numValue < 1) { + numValue = numValue * 100; + } + + // 血氧饱和度通常在0-100之间,验证数据有效性 + if (numValue >= 0 && numValue <= 100) { + return Number(numValue.toFixed(1)); + } + + console.warn('血氧饱和度数据异常:', numValue); + return null; +} + +function validateHeartRate(value: any): number | null { + if (value === undefined || value === null) return null; + + const numValue = Number(value); + + // 心率通常在30-200之间,验证数据有效性 + if (numValue >= 30 && numValue <= 200) { + return Math.round(numValue); + } + + console.warn('心率数据异常:', numValue); + return null; +} + +// 健康数据获取函数 +async function fetchStepCount(date: Date): Promise { + return new Promise((resolve) => { + AppleHealthKit.getStepCount({ + date: dayjs(date).toDate().toISOString() + }, (err, res) => { + if (err) { + logError('步数', err); + return resolve(0); + } + if (!res) { + logWarning('步数', '为空'); + return resolve(0); + } + logSuccess('步数', res); + resolve(res.value || 0); + }); + }); +} + +async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getActiveEnergyBurned(options, (err, res) => { + if (err) { + logError('消耗卡路里', err); + return resolve(0); + } + if (!res || !Array.isArray(res) || res.length === 0) { + logWarning('卡路里', '为空或格式错误'); + return resolve(0); + } + logSuccess('卡路里', res); + const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); + resolve(total); + }); + }); +} + +async function fetchBasalEnergyBurned(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getBasalEnergyBurned(options, (err, res) => { + if (err) { + logError('基础代谢', err); + return resolve(0); + } + if (!res || !Array.isArray(res) || res.length === 0) { + logWarning('基础代谢', '为空或格式错误'); + return resolve(0); + } + logSuccess('基础代谢', res); + const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); + resolve(total); + }); + }); +} + +async function fetchSleepDuration(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getSleepSamples(options, (err, res) => { + if (err) { + logError('睡眠数据', err); + return resolve(0); + } + if (!res || !Array.isArray(res) || res.length === 0) { + logWarning('睡眠', '为空或格式错误'); + return resolve(0); + } + logSuccess('睡眠', res); + resolve(calculateSleepDuration(res)); + }); + }); +} + +async function fetchHeartRateVariability(options: HealthDataOptions): Promise { + return new Promise((resolve) => { AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { if (err) { - console.error('获取HRV数据失败:', err); + logError('HRV数据', err); return resolve(null); } if (!res || !Array.isArray(res) || res.length === 0) { - console.warn('HRV数据为空或格式错误'); + logWarning('HRV', '为空或格式错误'); return resolve(null); } - console.log('HRV数据:', res); + logSuccess('HRV', res); - // 获取最新的HRV值 const latestHrv = res[res.length - 1]; if (latestHrv && latestHrv.value) { - resolve(latestHrv.value); + resolve(Math.round(latestHrv.value * 1000)); } else { resolve(null); } @@ -333,9 +231,155 @@ export async function fetchHRVForDate(date: Date): Promise { }); } -// 新增:获取今日HRV数据 +async function fetchActivitySummary(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getActivitySummary( + options, + (err: Object, results: HealthActivitySummary[]) => { + if (err) { + logError('ActivitySummary', err); + return resolve(null); + } + if (!results || results.length === 0) { + logWarning('ActivitySummary', '为空'); + return resolve(null); + } + logSuccess('ActivitySummary', results[0]); + resolve(results[0]); + }, + ); + }); +} + +async function fetchOxygenSaturation(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { + if (err) { + logError('血氧饱和度', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + logWarning('血氧饱和度', '为空或格式错误'); + return resolve(null); + } + logSuccess('血氧饱和度', res); + + const latestOxygen = res[res.length - 1]; + return resolve(validateOxygenSaturation(latestOxygen?.value)); + }); + }); +} + +async function fetchHeartRate(options: HealthDataOptions): Promise { + return new Promise((resolve) => { + AppleHealthKit.getHeartRateSamples(options, (err, res) => { + if (err) { + logError('心率', err); + return resolve(null); + } + if (!res || !Array.isArray(res) || res.length === 0) { + logWarning('心率', '为空或格式错误'); + return resolve(null); + } + logSuccess('心率', res); + + const latestHeartRate = res[res.length - 1]; + return resolve(validateHeartRate(latestHeartRate?.value)); + }); + }); +} + +// 默认健康数据 +function getDefaultHealthData(): TodayHealthData { + return { + steps: 0, + activeEnergyBurned: 0, + basalEnergyBurned: 0, + sleepDuration: 0, + hrv: null, + activeCalories: 0, + activeCaloriesGoal: 350, + exerciseMinutes: 0, + exerciseMinutesGoal: 30, + standHours: 0, + standHoursGoal: 12, + oxygenSaturation: null, + heartRate: null + }; +} + +export async function fetchHealthDataForDate(date: Date): Promise { + try { + console.log('开始获取指定日期健康数据...', date); + + const options = createDateRange(date); + console.log('查询选项:', options); + + // 并行获取所有健康数据 + const [ + steps, + activeEnergyBurned, + basalEnergyBurned, + sleepDuration, + hrv, + activitySummary, + oxygenSaturation, + heartRate + ] = await Promise.all([ + fetchStepCount(date), + fetchActiveEnergyBurned(options), + fetchBasalEnergyBurned(options), + fetchSleepDuration(options), + fetchHeartRateVariability(options), + fetchActivitySummary(options), + fetchOxygenSaturation(options), + fetchHeartRate(options) + ]); + + console.log('指定日期健康数据获取完成:', { + steps, + activeEnergyBurned, + basalEnergyBurned, + sleepDuration, + hrv, + activitySummary, + oxygenSaturation, + heartRate + }); + + return { + steps, + activeEnergyBurned, + basalEnergyBurned, + sleepDuration, + hrv, + activeCalories: Math.round(activitySummary?.activeEnergyBurned || 0), + activeCaloriesGoal: Math.round(activitySummary?.activeEnergyBurnedGoal || 350), + exerciseMinutes: Math.round(activitySummary?.appleExerciseTime || 0), + exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30), + standHours: Math.round(activitySummary?.appleStandHours || 0), + standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12), + oxygenSaturation, + heartRate + }; + } catch (error) { + console.error('获取指定日期健康数据失败:', error); + return getDefaultHealthData(); + } +} + +export async function fetchTodayHealthData(): Promise { + return fetchHealthDataForDate(dayjs().toDate()); +} + +export async function fetchHRVForDate(date: Date): Promise { + console.log('开始获取指定日期HRV数据...', date); + const options = createDateRange(date); + return fetchHeartRateVariability(options); +} + export async function fetchTodayHRV(): Promise { - return fetchHRVForDate(new Date()); + return fetchHRVForDate(dayjs().toDate()); } // 更新healthkit中的体重 @@ -354,17 +398,10 @@ export async function updateWeight(weight: number) { }); } -// 新增:测试血氧饱和度数据获取 -export async function testOxygenSaturationData(date: Date = new Date()): Promise { +export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise { console.log('=== 开始测试血氧饱和度数据获取 ==='); - const start = dayjs(date).startOf('day').toDate(); - const end = dayjs(date).endOf('day').toDate(); - - const options = { - startDate: start.toISOString(), - endDate: end.toISOString() - } as any; + const options = createDateRange(date); return new Promise((resolve) => { AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { @@ -392,23 +429,14 @@ export async function testOxygenSaturationData(date: Date = new Date()): Promise }); }); - // 获取最新的血氧饱和度值 + // 获取最新的血氧饱和度值并验证 const latestOxygen = res[res.length - 1]; - if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) { - let value = Number(latestOxygen.value); + if (latestOxygen?.value !== undefined && latestOxygen?.value !== null) { + const processedValue = validateOxygenSaturation(latestOxygen.value); console.log('处理前的值:', latestOxygen.value); - console.log('转换为数字后的值:', value); - - // 检查数据格式:如果值小于1,可能是小数形式(0.0-1.0),需要转换为百分比 - if (value > 0 && value < 1) { - const originalValue = value; - value = value * 100; - console.log('血氧饱和度数据从小数转换为百分比:', originalValue, '->', value); - } - - console.log('最终处理后的值:', value); - console.log('数据有效性检查:', value >= 0 && value <= 100 ? '有效' : '无效'); + console.log('最终处理后的值:', processedValue); + console.log('数据有效性检查:', processedValue !== null ? '有效' : '无效'); } console.log('=== 血氧饱和度数据测试完成 ==='); diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts index 7450644..481877c 100644 --- a/utils/notificationHelpers.ts +++ b/utils/notificationHelpers.ts @@ -1,6 +1,27 @@ import * as Notifications from 'expo-notifications'; import { NotificationData, notificationService } from '../services/notifications'; +/** + * 构建 coach 页面的深度链接 + */ +export function buildCoachDeepLink(params: { + action?: 'diet' | 'weight' | 'mood' | 'workout'; + subAction?: 'record' | 'photo' | 'text' | 'card'; + meal?: 'breakfast' | 'lunch' | 'dinner' | 'snack'; + message?: string; +}): string { + const baseUrl = '/coach'; + const searchParams = new URLSearchParams(); + + if (params.action) searchParams.set('action', params.action); + if (params.subAction) searchParams.set('subAction', params.subAction); + if (params.meal) searchParams.set('meal', params.meal); + if (params.message) searchParams.set('message', encodeURIComponent(params.message)); + + const queryString = searchParams.toString(); + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +} + /** * 运动相关的通知辅助函数 */ @@ -353,6 +374,13 @@ export class NutritionNotificationHelpers { return existingLunchReminder.identifier; } + // 构建跳转到 coach 页面的深度链接 + const coachUrl = buildCoachDeepLink({ + action: 'diet', + subAction: 'card', + meal: 'lunch' + }); + // 创建午餐提醒通知 const notificationId = await notificationService.scheduleCalendarRepeatingNotification( { @@ -361,7 +389,8 @@ export class NutritionNotificationHelpers { data: { type: 'lunch_reminder', isDailyReminder: true, - meal: '午餐' + meal: '午餐', + url: coachUrl // 添加深度链接 }, sound: true, priority: 'normal', @@ -385,10 +414,20 @@ export class NutritionNotificationHelpers { * 发送午餐记录提醒 */ static async sendLunchReminder(userName: string) { + const coachUrl = buildCoachDeepLink({ + action: 'diet', + subAction: 'card', + meal: 'lunch' + }); + return notificationService.sendImmediateNotification({ title: '午餐记录提醒', body: `${userName},记得记录今天的午餐情况哦!`, - data: { type: 'lunch_reminder', meal: '午餐' }, + data: { + type: 'lunch_reminder', + meal: '午餐', + url: coachUrl + }, sound: true, priority: 'normal', }); @@ -436,11 +475,28 @@ export class NutritionNotificationHelpers { reminderTime.setDate(reminderTime.getDate() + 1); } + // 构建深度链接 + const mealTypeMap: Record = { + '早餐': 'breakfast', + '午餐': 'lunch', + '晚餐': 'dinner' + }; + + const coachUrl = buildCoachDeepLink({ + action: 'diet', + subAction: 'card', + meal: mealTypeMap[mealTime.meal] as 'breakfast' | 'lunch' | 'dinner' + }); + const notificationId = await notificationService.scheduleRepeatingNotification( { title: `${mealTime.meal}提醒`, body: `${userName},记得记录您的${mealTime.meal}情况`, - data: { type: 'meal_reminder', meal: mealTime.meal }, + data: { + type: 'meal_reminder', + meal: mealTime.meal, + url: coachUrl + }, sound: true, priority: 'normal', }, @@ -575,19 +631,50 @@ export const NotificationTemplates = { }), }, nutrition: { - reminder: (userName: string, meal: string) => ({ - title: `${meal}提醒`, - body: `${userName},记得记录您的${meal}情况`, - data: { type: 'meal_reminder', meal }, - sound: true, - priority: 'normal' as const, - }), - lunch: (userName: string) => ({ - title: '午餐记录提醒', - body: `${userName},记得记录今天的午餐情况哦!`, - data: { type: 'lunch_reminder', meal: '午餐' }, - sound: true, - priority: 'normal' as const, - }), + reminder: (userName: string, meal: string) => { + const mealTypeMap: Record = { + '早餐': 'breakfast', + '午餐': 'lunch', + '晚餐': 'dinner', + '加餐': 'snack' + }; + + const coachUrl = buildCoachDeepLink({ + action: 'diet', + subAction: 'card', + meal: mealTypeMap[meal] as 'breakfast' | 'lunch' | 'dinner' | 'snack' + }); + + return { + title: `${meal}提醒`, + body: `${userName},记得记录您的${meal}情况`, + data: { + type: 'meal_reminder', + meal, + url: coachUrl + }, + sound: true, + priority: 'normal' as const, + }; + }, + lunch: (userName: string) => { + const coachUrl = buildCoachDeepLink({ + action: 'diet', + subAction: 'card', + meal: 'lunch' + }); + + return { + title: '午餐记录提醒', + body: `${userName},记得记录今天的午餐情况哦!`, + data: { + type: 'lunch_reminder', + meal: '午餐', + url: coachUrl + }, + sound: true, + priority: 'normal' as const, + }; + }, }, };