feat: 更新 CLAUDE.md 文件及多个组件以优化用户体验和功能

- 更新 CLAUDE.md 文件,重构架构部分,增加认证和数据层的描述
- 在 GoalsScreen 中新增目标模板选择功能,支持用户选择和创建目标
- 在 CreateGoalModal 中添加初始数据支持,优化目标创建体验
- 新增 GoalTemplateModal 组件,提供目标模板选择界面
- 更新 NotificationHelpers,支持构建深度链接以便于导航
- 在 CoachScreen 中处理路由参数,增强用户交互体验
- 更新多个组件的样式和逻辑,提升整体用户体验
- 删除不再使用的中文回复规则文档
This commit is contained in:
richarjiang
2025-08-26 15:04:04 +08:00
parent 7f2afdf671
commit 3f89023447
13 changed files with 1113 additions and 359 deletions

View File

@@ -1,15 +0,0 @@
---
inclusion: always
---
# 中文回复规则
请始终使用中文进行回复和编写文档。包括:
- 所有对话回复都使用中文
- 代码注释使用中文
- 文档和说明使用中文
- 错误信息和提示使用中文
- 变量名和函数名可以使用英文,但注释和文档说明必须是中文
这个规则适用于所有交互,除非用户明确要求使用其他语言。

View File

@@ -11,35 +11,38 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Reset project**: `npm run reset-project` - **Reset project**: `npm run reset-project`
## Architecture ## Architecture
- **Framework**: React Native (Expo) with TypeScript. - **Framework**: React Native (Expo) with TypeScript using Expo Router for file-based navigation
- **Navigation**: Expo Router for file-based routing (`app/` directory). - **State Management**: Redux Toolkit with domain-specific slices (`store/`) and typed hooks (`hooks/redux.ts`)
- **State Management**: Redux Toolkit with slices for different domains (user, training plans, workouts, challenges, etc.). - **Authentication**: Custom auth guard system with `useAuthGuard` hook for protected navigation
- **UI**: Themed components (`ThemedText`, `ThemedView`) and reusable UI elements (`Collapsible`, `ParallaxScrollView`). - **Navigation**:
- **API Layer**: Service files for communicating with backend APIs (`services/` directory). - File-based routing in `app/` directory with nested layouts
- **Data Persistence**: AsyncStorage for local data storage. - Tab-based navigation with custom styling and haptic feedback
- **Platform-Specific**: Android (`android/`) and iOS (`ios/`) configurations with native modules. - Route constants defined in `constants/Routes.ts`
- **Hooks**: Custom hooks for color scheme (`useColorScheme`), theme management (`useThemeColor`), and Redux integration (`useRedux`). - **UI System**:
- **Dependencies**: React Navigation for tab-based navigation, Expo modules for native features (haptics, blur, image picking, etc.), and third-party libraries for specific functionality. - 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 ## Key Architecture Patterns
- **Authentication**: Login flow with Apple authentication support. - **Redux Auto-sync**: Listener middleware automatically syncs checkin data changes to backend
- **Training Plans**: Creation and management of personalized pilates training plans. - **Type-safe Navigation**: Uses Expo Router with TypeScript for route type safety
- **Workouts**: Daily workout tracking and session management. - **Authentication Flow**: `pushIfAuthedElseLogin` function handles auth-protected navigation
- **AI Features**: AI coach chat and posture assessment capabilities. - **Theme System**: Dynamic theming with light/dark mode support and color tokens
- **Health Integration**: Integration with health data tracking. - **Service Layer**: Centralized API client with interceptors and error handling
- **Content Management**: Article reading and educational content.
- **Challenge System**: Challenge participation and progress tracking.
- **User Profiles**: Personal information management and goal setting.
## Directory Structure ## Development Conventions
- `app/`: Main application screens and routing - Use absolute imports with `@/` prefix for all internal imports
- `components/`: Reusable UI components - Follow existing Redux slice patterns for state management
- `constants/`: Application constants and configuration - Implement auth guards using `useAuthGuard` hook for protected features
- `hooks/`: Custom React hooks - Use themed components for consistent styling
- `services/`: API service layer - Follow established navigation patterns with typed routes
- `store/`: Redux store and slices
- `types/`: TypeScript type definitions
## rules
- 路由跳转使用 pushIfAuthedElseLogin

