feat: 更新依赖项并优化组件结构

- 在 package.json 和 package-lock.json 中新增 @sentry/react-native、react-native-device-info 和 react-native-purchases 依赖
- 更新统计页面,替换 CircularRing 组件为 FitnessRingsCard,增强健身数据展示
- 在布局文件中引入 ToastProvider,优化用户通知体验
- 新增 SuccessToast 组件,提供全局成功提示功能
- 更新健康数据获取逻辑,支持健身圆环数据的提取
- 优化多个组件的样式和交互,提升用户体验
This commit is contained in:
richarjiang
2025-08-21 09:51:25 +08:00
parent 19b92547e1
commit 78620f18ee
21 changed files with 2494 additions and 108 deletions

View File

@@ -1,5 +1,5 @@
import dayjs from 'dayjs';
import type { HealthKitPermissions } from 'react-native-health';
import type { HealthActivitySummary, HealthKitPermissions } from 'react-native-health';
import AppleHealthKit from 'react-native-health';
const PERMISSIONS: HealthKitPermissions = {
@@ -9,6 +9,7 @@ const PERMISSIONS: HealthKitPermissions = {
AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
AppleHealthKit.Constants.Permissions.SleepAnalysis,
AppleHealthKit.Constants.Permissions.HeartRateVariability,
AppleHealthKit.Constants.Permissions.ActivitySummary,
],
write: [
// 支持体重写入
@@ -22,6 +23,13 @@ export type TodayHealthData = {
activeEnergyBurned: number; // kilocalories
sleepDuration: number; // 睡眠时长(分钟)
hrv: number | null; // 心率变异性 (ms)
// 健身圆环数据
activeCalories: number;
activeCaloriesGoal: number;
exerciseMinutes: number;
exerciseMinutesGoal: number;
standHours: number;
standHoursGoal: number;
};
export async function ensureHealthPermissions(): Promise<boolean> {
@@ -57,13 +65,20 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
endDate: end.toISOString()
} as any;
const activitySummaryOptions = {
startDate: start.toISOString(),
endDate: end.toISOString()
};
console.log('查询选项:', options);
// 并行获取所有健康数据
const [steps, calories, sleepDuration, hrv] = await Promise.all([
// 并行获取所有健康数据包括ActivitySummary
const [steps, calories, sleepDuration, hrv, activitySummary] = await Promise.all([
// 获取步数
new Promise<number>((resolve) => {
AppleHealthKit.getStepCount(options, (err, res) => {
AppleHealthKit.getStepCount({
date: dayjs(date).toISOString()
}, (err, res) => {
if (err) {
console.error('获取步数失败:', err);
return resolve(0);
@@ -144,11 +159,43 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
resolve(null);
}
});
}),
// 获取ActivitySummary数据健身圆环数据
new Promise<HealthActivitySummary | null>((resolve) => {
AppleHealthKit.getActivitySummary(
activitySummaryOptions,
(err: Object, results: HealthActivitySummary[]) => {
if (err) {
console.error('获取ActivitySummary失败:', err);
return resolve(null);
}
if (!results || results.length === 0) {
console.warn('ActivitySummary数据为空');
return resolve(null);
}
console.log('ActivitySummary数据:', results[0]);
resolve(results[0]);
},
);
})
]);
console.log('指定日期健康数据获取完成:', { steps, calories, sleepDuration, hrv });
return { steps, activeEnergyBurned: calories, sleepDuration, hrv };
console.log('指定日期健康数据获取完成:', { steps, calories, sleepDuration, hrv, activitySummary });
return {
steps,
activeEnergyBurned: calories,
sleepDuration,
hrv,
// 健身圆环数据
activeCalories: activitySummary?.activeEnergyBurned || 0,
activeCaloriesGoal: activitySummary?.activeEnergyBurnedGoal || 350,
exerciseMinutes: activitySummary?.appleExerciseTime || 0,
exerciseMinutesGoal: activitySummary?.appleExerciseTimeGoal || 30,
standHours: activitySummary?.appleStandHours || 0,
standHoursGoal: activitySummary?.appleStandHoursGoal || 12
};
}
export async function fetchTodayHealthData(): Promise<TodayHealthData> {

24
utils/native.utils.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Dimensions, StatusBar } from 'react-native';
import DeviceInfo from 'react-native-device-info';
export const getStatusBarHeight = () => {
return StatusBar.currentHeight;
};
export const getDeviceDimensions = () => {
const { width, height } = Dimensions.get('window');
// 检测是否为平板设备
const isTablet = DeviceInfo.isTablet();
// 对于平板设备,使用不同的基准尺寸
const baseWidth = isTablet ? 768 : 375; // iPad 通常使用 768pt 作为宽度基准
const ratio = width / baseWidth;
return {
width,
height,
ratio,
isTablet,
};
};

113
utils/sentry.utils.ts Normal file
View File

@@ -0,0 +1,113 @@
import * as Sentry from '@sentry/react-native';
export const captureException = (error: Error) => {
Sentry.captureException(error);
};
export const captureMessage = (message: string) => {
Sentry.captureMessage(message);
};
// 智能处理大型对象的日志记录,避免截断
export const captureMessageWithContext = (
message: string,
context?: Record<string, any>,
level: 'debug' | 'info' | 'warning' | 'error' = 'info'
) => {
try {
// 如果有上下文数据,优先使用 addBreadcrumb 或 setContext
if (context) {
// 检查 JSON 字符串长度
const contextString = JSON.stringify(context);
if (contextString.length > 7000) { // 留 1000+ 字符给消息本身
// 如果上下文数据太大,将其作为 context 附加到事件
Sentry.withScope(scope => {
scope.setContext('large_data', context);
Sentry.captureMessage(`${message} (大型上下文数据已附加到事件上下文)`, level);
});
} else {
// 数据较小时,直接包含在消息中
Sentry.captureMessage(`${message}: ${contextString}`, level);
}
} else {
Sentry.captureMessage(message, level);
}
} catch (error) {
// 如果 JSON.stringify 失败,回退到基本消息
console.error('序列化上下文数据失败:', error);
Sentry.captureMessage(`${message} (上下文数据序列化失败)`, level);
}
};
// 专门用于记录购买相关的日志,带有结构化数据
export const capturePurchaseEvent = (
eventType: 'init' | 'success' | 'error' | 'restore',
message: string,
data?: any
) => {
const tags = {
event_type: 'purchase',
purchase_action: eventType,
};
Sentry.withScope(scope => {
// 设置标签
Object.entries(tags).forEach(([key, value]) => {
scope.setTag(key, value);
});
// 如果有数据,设置为上下文
if (data) {
scope.setContext('purchase_data', data);
}
// 根据事件类型设置适当的级别
const level = eventType === 'error' ? 'error' : 'info';
Sentry.captureMessage(message, level);
});
};
// 记录用户操作日志
export const captureUserAction = (
action: string,
details?: Record<string, any>
) => {
Sentry.addBreadcrumb({
message: `用户操作: ${action}`,
category: 'user_action',
data: details,
level: 'info',
});
};
// 记录 API 调用日志
export const captureApiCall = (
endpoint: string,
method: string,
success: boolean,
responseData?: any,
error?: Error
) => {
const breadcrumb = {
message: `API ${method} ${endpoint}`,
category: 'http',
data: {
method,
endpoint,
success,
} as any,
level: success ? 'info' : 'error' as any,
};
if (success && responseData) {
// 对于成功的响应,只记录关键信息避免数据过大
breadcrumb.data.response_keys = Object.keys(responseData);
}
if (error) {
breadcrumb.data.error = error.message;
}
Sentry.addBreadcrumb(breadcrumb);
};

58
utils/toast.utils.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* 全局Toast工具函数
*
* 使用方式:
* import { Toast } from '@/utils/toast.utils';
*
* Toast.success('操作成功!');
* Toast.error('操作失败!');
* Toast.warning('注意!');
*/
import { ToastContextType } from '@/contexts/ToastContext';
let toastRef: ToastContextType | null = null;
export const setToastRef = (ref: ToastContextType) => {
toastRef = ref;
};
export const Toast = {
success: (message: string, duration?: number) => {
if (toastRef) {
toastRef.showSuccess(message, duration);
} else {
console.warn('Toast not initialized. Please wrap your app with ToastProvider');
}
},
error: (message: string, duration?: number) => {
if (toastRef) {
toastRef.showError(message, duration);
} else {
console.warn('Toast not initialized. Please wrap your app with ToastProvider');
}
},
warning: (message: string, duration?: number) => {
if (toastRef) {
toastRef.showWarning(message, duration);
} else {
console.warn('Toast not initialized. Please wrap your app with ToastProvider');
}
},
show: (config: {
message: string;
duration?: number;
backgroundColor?: string;
textColor?: string;
icon?: string;
}) => {
if (toastRef) {
toastRef.showToast(config);
} else {
console.warn('Toast not initialized. Please wrap your app with ToastProvider');
}
},
};