feat: 更新应用名称为“Out Live”,删除推送通知使用指南和喝水记录API修复测试文档,优化饮水设置页面,添加登录状态检查
This commit is contained in:
@@ -1,375 +0,0 @@
|
||||
# 推送通知功能使用指南
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
推送通知功能已经集成到项目中,无需额外安装依赖。
|
||||
|
||||
### 2. 基本使用
|
||||
|
||||
在组件中使用推送通知功能:
|
||||
|
||||
```typescript
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
|
||||
function MyComponent() {
|
||||
const { sendNotification, sendWorkoutReminder } = useNotifications();
|
||||
|
||||
const handleSendNotification = async () => {
|
||||
await sendNotification({
|
||||
title: '测试通知',
|
||||
body: '这是一个测试通知',
|
||||
sound: true,
|
||||
priority: 'high'
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendWorkoutReminder = async () => {
|
||||
await sendWorkoutReminder('运动提醒', '该开始今天的普拉提训练了!');
|
||||
};
|
||||
|
||||
return (
|
||||
// 你的组件内容
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用辅助函数
|
||||
|
||||
```typescript
|
||||
import { WorkoutNotificationHelpers, GoalNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
|
||||
// 发送运动开始提醒
|
||||
await WorkoutNotificationHelpers.sendWorkoutStartReminder('张三');
|
||||
|
||||
// 发送目标达成通知
|
||||
await GoalNotificationHelpers.sendGoalAchievementNotification('张三', '每周运动3次');
|
||||
|
||||
// 安排每日运动提醒
|
||||
await WorkoutNotificationHelpers.scheduleDailyWorkoutReminder('张三', 9, 0); // 每天上午9点
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### ✅ 已实现功能
|
||||
|
||||
- [x] 立即发送通知
|
||||
- [x] 定时发送通知
|
||||
- [x] 重复发送通知
|
||||
- [x] 通知权限管理
|
||||
- [x] 通知点击处理
|
||||
- [x] 通知取消功能
|
||||
- [x] 多种通知类型支持
|
||||
- [x] 通知模板系统
|
||||
- [x] 批量通知发送
|
||||
- [x] 通知状态查询
|
||||
|
||||
### 📱 支持的通知类型
|
||||
|
||||
1. **运动相关**
|
||||
- 运动开始提醒
|
||||
- 运动完成通知
|
||||
- 每日运动提醒
|
||||
|
||||
2. **目标相关**
|
||||
- 目标达成通知
|
||||
- 目标进度更新
|
||||
- 目标截止提醒
|
||||
|
||||
3. **心情相关**
|
||||
- 心情打卡提醒
|
||||
- 每日心情提醒
|
||||
|
||||
4. **营养相关**
|
||||
- 营养记录提醒
|
||||
- 三餐提醒
|
||||
|
||||
5. **通用通知**
|
||||
- 欢迎通知
|
||||
- 成就通知
|
||||
- 系统维护通知
|
||||
|
||||
## 详细使用示例
|
||||
|
||||
### 1. 在运动页面中使用
|
||||
|
||||
```typescript
|
||||
import { WorkoutNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
|
||||
// 运动开始时
|
||||
const handleWorkoutStart = async () => {
|
||||
await WorkoutNotificationHelpers.sendWorkoutStartReminder(userName);
|
||||
// 开始运动逻辑
|
||||
};
|
||||
|
||||
// 运动完成时
|
||||
const handleWorkoutComplete = async (duration: number) => {
|
||||
await WorkoutNotificationHelpers.sendWorkoutCompleteNotification(userName, duration);
|
||||
// 完成运动逻辑
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 在目标页面中使用
|
||||
|
||||
```typescript
|
||||
import { GoalNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
|
||||
// 目标达成时
|
||||
const handleGoalAchieved = async (goalName: string) => {
|
||||
await GoalNotificationHelpers.sendGoalAchievementNotification(userName, goalName);
|
||||
// 目标达成逻辑
|
||||
};
|
||||
|
||||
// 目标进度更新时
|
||||
const handleGoalProgress = async (goalName: string, progress: number) => {
|
||||
if (progress >= 50 && progress < 100) {
|
||||
await GoalNotificationHelpers.sendGoalProgressNotification(userName, goalName, progress);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 在心情页面中使用
|
||||
|
||||
```typescript
|
||||
import { MoodNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
|
||||
// 安排每日心情打卡提醒
|
||||
const setupMoodReminder = async () => {
|
||||
await MoodNotificationHelpers.scheduleDailyMoodReminder(userName, 20, 0); // 每天晚上8点
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 在营养页面中使用
|
||||
|
||||
```typescript
|
||||
import { NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
|
||||
// 安排营养记录提醒
|
||||
const setupNutritionReminders = async () => {
|
||||
await NutritionNotificationHelpers.scheduleNutritionReminders(userName);
|
||||
};
|
||||
```
|
||||
|
||||
## 通知模板使用
|
||||
|
||||
```typescript
|
||||
import { NotificationTemplates } from '@/utils/notificationHelpers';
|
||||
import { notificationService } from '@/services/notifications';
|
||||
|
||||
// 使用模板发送通知
|
||||
const sendWorkoutNotification = async (userName: string) => {
|
||||
const notification = NotificationTemplates.workout.start(userName);
|
||||
await notificationService.sendImmediateNotification(notification);
|
||||
};
|
||||
|
||||
const sendGoalNotification = async (userName: string, goalName: string) => {
|
||||
const notification = NotificationTemplates.goal.achievement(userName, goalName);
|
||||
await notificationService.sendImmediateNotification(notification);
|
||||
};
|
||||
```
|
||||
|
||||
## 权限管理
|
||||
|
||||
### 检查权限状态
|
||||
|
||||
```typescript
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
|
||||
function MyComponent() {
|
||||
const { permissionStatus, requestPermission } = useNotifications();
|
||||
|
||||
const handleRequestPermission = async () => {
|
||||
const status = await requestPermission();
|
||||
if (status === 'granted') {
|
||||
console.log('通知权限已授予');
|
||||
} else {
|
||||
console.log('通知权限被拒绝');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>权限状态: {permissionStatus}</Text>
|
||||
<Button title="请求权限" onPress={handleRequestPermission} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 处理权限被拒绝
|
||||
|
||||
```typescript
|
||||
const handleNotificationToggle = async (value: boolean) => {
|
||||
if (value) {
|
||||
try {
|
||||
const status = await requestPermission();
|
||||
if (status === 'granted') {
|
||||
setNotificationEnabled(true);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'权限被拒绝',
|
||||
'请在系统设置中开启通知权限以获得最佳体验',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{ text: '去设置', onPress: () => Linking.openSettings() }
|
||||
]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '请求通知权限失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 通知管理
|
||||
|
||||
### 取消特定通知
|
||||
|
||||
```typescript
|
||||
import { notificationService } from '@/services/notifications';
|
||||
|
||||
// 取消特定通知
|
||||
await notificationService.cancelNotification(notificationId);
|
||||
|
||||
// 取消所有通知
|
||||
await notificationService.cancelAllNotifications();
|
||||
|
||||
// 取消特定类型的通知
|
||||
import { GeneralNotificationHelpers } from '@/utils/notificationHelpers';
|
||||
await GeneralNotificationHelpers.cancelNotificationsByType('workout_reminder');
|
||||
```
|
||||
|
||||
### 查询已安排的通知
|
||||
|
||||
```typescript
|
||||
import { notificationService } from '@/services/notifications';
|
||||
|
||||
// 获取所有已安排的通知
|
||||
const notifications = await notificationService.getAllScheduledNotifications();
|
||||
console.log('已安排的通知数量:', notifications.length);
|
||||
|
||||
notifications.forEach(notification => {
|
||||
console.log('通知ID:', notification.identifier);
|
||||
console.log('通知标题:', notification.content.title);
|
||||
console.log('通知内容:', notification.content.body);
|
||||
console.log('通知类型:', notification.content.data?.type);
|
||||
});
|
||||
```
|
||||
|
||||
## 测试功能
|
||||
|
||||
### 使用测试组件
|
||||
|
||||
项目中包含了一个完整的测试组件 `NotificationTest.tsx`,可以用来测试所有通知功能:
|
||||
|
||||
```typescript
|
||||
import { NotificationTest } from '@/components/NotificationTest';
|
||||
|
||||
// 在开发环境中使用
|
||||
function TestScreen() {
|
||||
return <NotificationTest />;
|
||||
}
|
||||
```
|
||||
|
||||
### 手动测试
|
||||
|
||||
```typescript
|
||||
// 测试立即通知
|
||||
await sendNotification({
|
||||
title: '测试通知',
|
||||
body: '这是一个测试通知',
|
||||
sound: true,
|
||||
priority: 'high'
|
||||
});
|
||||
|
||||
// 测试定时通知(5秒后)
|
||||
const futureDate = new Date(Date.now() + 5000);
|
||||
await scheduleNotification(
|
||||
{
|
||||
title: '定时测试',
|
||||
body: '这是一个5秒后的定时通知',
|
||||
sound: true
|
||||
},
|
||||
futureDate
|
||||
);
|
||||
|
||||
// 测试重复通知(每分钟)
|
||||
await scheduleRepeatingNotification(
|
||||
{
|
||||
title: '重复测试',
|
||||
body: '这是一个每分钟重复的通知',
|
||||
sound: true
|
||||
},
|
||||
{ minutes: 1 }
|
||||
);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 通知内容设计
|
||||
|
||||
- **标题**: 简洁明了,不超过50个字符
|
||||
- **内容**: 具体有用,不超过200个字符
|
||||
- **时机**: 选择合适的发送时间
|
||||
- **频率**: 避免过于频繁的通知
|
||||
|
||||
### 2. 用户体验
|
||||
|
||||
- 提供通知开关选项
|
||||
- 允许用户自定义通知时间
|
||||
- 支持通知类型的选择
|
||||
- 提供通知历史记录
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
- 始终处理权限请求失败
|
||||
- 提供用户友好的错误提示
|
||||
- 实现降级处理方案
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 避免同时发送大量通知
|
||||
- 及时清理不需要的通知
|
||||
- 合理使用重复通知
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **通知不显示**
|
||||
- 检查权限是否已授予
|
||||
- 确认应用是否在前台
|
||||
- 验证通知配置是否正确
|
||||
|
||||
2. **定时通知不触发**
|
||||
- 检查设备是否重启
|
||||
- 确认应用是否被系统杀死
|
||||
- 验证时间设置是否正确
|
||||
|
||||
3. **权限被拒绝**
|
||||
- 引导用户到系统设置
|
||||
- 提供权限说明
|
||||
- 实现降级处理方案
|
||||
|
||||
### 调试技巧
|
||||
|
||||
```typescript
|
||||
// 启用详细日志
|
||||
console.log('通知权限状态:', await notificationService.getPermissionStatus());
|
||||
console.log('已安排通知:', await notificationService.getAllScheduledNotifications());
|
||||
|
||||
// 测试通知发送
|
||||
await notificationService.sendImmediateNotification({
|
||||
title: '调试通知',
|
||||
body: '这是一个调试通知',
|
||||
sound: true
|
||||
});
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
推送通知功能已经完整集成到项目中,提供了丰富的功能和良好的用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。
|
||||
|
||||
如有任何问题或建议,请参考 `docs/notification-implementation.md` 文档或联系开发团队。
|
||||
2
app.json
2
app.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "海豹健康",
|
||||
"name": "Out Live",
|
||||
"slug": "digital-pilates",
|
||||
"version": "1.0.5",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
||||
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
|
||||
import { fetchTodayWaterStats } from '@/store/waterSlice';
|
||||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||||
import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
|
||||
import { getTestHealthData } from '@/utils/mockHealthData';
|
||||
@@ -89,8 +89,6 @@ export default function ExploreScreen() {
|
||||
// 从 Redux 获取指定日期的健康数据
|
||||
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
|
||||
|
||||
// 获取今日喝水统计数据
|
||||
const todayWaterStats = useAppSelector(selectTodayStats);
|
||||
|
||||
// 解构健康数据(支持mock数据)
|
||||
const mockData = useMockData ? getTestHealthData('mock') : null;
|
||||
@@ -101,7 +99,7 @@ export default function ExploreScreen() {
|
||||
const sleepDuration = useMockData ? (mockData?.sleepDuration ?? null) : (healthData?.sleepDuration ?? null);
|
||||
const hrvValue = useMockData ? (mockData?.hrv ?? 0) : (healthData?.hrv ?? 0);
|
||||
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
|
||||
const heartRate = useMockData ? (mockData?.heartRate ?? null) : (healthData?.heartRate ?? null);
|
||||
|
||||
const fitnessRingsData = useMockData ? {
|
||||
activeCalories: mockData?.activeCalories ?? 0,
|
||||
activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350,
|
||||
@@ -455,7 +453,7 @@ export default function ExploreScreen() {
|
||||
|
||||
{/* 右边文字区域 */}
|
||||
<View style={styles.headerTextContainer}>
|
||||
<Text style={styles.headerTitle}>海豹健康</Text>
|
||||
<Text style={styles.headerTitle}>Out Live</Text>
|
||||
</View>
|
||||
|
||||
{/* 开发环境调试按钮 */}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
||||
import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
@@ -32,6 +34,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
||||
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
|
||||
const [dailyGoal, setDailyGoal] = useState<string>('2000');
|
||||
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
|
||||
@@ -47,6 +50,19 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
||||
// 使用新的 hook 来处理指定日期的饮水数据
|
||||
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
|
||||
|
||||
// 检查登录状态
|
||||
useEffect(() => {
|
||||
const checkLoginStatus = async () => {
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
// 如果未登录,用户会被重定向到登录页面
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
checkLoginStatus();
|
||||
}, [ensureLoggedIn]);
|
||||
|
||||
const goalPresets = [1500, 2000, 2500, 3000, 3500, 4000];
|
||||
const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500];
|
||||
|
||||
@@ -203,7 +219,19 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
|
||||
<View style={styles.container}>
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* 装饰性圆圈 */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
|
||||
<HeaderBar
|
||||
title="饮水设置"
|
||||
onBack={() => {
|
||||
@@ -249,7 +277,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
||||
<View style={styles.settingLeft}>
|
||||
<Text style={[styles.settingTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
||||
<Text style={[styles.settingSubtitle, { color: colorTokens.textSecondary }]}>
|
||||
设置点击右上角"+"按钮时添加的默认饮水量
|
||||
{`设置点击右上角"+"按钮时添加的默认饮水量`}
|
||||
</Text>
|
||||
<Text style={[styles.settingValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
|
||||
</View>
|
||||
@@ -382,6 +410,33 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: 'absolute',
|
||||
top: 40,
|
||||
right: 20,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.1,
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -15,
|
||||
left: -15,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.05,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -395,19 +450,19 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 32,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: 20,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
subsectionTitle: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
marginBottom: 12,
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
sectionSubtitle: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '400',
|
||||
lineHeight: 18,
|
||||
},
|
||||
@@ -582,15 +637,18 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 40,
|
||||
gap: 12,
|
||||
gap: 16,
|
||||
},
|
||||
noRecordsText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
lineHeight: 20,
|
||||
},
|
||||
noRecordsSubText: {
|
||||
fontSize: 14,
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
opacity: 0.7,
|
||||
},
|
||||
modalBackdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
||||
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
@@ -26,6 +27,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
||||
selectedDate
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
|
||||
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
||||
|
||||
@@ -120,6 +122,12 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
||||
|
||||
// 处理添加喝水 - 右上角按钮直接添加
|
||||
const handleQuickAddWater = async () => {
|
||||
// 检查用户是否已登录
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 触发震动反馈
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
@@ -135,7 +143,13 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
||||
};
|
||||
|
||||
// 处理卡片点击 - 跳转到饮水设置页面
|
||||
const handleCardPress = () => {
|
||||
const handleCardPress = async () => {
|
||||
// 检查用户是否已登录
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 触发震动反馈
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
||||
@@ -39,7 +39,7 @@ export function WeightHistoryCard() {
|
||||
const dispatch = useAppDispatch();
|
||||
const userProfile = useAppSelector((s) => s.user.profile);
|
||||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [showChart, setShowChart] = useState(false);
|
||||
const [showBMIModal, setShowBMIModal] = useState(false);
|
||||
|
||||
@@ -71,12 +71,9 @@ export function WeightHistoryCard() {
|
||||
|
||||
const loadWeightHistory = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await dispatch(fetchWeightHistory() as any);
|
||||
} catch (error) {
|
||||
console.error('加载体重历史失败:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { usePathname, useRouter } from 'expo-router';
|
||||
import { useCallback } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { api } from '@/services/api';
|
||||
import { logout as logoutAction } from '@/store/userSlice';
|
||||
@@ -24,7 +25,7 @@ export function useAuthGuard() {
|
||||
const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => {
|
||||
if (isLoggedIn) return true;
|
||||
|
||||
const redirectTo = options?.redirectTo ?? currentPath ?? '/(tabs)';
|
||||
const redirectTo = options?.redirectTo ?? currentPath ?? ROUTES.TAB_STATISTICS;
|
||||
const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined;
|
||||
|
||||
router.push({
|
||||
|
||||
@@ -13,6 +13,8 @@ install! 'cocoapods',
|
||||
|
||||
prepare_react_native_project!
|
||||
|
||||
project 'digitalpilates.xcodeproj'
|
||||
|
||||
target 'digitalpilates' do
|
||||
use_expo_modules!
|
||||
|
||||
|
||||
@@ -2388,10 +2388,10 @@ SPEC CHECKSUMS:
|
||||
ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d
|
||||
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
||||
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
|
||||
fast_float: 23278fd30b349f976d2014f4aec9e2d7bc1c3806
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
|
||||
fmt: b85d977e8fe789fd71c77123f9f4920d88c4d170
|
||||
glog: 682871fb30f4a65f657bf357581110656ea90b08
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
@@ -2401,7 +2401,7 @@ SPEC CHECKSUMS:
|
||||
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||
RCT-Folly: 031db300533e2dfa954cdc5a859b792d5c14ed7b
|
||||
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
|
||||
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
|
||||
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
|
||||
RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab
|
||||
@@ -2490,6 +2490,6 @@ SPEC CHECKSUMS:
|
||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||
|
||||
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
||||
PODFILE CHECKSUM: d6bbff2c1fc3ea45fb6bb3b1c3bb766a4f2d5dd4
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -348,7 +348,7 @@
|
||||
"FB_SONARKIT_ENABLED=1",
|
||||
);
|
||||
INFOPLIST_FILE = digitalpilates/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "海豹健康";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -386,7 +386,7 @@
|
||||
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
||||
INFOPLIST_FILE = digitalpilates/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "海豹健康";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>海豹健康</string>
|
||||
<string>Out Live</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -23,7 +23,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.8</string>
|
||||
<string>1.0.9</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
# 喝水记录 API 修复测试文档
|
||||
|
||||
## 修复内容总结
|
||||
|
||||
### 1. 服务层修复 (services/waterRecords.ts)
|
||||
|
||||
#### 接口路径修复
|
||||
- ✅ 更新喝水目标:`/water-goal` → `/water-records/goal/daily`
|
||||
- ✅ 获取统计数据:`/water-stats/today` → `/water-records/stats`
|
||||
- ✅ 获取指定日期统计:`/water-stats/${date}` → `/water-records/stats?date=${date}`
|
||||
|
||||
#### 数据结构修复
|
||||
- ✅ 字段名称:`remark` → `note`
|
||||
- ✅ 枚举值:`'manual' | 'auto' | 'other'` → `'Manual' | 'Auto'`
|
||||
- ✅ 新增字段:`recordedAt` (记录时间)
|
||||
- ✅ 响应结构:处理标准 API 响应格式 `{ data: {...}, pagination: {...} }`
|
||||
|
||||
#### 类型定义更新
|
||||
```typescript
|
||||
// 旧版本
|
||||
interface WaterRecord {
|
||||
source: 'manual' | 'auto' | 'other';
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
// 新版本
|
||||
interface WaterRecord {
|
||||
source?: 'Manual' | 'Auto';
|
||||
note?: string;
|
||||
recordedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Redux Store 修复 (store/waterSlice.ts)
|
||||
|
||||
#### Loading 状态完善
|
||||
- ✅ 新增:`create`, `update`, `delete` loading 状态
|
||||
|
||||
#### 完成率计算修复
|
||||
- ✅ 统一使用百分比格式:`(totalAmount / dailyGoal) * 100`
|
||||
- ✅ 所有相关计算都已更新
|
||||
|
||||
#### 日期字段处理
|
||||
- ✅ 优先使用 `recordedAt`,回退到 `createdAt`
|
||||
|
||||
### 3. Hooks 修复 (hooks/useWaterData.ts)
|
||||
|
||||
#### 函数签名更新
|
||||
```typescript
|
||||
// 旧版本
|
||||
addWaterRecord(amount: number, remark?: string)
|
||||
|
||||
// 新版本
|
||||
addWaterRecord(amount: number, note?: string, recordedAt?: string)
|
||||
```
|
||||
|
||||
#### 完成率计算
|
||||
- ✅ 返回百分比格式而非小数
|
||||
|
||||
### 4. 组件修复
|
||||
|
||||
#### WaterIntakeCard.tsx
|
||||
- ✅ 日期字段:优先使用 `recordedAt`
|
||||
- ✅ 完成率显示:移除多余的 `* 100` 计算
|
||||
|
||||
#### AddWaterModal.tsx
|
||||
- ✅ 字段名称:`remark` → `note`
|
||||
- ✅ 数据结构:添加 `source: 'Manual'`
|
||||
|
||||
## 测试要点
|
||||
|
||||
### 1. API 调用测试
|
||||
```javascript
|
||||
// 测试创建记录
|
||||
const createResult = await createWaterRecord({
|
||||
amount: 250,
|
||||
note: "测试记录",
|
||||
source: "Manual",
|
||||
recordedAt: "2023-12-01T10:00:00.000Z"
|
||||
});
|
||||
|
||||
// 测试获取统计
|
||||
const stats = await getTodayWaterStats();
|
||||
console.log('完成率应该是百分比:', stats.completionRate); // 应该是 0-100 的数值
|
||||
|
||||
// 测试更新目标
|
||||
const goalResult = await updateWaterGoal({ dailyWaterGoal: 2500 });
|
||||
```
|
||||
|
||||
### 2. Redux 状态测试
|
||||
```javascript
|
||||
// 测试完成率计算
|
||||
// 假设总量 1500ml,目标 2000ml
|
||||
// 期望完成率:75 (百分比)
|
||||
const expectedRate = (1500 / 2000) * 100; // 75
|
||||
```
|
||||
|
||||
### 3. 组件渲染测试
|
||||
- ✅ 完成率显示正确(不会超过 100%)
|
||||
- ✅ 图表数据使用正确的时间字段
|
||||
- ✅ 表单提交使用正确的字段名称
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
### 向后兼容
|
||||
- ✅ 保留了 `createdAt` 字段的回退逻辑
|
||||
- ✅ 保留了单日期查询的兼容性处理
|
||||
- ✅ 保留了原有的选择器函数
|
||||
|
||||
### 新功能支持
|
||||
- ✅ 支持自定义记录时间 (`recordedAt`)
|
||||
- ✅ 支持新的 API 响应格式
|
||||
- ✅ 支持百分比格式的完成率
|
||||
|
||||
## 需要验证的功能
|
||||
|
||||
1. **创建记录**:确保新记录包含正确的字段
|
||||
2. **更新记录**:确保更新时使用正确的字段名
|
||||
3. **删除记录**:确保删除后统计数据正确更新
|
||||
4. **目标设置**:确保目标更新后完成率重新计算
|
||||
5. **统计查询**:确保返回正确的百分比格式完成率
|
||||
6. **图表显示**:确保使用正确的时间字段进行分组
|
||||
|
||||
## 潜在问题
|
||||
|
||||
1. **时区处理**:`recordedAt` 字段的时区处理需要注意
|
||||
2. **数据迁移**:现有数据可能没有 `recordedAt` 字段
|
||||
3. **API 兼容性**:确保后端 API 已经更新到新版本
|
||||
|
||||
## 建议测试流程
|
||||
|
||||
1. 单元测试:测试各个函数的输入输出
|
||||
2. 集成测试:测试 Redux 状态管理
|
||||
3. 端到端测试:测试完整的用户操作流程
|
||||
4. API 测试:使用 Postman 或类似工具测试 API 接口
|
||||
Reference in New Issue
Block a user