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": {
|
"expo": {
|
||||||
"name": "海豹健康",
|
"name": "Out Live",
|
||||||
"slug": "digital-pilates",
|
"slug": "digital-pilates",
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
|
|||||||
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
|
||||||
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||||
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
|
import { fetchTodayWaterStats } from '@/store/waterSlice';
|
||||||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
|
import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
|
||||||
import { getTestHealthData } from '@/utils/mockHealthData';
|
import { getTestHealthData } from '@/utils/mockHealthData';
|
||||||
@@ -89,8 +89,6 @@ export default function ExploreScreen() {
|
|||||||
// 从 Redux 获取指定日期的健康数据
|
// 从 Redux 获取指定日期的健康数据
|
||||||
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
|
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
|
||||||
|
|
||||||
// 获取今日喝水统计数据
|
|
||||||
const todayWaterStats = useAppSelector(selectTodayStats);
|
|
||||||
|
|
||||||
// 解构健康数据(支持mock数据)
|
// 解构健康数据(支持mock数据)
|
||||||
const mockData = useMockData ? getTestHealthData('mock') : null;
|
const mockData = useMockData ? getTestHealthData('mock') : null;
|
||||||
@@ -101,7 +99,7 @@ export default function ExploreScreen() {
|
|||||||
const sleepDuration = useMockData ? (mockData?.sleepDuration ?? null) : (healthData?.sleepDuration ?? null);
|
const sleepDuration = useMockData ? (mockData?.sleepDuration ?? null) : (healthData?.sleepDuration ?? null);
|
||||||
const hrvValue = useMockData ? (mockData?.hrv ?? 0) : (healthData?.hrv ?? 0);
|
const hrvValue = useMockData ? (mockData?.hrv ?? 0) : (healthData?.hrv ?? 0);
|
||||||
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
|
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
|
||||||
const heartRate = useMockData ? (mockData?.heartRate ?? null) : (healthData?.heartRate ?? null);
|
|
||||||
const fitnessRingsData = useMockData ? {
|
const fitnessRingsData = useMockData ? {
|
||||||
activeCalories: mockData?.activeCalories ?? 0,
|
activeCalories: mockData?.activeCalories ?? 0,
|
||||||
activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350,
|
activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350,
|
||||||
@@ -455,7 +453,7 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
{/* 右边文字区域 */}
|
{/* 右边文字区域 */}
|
||||||
<View style={styles.headerTextContainer}>
|
<View style={styles.headerTextContainer}>
|
||||||
<Text style={styles.headerTitle}>海豹健康</Text>
|
<Text style={styles.headerTitle}>Out Live</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 开发环境调试按钮 */}
|
{/* 开发环境调试按钮 */}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
||||||
import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences';
|
import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { Picker } from '@react-native-picker/picker';
|
import { Picker } from '@react-native-picker/picker';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +34,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
|
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
const { ensureLoggedIn } = useAuthGuard();
|
||||||
|
|
||||||
const [dailyGoal, setDailyGoal] = useState<string>('2000');
|
const [dailyGoal, setDailyGoal] = useState<string>('2000');
|
||||||
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
|
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
|
||||||
@@ -47,6 +50,19 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
// 使用新的 hook 来处理指定日期的饮水数据
|
// 使用新的 hook 来处理指定日期的饮水数据
|
||||||
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
|
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 goalPresets = [1500, 2000, 2500, 3000, 3500, 4000];
|
||||||
const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500];
|
const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500];
|
||||||
|
|
||||||
@@ -203,7 +219,19 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<HeaderBar
|
||||||
title="饮水设置"
|
title="饮水设置"
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
@@ -249,7 +277,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
<View style={styles.settingLeft}>
|
<View style={styles.settingLeft}>
|
||||||
<Text style={[styles.settingTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
<Text style={[styles.settingTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
||||||
<Text style={[styles.settingSubtitle, { color: colorTokens.textSecondary }]}>
|
<Text style={[styles.settingSubtitle, { color: colorTokens.textSecondary }]}>
|
||||||
设置点击右上角"+"按钮时添加的默认饮水量
|
{`设置点击右上角"+"按钮时添加的默认饮水量`}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.settingValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
|
<Text style={[styles.settingValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -382,6 +410,33 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
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: {
|
keyboardAvoidingView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -395,19 +450,19 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 20,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
},
|
},
|
||||||
subsectionTitle: {
|
subsectionTitle: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
letterSpacing: -0.3,
|
letterSpacing: -0.3,
|
||||||
},
|
},
|
||||||
sectionSubtitle: {
|
sectionSubtitle: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
@@ -582,15 +637,18 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingVertical: 40,
|
paddingVertical: 40,
|
||||||
gap: 12,
|
gap: 16,
|
||||||
},
|
},
|
||||||
noRecordsText: {
|
noRecordsText: {
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
noRecordsSubText: {
|
noRecordsSubText: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
lineHeight: 18,
|
||||||
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
modalBackdrop: {
|
modalBackdrop: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
||||||
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
import { getQuickWaterAmount } from '@/utils/userPreferences';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
@@ -26,6 +27,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
selectedDate
|
selectedDate
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { ensureLoggedIn } = useAuthGuard();
|
||||||
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
|
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
|
||||||
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
||||||
|
|
||||||
@@ -120,6 +122,12 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
|
|
||||||
// 处理添加喝水 - 右上角按钮直接添加
|
// 处理添加喝水 - 右上角按钮直接添加
|
||||||
const handleQuickAddWater = async () => {
|
const handleQuickAddWater = async () => {
|
||||||
|
// 检查用户是否已登录
|
||||||
|
const isLoggedIn = await ensureLoggedIn();
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 触发震动反馈
|
// 触发震动反馈
|
||||||
if (process.env.EXPO_OS === 'ios') {
|
if (process.env.EXPO_OS === 'ios') {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
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') {
|
if (process.env.EXPO_OS === 'ios') {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function WeightHistoryCard() {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const userProfile = useAppSelector((s) => s.user.profile);
|
const userProfile = useAppSelector((s) => s.user.profile);
|
||||||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [showChart, setShowChart] = useState(false);
|
const [showChart, setShowChart] = useState(false);
|
||||||
const [showBMIModal, setShowBMIModal] = useState(false);
|
const [showBMIModal, setShowBMIModal] = useState(false);
|
||||||
|
|
||||||
@@ -71,12 +71,9 @@ export function WeightHistoryCard() {
|
|||||||
|
|
||||||
const loadWeightHistory = async () => {
|
const loadWeightHistory = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
|
||||||
await dispatch(fetchWeightHistory() as any);
|
await dispatch(fetchWeightHistory() as any);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载体重历史失败:', error);
|
console.error('加载体重历史失败:', error);
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { usePathname, useRouter } from 'expo-router';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Alert } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
import { logout as logoutAction } from '@/store/userSlice';
|
import { logout as logoutAction } from '@/store/userSlice';
|
||||||
@@ -24,7 +25,7 @@ export function useAuthGuard() {
|
|||||||
const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => {
|
const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => {
|
||||||
if (isLoggedIn) return true;
|
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;
|
const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined;
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
@@ -62,7 +63,7 @@ export function useAuthGuard() {
|
|||||||
try {
|
try {
|
||||||
// 调用 Redux action 清除本地状态和缓存
|
// 调用 Redux action 清除本地状态和缓存
|
||||||
await dispatch(logoutAction()).unwrap();
|
await dispatch(logoutAction()).unwrap();
|
||||||
|
|
||||||
// 跳转到登录页面
|
// 跳转到登录页面
|
||||||
router.push('/auth/login');
|
router.push('/auth/login');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -95,13 +96,13 @@ export function useAuthGuard() {
|
|||||||
try {
|
try {
|
||||||
// 调用注销账号API
|
// 调用注销账号API
|
||||||
await api.delete('/api/users/delete-account');
|
await api.delete('/api/users/delete-account');
|
||||||
|
|
||||||
// 清除额外的本地数据
|
// 清除额外的本地数据
|
||||||
await AsyncStorage.multiRemove(['@user_personal_info', '@onboarding_completed']);
|
await AsyncStorage.multiRemove(['@user_personal_info', '@onboarding_completed']);
|
||||||
|
|
||||||
// 执行退出登录逻辑
|
// 执行退出登录逻辑
|
||||||
await dispatch(logoutAction()).unwrap();
|
await dispatch(logoutAction()).unwrap();
|
||||||
|
|
||||||
Alert.alert('账号已注销', '您的账号已成功注销', [
|
Alert.alert('账号已注销', '您的账号已成功注销', [
|
||||||
{
|
{
|
||||||
text: '确定',
|
text: '确定',
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ install! 'cocoapods',
|
|||||||
|
|
||||||
prepare_react_native_project!
|
prepare_react_native_project!
|
||||||
|
|
||||||
|
project 'digitalpilates.xcodeproj'
|
||||||
|
|
||||||
target 'digitalpilates' do
|
target 'digitalpilates' do
|
||||||
use_expo_modules!
|
use_expo_modules!
|
||||||
|
|
||||||
|
|||||||
@@ -2388,10 +2388,10 @@ SPEC CHECKSUMS:
|
|||||||
ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d
|
ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d
|
||||||
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
||||||
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
|
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
|
||||||
fast_float: 23278fd30b349f976d2014f4aec9e2d7bc1c3806
|
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||||
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
|
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
|
||||||
fmt: b85d977e8fe789fd71c77123f9f4920d88c4d170
|
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||||
glog: 682871fb30f4a65f657bf357581110656ea90b08
|
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
@@ -2401,7 +2401,7 @@ SPEC CHECKSUMS:
|
|||||||
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
||||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||||
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||||
RCT-Folly: 031db300533e2dfa954cdc5a859b792d5c14ed7b
|
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
|
||||||
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
|
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
|
||||||
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
|
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
|
||||||
RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab
|
RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab
|
||||||
@@ -2490,6 +2490,6 @@ SPEC CHECKSUMS:
|
|||||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||||
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
|
||||||
|
|
||||||
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
PODFILE CHECKSUM: d6bbff2c1fc3ea45fb6bb3b1c3bb766a4f2d5dd4
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
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",
|
"FB_SONARKIT_ENABLED=1",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = digitalpilates/Info.plist;
|
INFOPLIST_FILE = digitalpilates/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "海豹健康";
|
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
||||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
||||||
INFOPLIST_FILE = digitalpilates/Info.plist;
|
INFOPLIST_FILE = digitalpilates/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "海豹健康";
|
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>海豹健康</string>
|
<string>Out Live</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.8</string>
|
<string>1.0.9</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<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