View File

@@ -44,9 +44,9 @@ export default function TabLayout() {
case 'explore': case 'explore':
return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const; return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const;
case 'coach': case 'coach':
return { icon: 'person.3.fill', title: 'Seal' } as const; return { icon: 'message.fill', title: 'AI' } as const;
case 'goals': case 'goals':
return { icon: 'flag.fill', title: '目标' } as const; return { icon: 'flag.fill', title: '习惯' } as const;
case 'statistics': case 'statistics':
return { icon: 'chart.pie.fill', title: '统计' } as const; return { icon: 'chart.pie.fill', title: '统计' } as const;
case 'personal': case 'personal':

View File

@@ -118,8 +118,17 @@ const CardType = {
type CardType = typeof CardType[keyof typeof 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() { export default function CoachScreen() {
const params = useLocalSearchParams<{ name?: string }>(); const params = useLocalSearchParams<CoachScreenParams>();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard();
@@ -389,6 +398,69 @@ export default function CoachScreen() {
}; };
}, [insets.bottom]); }, [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<string, string> = {
breakfast: '早餐',
lunch: '午餐',
dinner: '晚餐',
snack: '加餐'
};
return mealNames[meal] || '';
};
const streamAbortRef = useRef<{ abort: () => void } | null>(null); const streamAbortRef = useRef<{ abort: () => void } | null>(null);
// 组件卸载时清理流式请求和定时器 // 组件卸载时清理流式请求和定时器

View File

@@ -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 { GoalsPageGuide } from '@/components/GoalsPageGuide';
import { GuideTestButton } from '@/components/GuideTestButton'; import { GuideTestButton } from '@/components/GuideTestButton';
import { TaskCard } from '@/components/TaskCard'; import { TaskCard } from '@/components/TaskCard';
@@ -7,6 +8,7 @@ import { TaskProgressCard } from '@/components/TaskProgressCard';
import { useGlobalDialog } from '@/components/ui/DialogProvider'; import { useGlobalDialog } from '@/components/ui/DialogProvider';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar'; import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
import { GoalTemplate } from '@/constants/goalTemplates';
import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme'; import { useColorScheme } from '@/hooks/useColorScheme';
@@ -37,9 +39,7 @@ export default function GoalsScreen() {
tasksLoading, tasksLoading,
tasksError, tasksError,
tasksPagination, tasksPagination,
completeLoading,
completeError, completeError,
skipLoading,
skipError, skipError,
} = useAppSelector((state) => state.tasks); } = useAppSelector((state) => state.tasks);
@@ -50,11 +50,13 @@ export default function GoalsScreen() {
const userProfile = useAppSelector((state) => state.user.profile); const userProfile = useAppSelector((state) => state.user.profile);
const [showTemplateModal, setShowTemplateModal] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all'); const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗 const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗
const [showGuide, setShowGuide] = useState(false); // 控制引导显示 const [showGuide, setShowGuide] = useState(false); // 控制引导显示
const [selectedTemplateData, setSelectedTemplateData] = useState<Partial<CreateGoalRequest> | undefined>();
// 页面聚焦时重新加载数据 // 页面聚焦时重新加载数据
useFocusEffect( useFocusEffect(
@@ -141,6 +143,29 @@ export default function GoalsScreen() {
const handleModalSuccess = () => { const handleModalSuccess = () => {
// 不需要在这里改变 modalKey因为弹窗已经关闭了 // 不需要在这里改变 modalKey因为弹窗已经关闭了
// 下次打开时会自动使用新的 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() {
<View style={styles.header}> <View style={styles.header}>
<View> <View>
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}> <Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
</Text> </Text>
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}> <Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
</Text> </Text>
</View> </View>
</View> </View>
@@ -378,10 +403,7 @@ export default function GoalsScreen() {
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.cardAddButton, { backgroundColor: colorTokens.primary }]} style={[styles.cardAddButton, { backgroundColor: colorTokens.primary }]}
onPress={() => { onPress={handleOpenTemplateModal}
setModalKey(prev => prev + 1); // 每次打开弹窗时使用新的 key
setShowCreateModal(true);
}}
> >
<Text style={styles.cardAddButtonText}>+</Text> <Text style={styles.cardAddButtonText}>+</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -420,14 +442,26 @@ export default function GoalsScreen() {
/> />
</View> </View>
{/* 目标模板选择弹窗 */}
<GoalTemplateModal
visible={showTemplateModal}
onClose={() => setShowTemplateModal(false)}
onSelectTemplate={handleSelectTemplate}
onCreateCustom={handleCreateCustomGoal}
/>
{/* 创建目标弹窗 */} {/* 创建目标弹窗 */}
<CreateGoalModal <CreateGoalModal
key={modalKey} key={modalKey}
visible={showCreateModal} visible={showCreateModal}
onClose={() => setShowCreateModal(false)} onClose={() => {
setShowCreateModal(false);
setSelectedTemplateData(undefined);
}}
onSubmit={handleCreateGoal} onSubmit={handleCreateGoal}
onSuccess={handleModalSuccess} onSuccess={handleModalSuccess}
loading={createLoading} loading={createLoading}
initialData={selectedTemplateData}
/> />
{/* 目标页面引导 */} {/* 目标页面引导 */}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -4,7 +4,7 @@ import { CreateGoalRequest, GoalPriority, RepeatType } from '@/types/goals';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker'; import DateTimePicker from '@react-native-community/datetimepicker';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
Alert, Alert,
Image, Image,
@@ -26,6 +26,7 @@ interface CreateGoalModalProps {
onSubmit: (goalData: CreateGoalRequest) => void; onSubmit: (goalData: CreateGoalRequest) => void;
onSuccess?: () => void; onSuccess?: () => void;
loading?: boolean; loading?: boolean;
initialData?: Partial<CreateGoalRequest>;
} }
const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [ const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [
@@ -42,28 +43,49 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
onSubmit, onSubmit,
onSuccess, onSuccess,
loading = false, loading = false,
initialData,
}) => { }) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme]; const colorTokens = Colors[theme];
// 表单状态 // 表单状态
const [title, setTitle] = useState(''); const [title, setTitle] = useState(initialData?.title || '');
const [description, setDescription] = useState(''); const [description, setDescription] = useState(initialData?.description || '');
const [repeatType, setRepeatType] = useState<RepeatType>('daily'); const [repeatType, setRepeatType] = useState<RepeatType>(initialData?.repeatType || 'daily');
const [frequency, setFrequency] = useState(1); const [frequency, setFrequency] = useState(initialData?.frequency || 1);
const [hasReminder, setHasReminder] = useState(false); const [hasReminder, setHasReminder] = useState(initialData?.hasReminder || false);
const [showFrequencyPicker, setShowFrequencyPicker] = useState(false); const [showFrequencyPicker, setShowFrequencyPicker] = useState(false);
const [showRepeatTypePicker, setShowRepeatTypePicker] = useState(false); const [showRepeatTypePicker, setShowRepeatTypePicker] = useState(false);
const [reminderTime, setReminderTime] = useState('20:00'); const [reminderTime, setReminderTime] = useState(initialData?.reminderTime || '20:00');
const [category, setCategory] = useState(''); const [category, setCategory] = useState(initialData?.category || '');
const [priority, setPriority] = useState<GoalPriority>(5); const [priority, setPriority] = useState<GoalPriority>(initialData?.priority || 5);
const [showTimePicker, setShowTimePicker] = useState(false); const [showTimePicker, setShowTimePicker] = useState(false);
const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null); const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null);
// 周几选择状态 // 周几选择状态
const [selectedWeekdays, setSelectedWeekdays] = useState<number[]>([1, 2, 3, 4, 5]); // 默认周一到周五 const [selectedWeekdays, setSelectedWeekdays] = useState<number[]>(
initialData?.customRepeatRule?.weekdays || [1, 2, 3, 4, 5]
); // 默认周一到周五
// 每月日期选择状态 // 每月日期选择状态
const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>([1, 15]); // 默认1号和15号 const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>(
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 = () => { const resetForm = () => {

View File

@@ -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 (
<TouchableOpacity
key={category.id}
style={[
styles.categoryTab,
isSelected && styles.categoryTabSelected
]}
onPress={() => setSelectedCategory(category.id)}
activeOpacity={0.8}
>
{category.isRecommended && (
<Image
source={require('@/assets/images/icons/icon-recommend.png')}
style={{ width: 24, height: 24 }}
/>
)}
<Text style={[
styles.categoryTabText,
isSelected && styles.categoryTabTextSelected
]}>
{category.title}
</Text>
</TouchableOpacity>
);
};
// 渲染模板项
const renderTemplateItem = (template: GoalTemplate) => (
<TouchableOpacity
key={template.id}
style={[styles.templateItem]}
onPress={() => onSelectTemplate(template)}
activeOpacity={0.8}
>
<View style={[styles.templateIcon]}>
<MaterialIcons
name={template.icon as any}
size={24}
color={template.iconColor}
/>
</View>
<Text style={[styles.templateTitle]}>
{template.title}
</Text>
</TouchableOpacity>
);
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
{/* 头部 */}
<View style={styles.header}>
<TouchableOpacity
onPress={onClose}
style={styles.closeButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="keyboard-arrow-down" size={24} color="#374151" />
</TouchableOpacity>
<Text style={styles.title}></Text>
<View style={{ width: 24 }} />
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 创建自定义目标 */}
<TouchableOpacity
style={styles.customGoalButton}
onPress={onCreateCustom}
activeOpacity={0.8}
>
<View style={styles.customGoalIcon}>
<MaterialIcons name="add" size={24} color="#7C3AED" />
</View>
<Text style={styles.customGoalText}></Text>
</TouchableOpacity>
{/* 分类选择器 */}
<View style={styles.categorySection}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.categoryScrollContent}
style={styles.categoryScrollView}
>
{goalCategories.map(renderCategoryTab)}
</ScrollView>
</View>
{/* 当前分类的模板 */}
<View style={styles.templatesSection}>
<View style={styles.templatesGrid}>
{currentTemplates.map(renderTemplateItem)}
</View>
</View>
</ScrollView>
</SafeAreaView>
</Modal>
);
}
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,
},
});

View File

@@ -30,7 +30,7 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
{/* 标题区域 */} {/* 标题区域 */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={styles.title}></Text> <Text style={styles.title}></Text>
<TouchableOpacity <TouchableOpacity
style={styles.goalsIconButton} style={styles.goalsIconButton}
onPress={handleNavigateToGoals} onPress={handleNavigateToGoals}

View File

@@ -20,6 +20,7 @@ const MAPPING = {
'chevron.right': 'chevron-right', 'chevron.right': 'chevron-right',
'person.fill': 'person', 'person.fill': 'person',
'person.3.fill': 'people', 'person.3.fill': 'people',
'message.fill': 'message',
} as IconMapping; } as IconMapping;
/** /**

227
constants/goalTemplates.ts Normal file
View File

@@ -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<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;
};

View File

@@ -2,6 +2,12 @@ import dayjs from 'dayjs';
import type { HealthActivitySummary, HealthKitPermissions } from 'react-native-health'; import type { HealthActivitySummary, HealthKitPermissions } from 'react-native-health';
import AppleHealthKit from 'react-native-health'; import AppleHealthKit from 'react-native-health';
type HealthDataOptions = {
startDate: string;
endDate: string;
};
const PERMISSIONS: HealthKitPermissions = { const PERMISSIONS: HealthKitPermissions = {
permissions: { permissions: {
read: [ read: [
@@ -61,122 +67,160 @@ export async function ensureHealthPermissions(): Promise<boolean> {
}); });
} }
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> { // 日期工具函数
console.log('开始获取指定日期健康数据...', date); function createDateRange(date: Date): HealthDataOptions {
return {
const start = dayjs(date).startOf('day').toDate(); startDate: dayjs(date).startOf('day').toDate().toISOString(),
const end = dayjs(date).endOf('day').toDate(); endDate: dayjs(date).endOf('day').toDate().toISOString()
const options = {
startDate: start.toISOString(),
endDate: end.toISOString()
} as any;
const activitySummaryOptions = {
startDate: start.toISOString(),
endDate: end.toISOString()
}; };
}
console.log('查询选项:', options); // 睡眠时长计算
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);
}
// 并行获取所有健康数据包括ActivitySummary、血氧饱和度和心率 // 通用错误处理
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate] = await Promise.all([ function logError(operation: string, error: any): void {
// 获取步数 console.error(`获取${operation}失败:`, error);
new Promise<number>((resolve) => { }
function logWarning(operation: string, message: string): void {
console.warn(`${operation}数据${message}`);
}
function logSuccess(operation: string, data: any): void {
console.log(`${operation}数据:`, data);
}
// 数值验证和转换
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<number> {
return new Promise((resolve) => {
AppleHealthKit.getStepCount({ AppleHealthKit.getStepCount({
date: dayjs(date).toISOString() date: dayjs(date).toDate().toISOString()
}, (err, res) => { }, (err, res) => {
if (err) { if (err) {
console.error('获取步数失败:', err); logError('步数', err);
return resolve(0); return resolve(0);
} }
if (!res) { if (!res) {
console.warn('步数数据为空'); logWarning('步数', '为空');
return resolve(0); return resolve(0);
} }
console.log('步数数据:', res); logSuccess('步数', res);
resolve(res.value || 0); resolve(res.value || 0);
}); });
}), });
}
// 获取消耗卡路里 async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
new Promise<number>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => { AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
if (err) { if (err) {
console.error('获取消耗卡路里失败:', err); logError('消耗卡路里', err);
return resolve(0); return resolve(0);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('卡路里数据为空或格式错误'); logWarning('卡路里', '为空或格式错误');
return resolve(0); return resolve(0);
} }
console.log('卡路里数据:', res); logSuccess('卡路里', res);
// 求和该日内的所有记录(单位:千卡)
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total); resolve(total);
}); });
}), });
}
// 获取基础代谢率 async function fetchBasalEnergyBurned(options: HealthDataOptions): Promise<number> {
new Promise<number>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getBasalEnergyBurned(options, (err, res) => { AppleHealthKit.getBasalEnergyBurned(options, (err, res) => {
if (err) { if (err) {
console.error('获取基础代谢失败:', err); logError('基础代谢', err);
return resolve(0); return resolve(0);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('基础代谢数据为空或格式错误'); logWarning('基础代谢', '为空或格式错误');
return resolve(0); return resolve(0);
} }
console.log('基础代谢数据:', res); logSuccess('基础代谢', res);
// 求和该日内的所有记录(单位:千卡)
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0); const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total); resolve(total);
}); });
}), });
}
// 获取睡眠时长 async function fetchSleepDuration(options: HealthDataOptions): Promise<number> {
new Promise<number>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getSleepSamples(options, (err, res) => { AppleHealthKit.getSleepSamples(options, (err, res) => {
if (err) { if (err) {
console.error('获取睡眠数据失败:', err); logError('睡眠数据', err);
return resolve(0); return resolve(0);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('睡眠数据为空或格式错误'); logWarning('睡眠', '为空或格式错误');
return resolve(0); return resolve(0);
} }
console.log('睡眠数据:', res); logSuccess('睡眠', res);
resolve(calculateSleepDuration(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数据 async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
new Promise<number | null>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
if (err) { if (err) {
console.error('获取HRV数据失败:', err); logError('HRV数据', err);
return resolve(null); return resolve(null);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('HRV数据为空或格式错误'); logWarning('HRV', '为空或格式错误');
return resolve(null); return resolve(null);
} }
console.log('HRV数据:', res); logSuccess('HRV', res);
// 获取最新的HRV值
const latestHrv = res[res.length - 1]; const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) { if (latestHrv && latestHrv.value) {
resolve(Math.round(latestHrv.value * 1000)); resolve(Math.round(latestHrv.value * 1000));
@@ -184,158 +228,158 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
resolve(null); resolve(null);
} }
}); });
}), });
}
// 获取ActivitySummary数据健身圆环数据 async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthActivitySummary | null> {
new Promise<HealthActivitySummary | null>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getActivitySummary( AppleHealthKit.getActivitySummary(
activitySummaryOptions, options,
(err: Object, results: HealthActivitySummary[]) => { (err: Object, results: HealthActivitySummary[]) => {
if (err) { if (err) {
console.error('获取ActivitySummary失败:', err); logError('ActivitySummary', err);
return resolve(null); return resolve(null);
} }
if (!results || results.length === 0) { if (!results || results.length === 0) {
console.warn('ActivitySummary数据为空'); logWarning('ActivitySummary', '为空');
return resolve(null); return resolve(null);
} }
console.log('ActivitySummary数据:', results[0]); logSuccess('ActivitySummary', results[0]);
resolve(results[0]); resolve(results[0]);
}, },
); );
}), });
}
// 获取血氧饱和度数据 async function fetchOxygenSaturation(options: HealthDataOptions): Promise<number | null> {
new Promise<number | null>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
if (err) { if (err) {
console.error('获取血氧饱和度失败:', err); logError('血氧饱和度', err);
return resolve(null); return resolve(null);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('血氧饱和度数据为空或格式错误'); logWarning('血氧饱和度', '为空或格式错误');
return resolve(null); return resolve(null);
} }
console.log('血氧饱和度数据:', res); logSuccess('血氧饱和度', res);
// 获取最新的血氧饱和度值
const latestOxygen = res[res.length - 1]; const latestOxygen = res[res.length - 1];
if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) { return resolve(validateOxygenSaturation(latestOxygen?.value));
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);
}
}); });
}), });
}
// 获取心率数据 async function fetchHeartRate(options: HealthDataOptions): Promise<number | null> {
new Promise<number | null>((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getHeartRateSamples(options, (err, res) => { AppleHealthKit.getHeartRateSamples(options, (err, res) => {
if (err) { if (err) {
console.error('获取心率失败:', err); logError('心率', err);
return resolve(null); return resolve(null);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('心率数据为空或格式错误'); logWarning('心率', '为空或格式错误');
return resolve(null); return resolve(null);
} }
console.log('心率数据:', res); logSuccess('心率', res);
// 获取最新的心率值
const latestHeartRate = res[res.length - 1]; const latestHeartRate = res[res.length - 1];
if (latestHeartRate && latestHeartRate.value !== undefined && latestHeartRate.value !== null) { return resolve(validateHeartRate(latestHeartRate?.value));
// 心率通常在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);
}
}); });
}) });
}
// 默认健康数据
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<TodayHealthData> {
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, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate }); console.log('指定日期健康数据获取完成:', {
steps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
hrv,
activitySummary,
oxygenSaturation,
heartRate
});
return { return {
steps, steps,
activeEnergyBurned: calories, activeEnergyBurned,
basalEnergyBurned: basalMetabolism, basalEnergyBurned,
sleepDuration, sleepDuration,
hrv, hrv,
// 健身圆环数据
activeCalories: Math.round(activitySummary?.activeEnergyBurned || 0), activeCalories: Math.round(activitySummary?.activeEnergyBurned || 0),
activeCaloriesGoal: Math.round(activitySummary?.activeEnergyBurnedGoal || 350), activeCaloriesGoal: Math.round(activitySummary?.activeEnergyBurnedGoal || 350),
exerciseMinutes: Math.round(activitySummary?.appleExerciseTime || 0), exerciseMinutes: Math.round(activitySummary?.appleExerciseTime || 0),
exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30), exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30),
standHours: Math.round(activitySummary?.appleStandHours || 0), standHours: Math.round(activitySummary?.appleStandHours || 0),
standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12), standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12),
// 血氧饱和度和心率数据
oxygenSaturation, oxygenSaturation,
heartRate heartRate
}; };
} catch (error) {
console.error('获取指定日期健康数据失败:', error);
return getDefaultHealthData();
}
} }
export async function fetchTodayHealthData(): Promise<TodayHealthData> { export async function fetchTodayHealthData(): Promise<TodayHealthData> {
return fetchHealthDataForDate(new Date()); return fetchHealthDataForDate(dayjs().toDate());
} }
// 新增专门获取HRV数据的函数
export async function fetchHRVForDate(date: Date): Promise<number | null> { export async function fetchHRVForDate(date: Date): Promise<number | null> {
console.log('开始获取指定日期HRV数据...', date); console.log('开始获取指定日期HRV数据...', date);
const options = createDateRange(date);
const start = new Date(date); return fetchHeartRateVariability(options);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
const options = {
startDate: start.toISOString(),
endDate: end.toISOString()
} as any;
return new Promise<number | null>((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(latestHrv.value);
} else {
resolve(null);
}
});
});
} }
// 新增获取今日HRV数据
export async function fetchTodayHRV(): Promise<number | null> { export async function fetchTodayHRV(): Promise<number | null> {
return fetchHRVForDate(new Date()); return fetchHRVForDate(dayjs().toDate());
} }
// 更新healthkit中的体重 // 更新healthkit中的体重
@@ -354,17 +398,10 @@ export async function updateWeight(weight: number) {
}); });
} }
// 新增:测试血氧饱和度数据获取 export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise<void> {
export async function testOxygenSaturationData(date: Date = new Date()): Promise<void> {
console.log('=== 开始测试血氧饱和度数据获取 ==='); console.log('=== 开始测试血氧饱和度数据获取 ===');
const start = dayjs(date).startOf('day').toDate(); const options = createDateRange(date);
const end = dayjs(date).endOf('day').toDate();
const options = {
startDate: start.toISOString(),
endDate: end.toISOString()
} as any;
return new Promise((resolve) => { return new Promise((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => { AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
@@ -392,23 +429,14 @@ export async function testOxygenSaturationData(date: Date = new Date()): Promise
}); });
}); });
// 获取最新的血氧饱和度值 // 获取最新的血氧饱和度值并验证
const latestOxygen = res[res.length - 1]; const latestOxygen = res[res.length - 1];
if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) { if (latestOxygen?.value !== undefined && latestOxygen?.value !== null) {
let value = Number(latestOxygen.value); const processedValue = validateOxygenSaturation(latestOxygen.value);
console.log('处理前的值:', latestOxygen.value); console.log('处理前的值:', latestOxygen.value);
console.log('转换为数字后的值:', value); console.log('最终处理后的值:', processedValue);
console.log('数据有效性检查:', processedValue !== null ? '有效' : '无效');
// 检查数据格式如果值小于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('=== 血氧饱和度数据测试完成 ==='); console.log('=== 血氧饱和度数据测试完成 ===');

View File

@@ -1,6 +1,27 @@
import * as Notifications from 'expo-notifications'; import * as Notifications from 'expo-notifications';
import { NotificationData, notificationService } from '../services/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; return existingLunchReminder.identifier;
} }
// 构建跳转到 coach 页面的深度链接
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: 'lunch'
});
// 创建午餐提醒通知 // 创建午餐提醒通知
const notificationId = await notificationService.scheduleCalendarRepeatingNotification( const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{ {
@@ -361,7 +389,8 @@ export class NutritionNotificationHelpers {
data: { data: {
type: 'lunch_reminder', type: 'lunch_reminder',
isDailyReminder: true, isDailyReminder: true,
meal: '午餐' meal: '午餐',
url: coachUrl // 添加深度链接
}, },
sound: true, sound: true,
priority: 'normal', priority: 'normal',
@@ -385,10 +414,20 @@ export class NutritionNotificationHelpers {
* 发送午餐记录提醒 * 发送午餐记录提醒
*/ */
static async sendLunchReminder(userName: string) { static async sendLunchReminder(userName: string) {
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: 'lunch'
});
return notificationService.sendImmediateNotification({ return notificationService.sendImmediateNotification({
title: '午餐记录提醒', title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`, body: `${userName},记得记录今天的午餐情况哦!`,
data: { type: 'lunch_reminder', meal: '午餐' }, data: {
type: 'lunch_reminder',
meal: '午餐',
url: coachUrl
},
sound: true, sound: true,
priority: 'normal', priority: 'normal',
}); });
@@ -436,11 +475,28 @@ export class NutritionNotificationHelpers {
reminderTime.setDate(reminderTime.getDate() + 1); reminderTime.setDate(reminderTime.getDate() + 1);
} }
// 构建深度链接
const mealTypeMap: Record<string, string> = {
'早餐': 'breakfast',
'午餐': 'lunch',
'晚餐': 'dinner'
};
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: mealTypeMap[mealTime.meal] as 'breakfast' | 'lunch' | 'dinner'
});
const notificationId = await notificationService.scheduleRepeatingNotification( const notificationId = await notificationService.scheduleRepeatingNotification(
{ {
title: `${mealTime.meal}提醒`, title: `${mealTime.meal}提醒`,
body: `${userName},记得记录您的${mealTime.meal}情况`, body: `${userName},记得记录您的${mealTime.meal}情况`,
data: { type: 'meal_reminder', meal: mealTime.meal }, data: {
type: 'meal_reminder',
meal: mealTime.meal,
url: coachUrl
},
sound: true, sound: true,
priority: 'normal', priority: 'normal',
}, },
@@ -575,19 +631,50 @@ export const NotificationTemplates = {
}), }),
}, },
nutrition: { nutrition: {
reminder: (userName: string, meal: string) => ({ reminder: (userName: string, meal: string) => {
const mealTypeMap: Record<string, string> = {
'早餐': 'breakfast',
'午餐': 'lunch',
'晚餐': 'dinner',
'加餐': 'snack'
};
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: mealTypeMap[meal] as 'breakfast' | 'lunch' | 'dinner' | 'snack'
});
return {
title: `${meal}提醒`, title: `${meal}提醒`,
body: `${userName},记得记录您的${meal}情况`, body: `${userName},记得记录您的${meal}情况`,
data: { type: 'meal_reminder', meal }, data: {
type: 'meal_reminder',
meal,
url: coachUrl
},
sound: true, sound: true,
priority: 'normal' as const, priority: 'normal' as const,
}), };
lunch: (userName: string) => ({ },
lunch: (userName: string) => {
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: 'lunch'
});
return {
title: '午餐记录提醒', title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`, body: `${userName},记得记录今天的午餐情况哦!`,
data: { type: 'lunch_reminder', meal: '午餐' }, data: {
type: 'lunch_reminder',
meal: '午餐',
url: coachUrl
},
sound: true, sound: true,
priority: 'normal' as const, priority: 'normal' as const,
}), };
},
}, },
}; };