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,6 +1,6 @@
import { AnimatedNumber } from '@/components/AnimatedNumber'; import { AnimatedNumber } from '@/components/AnimatedNumber';
import { BMICard } from '@/components/BMICard'; import { BMICard } from '@/components/BMICard';
import { CircularRing } from '@/components/CircularRing'; import { FitnessRingsCard } from '@/components/FitnessRingsCard';
import { NutritionRadarCard } from '@/components/NutritionRadarCard'; import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar'; import { ProgressBar } from '@/components/ProgressBar';
import { StressMeter } from '@/components/StressMeter'; import { StressMeter } from '@/components/StressMeter';
@@ -52,13 +52,21 @@ export default function ExploreScreen() {
// 日期条自动滚动到选中项 // 日期条自动滚动到选中项
const daysScrollRef = useRef<import('react-native').ScrollView | null>(null); const daysScrollRef = useRef<import('react-native').ScrollView | null>(null);
const [scrollWidth, setScrollWidth] = useState(0); const [scrollWidth, setScrollWidth] = useState(0);
const DAY_PILL_WIDTH = 68; const DAY_PILL_WIDTH = 48;
const DAY_PILL_SPACING = 12; const DAY_PILL_SPACING = 8;
const scrollToIndex = (index: number, animated = true) => { const scrollToIndex = (index: number, animated = true) => {
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING); if (!daysScrollRef.current || scrollWidth === 0) return;
const itemWidth = DAY_PILL_WIDTH + DAY_PILL_SPACING;
const baseOffset = index * itemWidth;
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2)); const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
// 确保不会滚动超出边界
const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth);
const finalOffset = Math.min(centerOffset, maxScrollOffset);
daysScrollRef.current.scrollTo({ x: finalOffset, animated });
}; };
useEffect(() => { useEffect(() => {
@@ -68,6 +76,14 @@ export default function ExploreScreen() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollWidth]); }, [scrollWidth]);
// 当选中索引变化时,滚动到对应位置
useEffect(() => {
if (scrollWidth > 0) {
scrollToIndex(selectedIndex, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedIndex]);
// HealthKit: 每次页面聚焦都拉取今日数据 // HealthKit: 每次页面聚焦都拉取今日数据
const [stepCount, setStepCount] = useState<number | null>(null); const [stepCount, setStepCount] = useState<number | null>(null);
const [activeCalories, setActiveCalories] = useState<number | null>(null); const [activeCalories, setActiveCalories] = useState<number | null>(null);
@@ -76,6 +92,15 @@ export default function ExploreScreen() {
// HRV数据 // HRV数据
const [hrvValue, setHrvValue] = useState<number>(0); const [hrvValue, setHrvValue] = useState<number>(0);
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(new Date()); const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(new Date());
// 健身圆环数据
const [fitnessRingsData, setFitnessRingsData] = useState({
activeCalories: 0,
activeCaloriesGoal: 350,
exerciseMinutes: 0,
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12
});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// 用于触发动画重置的 token当日期或数据变化时更新 // 用于触发动画重置的 token当日期或数据变化时更新
@@ -124,6 +149,15 @@ export default function ExploreScreen() {
setStepCount(data.steps); setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned)); setActiveCalories(Math.round(data.activeEnergyBurned));
setSleepDuration(data.sleepDuration); setSleepDuration(data.sleepDuration);
// 更新健身圆环数据
setFitnessRingsData({
activeCalories: data.activeCalories,
activeCaloriesGoal: data.activeCaloriesGoal,
exerciseMinutes: data.exerciseMinutes,
exerciseMinutesGoal: data.exerciseMinutesGoal,
standHours: data.standHours,
standHoursGoal: data.standHoursGoal
});
const hrv = data.hrv ?? 0; const hrv = data.hrv ?? 0;
setHrvValue(hrv); setHrvValue(hrv);
@@ -195,7 +229,6 @@ export default function ExploreScreen() {
// 日期点击时,加载对应日期数据 // 日期点击时,加载对应日期数据
const onSelectDate = (index: number) => { const onSelectDate = (index: number) => {
setSelectedIndex(index); setSelectedIndex(index);
scrollToIndex(index);
const target = days[index]?.date?.toDate(); const target = days[index]?.date?.toDate();
if (target) { if (target) {
loadHealthData(target); loadHealthData(target);
@@ -320,19 +353,17 @@ export default function ExploreScreen() {
// compact={true} // compact={true}
/> />
<View style={[styles.masonryCard, styles.trainingCard]}> <FitnessRingsCard
<Text style={styles.cardTitleSecondary}></Text> activeCalories={fitnessRingsData.activeCalories}
<View style={styles.trainingContent}> activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
<CircularRing exerciseMinutes={fitnessRingsData.exerciseMinutes}
size={120} exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
strokeWidth={12} standHours={fitnessRingsData.standHours}
trackColor="#E2D9FD" standHoursGoal={fitnessRingsData.standHoursGoal}
progressColor="#8B74F3"
progress={trainingProgress}
resetToken={animToken} resetToken={animToken}
style={styles.masonryCard}
/> />
</View>
</View>
<View style={[styles.masonryCard, styles.sleepCard]}> <View style={[styles.masonryCard, styles.sleepCard]}>
<View style={styles.cardHeaderRow}> <View style={styles.cardHeaderRow}>
@@ -396,13 +427,13 @@ const styles = StyleSheet.create({
}, },
dayItemWrapper: { dayItemWrapper: {
alignItems: 'center', alignItems: 'center',
width: 68, width: 48,
marginRight: 12, marginRight: 8,
}, },
dayPill: { dayPill: {
width: 68, width: 48,
height: 68, height: 48,
borderRadius: 18, borderRadius: 14,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
@@ -413,16 +444,16 @@ const styles = StyleSheet.create({
backgroundColor: lightColors.datePickerSelected, backgroundColor: lightColors.datePickerSelected,
}, },
dayLabel: { dayLabel: {
fontSize: 16, fontSize: 12,
fontWeight: '700', fontWeight: '700',
color: '#192126', color: '#192126',
marginBottom: 2, marginBottom: 1,
}, },
dayLabelSelected: { dayLabelSelected: {
color: '#FFFFFF', color: '#FFFFFF',
}, },
dayDate: { dayDate: {
fontSize: 16, fontSize: 12,
fontWeight: '800', fontWeight: '800',
color: '#192126', color: '#192126',
}, },
@@ -430,12 +461,12 @@ const styles = StyleSheet.create({
color: '#FFFFFF', color: '#FFFFFF',
}, },
selectedDot: { selectedDot: {
width: 8, width: 5,
height: 8, height: 5,
borderRadius: 4, borderRadius: 2.5,
backgroundColor: lightColors.datePickerSelected, backgroundColor: lightColors.datePickerSelected,
marginTop: 10, marginTop: 6,
marginBottom: 4, marginBottom: 2,
alignSelf: 'center', alignSelf: 'center',
}, },
sectionTitle: { sectionTitle: {
@@ -481,13 +512,13 @@ const styles = StyleSheet.create({
cardTitleSecondary: { cardTitleSecondary: {
color: '#9AA3AE', color: '#9AA3AE',
fontSize: 14, fontSize: 10,
fontWeight: '600', fontWeight: '600',
marginBottom: 10, marginBottom: 10,
}, },
caloriesValue: { caloriesValue: {
color: '#192126', color: '#192126',
fontSize: 22, fontSize: 18,
fontWeight: '800', fontWeight: '800',
}, },
trainingContent: { trainingContent: {
@@ -569,8 +600,8 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
iconSquare: { iconSquare: {
width: 30, width: 24,
height: 30, height: 24,
borderRadius: 8, borderRadius: 8,
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
alignItems: 'center', alignItems: 'center',
@@ -578,7 +609,7 @@ const styles = StyleSheet.create({
marginRight: 10, marginRight: 10,
}, },
cardTitle: { cardTitle: {
fontSize: 18, fontSize: 14,
fontWeight: '800', fontWeight: '800',
color: '#192126', color: '#192126',
}, },
@@ -606,7 +637,7 @@ const styles = StyleSheet.create({
backgroundColor: '#FFE4B8', backgroundColor: '#FFE4B8',
}, },
stepsValue: { stepsValue: {
fontSize: 16, fontSize: 14,
color: '#7A6A42', color: '#7A6A42',
fontWeight: '700', fontWeight: '700',
marginBottom: 8, marginBottom: 8,

View File

@@ -12,9 +12,9 @@ import { store } from '@/store';
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice'; import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
import React from 'react'; import React from 'react';
import RNExitApp from 'react-native-exit-app'; import RNExitApp from 'react-native-exit-app';
import Toast from 'react-native-toast-message';
import { DialogProvider } from '@/components/ui/DialogProvider'; import { DialogProvider } from '@/components/ui/DialogProvider';
import { ToastProvider } from '@/contexts/ToastContext';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
function Bootstrapper({ children }: { children: React.ReactNode }) { function Bootstrapper({ children }: { children: React.ReactNode }) {
@@ -58,7 +58,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
onAgree={handlePrivacyAgree} onAgree={handlePrivacyAgree}
onDisagree={handlePrivacyDisagree} onDisagree={handlePrivacyDisagree}
/> />
<Toast />
</DialogProvider> </DialogProvider>
); );
} }
@@ -77,6 +76,7 @@ export default function RootLayout() {
return ( return (
<Provider store={store}> <Provider store={store}>
<Bootstrapper> <Bootstrapper>
<ToastProvider>
<ThemeProvider value={DefaultTheme}> <ThemeProvider value={DefaultTheme}>
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="onboarding" /> <Stack.Screen name="onboarding" />
@@ -96,8 +96,8 @@ export default function RootLayout() {
<Stack.Screen name="+not-found" /> <Stack.Screen name="+not-found" />
</Stack> </Stack>
<StatusBar style="dark" /> <StatusBar style="dark" />
<Toast />
</ThemeProvider> </ThemeProvider>
</ToastProvider>
</Bootstrapper> </Bootstrapper>
</Provider> </Provider>
); );

View File

@@ -43,21 +43,37 @@ export default function NutritionRecordsScreen() {
// 日期滚动相关 // 日期滚动相关
const daysScrollRef = useRef<ScrollView | null>(null); const daysScrollRef = useRef<ScrollView | null>(null);
const [scrollWidth, setScrollWidth] = useState(0); const [scrollWidth, setScrollWidth] = useState(0);
const DAY_PILL_WIDTH = 68; const DAY_PILL_WIDTH = 60; // 48px width + 12px marginRight = 60px total per item
const DAY_PILL_SPACING = 12; const DAY_PILL_SPACING = 0; // spacing is included in the width above
// 日期滚动控制 // 日期滚动控制
const scrollToIndex = (index: number, animated = true) => { const scrollToIndex = (index: number, animated = true) => {
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING); if (scrollWidth <= 0) return;
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
const itemOffset = index * DAY_PILL_WIDTH;
const scrollViewCenterX = scrollWidth / 2;
const itemCenterX = DAY_PILL_WIDTH / 2;
const centerOffset = Math.max(0, itemOffset - scrollViewCenterX + itemCenterX);
daysScrollRef.current?.scrollTo({ x: centerOffset, animated }); daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
}; };
// 初始化时滚动到选中位置
useEffect(() => { useEffect(() => {
if (scrollWidth > 0) { if (scrollWidth > 0) {
// 延迟滚动以确保ScrollView已经完全渲染
setTimeout(() => {
scrollToIndex(selectedIndex, false); scrollToIndex(selectedIndex, false);
}, 100);
} }
}, [scrollWidth, selectedIndex]); }, [scrollWidth]);
// 选中日期变化时滚动
useEffect(() => {
if (scrollWidth > 0) {
scrollToIndex(selectedIndex, true);
}
}, [selectedIndex]);
// 加载记录数据 // 加载记录数据
const loadRecords = async (isRefresh = false, loadMore = false) => { const loadRecords = async (isRefresh = false, loadMore = false) => {
@@ -194,7 +210,6 @@ export default function NutritionRecordsScreen() {
onPress={() => { onPress={() => {
if (!isDisabled) { if (!isDisabled) {
setSelectedIndex(index); setSelectedIndex(index);
scrollToIndex(index);
} }
}} }}
disabled={isDisabled} disabled={isDisabled}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -128,9 +128,6 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
> >
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={styles.titleRow}> <View style={styles.titleRow}>
<View style={styles.iconSquare}>
<Ionicons name="fitness-outline" size={16} color="#192126" />
</View>
<Text style={[styles.cardTitle, compact && styles.compactTitle]}>BMI</Text> <Text style={[styles.cardTitle, compact && styles.compactTitle]}>BMI</Text>
</View> </View>
{!compact && ( {!compact && (
@@ -320,7 +317,7 @@ const styles = StyleSheet.create({
marginRight: 10, marginRight: 10,
}, },
cardTitle: { cardTitle: {
fontSize: 18, fontSize: 14,
fontWeight: '800', fontWeight: '800',
color: '#192126', color: '#192126',
}, },
@@ -380,7 +377,7 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
}, },
bmiValue: { bmiValue: {
fontSize: 32, fontSize: 20,
fontWeight: '800', fontWeight: '800',
marginRight: 12, marginRight: 12,
}, },
@@ -390,16 +387,16 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
}, },
categoryText: { categoryText: {
fontSize: 14, fontSize: 12,
fontWeight: '700', fontWeight: '700',
}, },
bmiDescription: { bmiDescription: {
fontSize: 14, fontSize: 12,
fontWeight: '600', fontWeight: '600',
marginBottom: 8, marginBottom: 8,
}, },
encouragementText: { encouragementText: {
fontSize: 13, fontSize: 10,
color: '#6B7280', color: '#6B7280',
fontWeight: '500', fontWeight: '500',
lineHeight: 18, lineHeight: 18,

View File

@@ -0,0 +1,182 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { CircularRing } from './CircularRing';
type FitnessRingsCardProps = {
style?: any;
// 活动卡路里数据
activeCalories?: number;
activeCaloriesGoal?: number;
// 锻炼分钟数据
exerciseMinutes?: number;
exerciseMinutesGoal?: number;
// 站立小时数据
standHours?: number;
standHoursGoal?: number;
// 动画重置令牌
resetToken?: unknown;
};
/**
* 健身圆环卡片组件,模仿 Apple Watch 的健身圆环
*/
export function FitnessRingsCard({
style,
activeCalories = 25,
activeCaloriesGoal = 350,
exerciseMinutes = 1,
exerciseMinutesGoal = 5,
standHours = 2,
standHoursGoal = 13,
resetToken,
}: FitnessRingsCardProps) {
// 计算进度百分比
const caloriesProgress = Math.min(1, Math.max(0, activeCalories / activeCaloriesGoal));
const exerciseProgress = Math.min(1, Math.max(0, exerciseMinutes / exerciseMinutesGoal));
const standProgress = Math.min(1, Math.max(0, standHours / standHoursGoal));
return (
<View style={[styles.container, style]}>
<View style={styles.contentContainer}>
{/* 左侧圆环 */}
<View style={styles.ringsContainer}>
<View style={styles.ringWrapper}>
{/* 外圈 - 活动卡路里 (红色) */}
<View style={[styles.ringPosition]}>
<CircularRing
size={36}
strokeWidth={2.5}
trackColor="rgba(255, 59, 48, 0.15)"
progressColor="#FF3B30"
progress={caloriesProgress}
showCenterText={false}
resetToken={resetToken}
startAngleDeg={-90}
/>
</View>
{/* 中圈 - 锻炼分钟 (橙色) */}
<View style={[styles.ringPosition]}>
<CircularRing
size={26}
strokeWidth={2}
trackColor="rgba(255, 149, 0, 0.15)"
progressColor="#FF9500"
progress={exerciseProgress}
showCenterText={false}
resetToken={resetToken}
startAngleDeg={-90}
/>
</View>
{/* 内圈 - 站立小时 (蓝色) */}
<View style={[styles.ringPosition]}>
<CircularRing
size={16}
strokeWidth={1.5}
trackColor="rgba(0, 122, 255, 0.15)"
progressColor="#007AFF"
progress={standProgress}
showCenterText={false}
resetToken={resetToken}
startAngleDeg={-90}
/>
</View>
</View>
</View>
{/* 右侧数据显示 */}
<View style={styles.dataContainer}>
<View style={styles.dataRow}>
<Text style={styles.dataText}>
<Text style={styles.dataValue}>{activeCalories}</Text>
<Text style={styles.dataGoal}>/{activeCaloriesGoal}</Text>
</Text>
<Text style={styles.dataUnit}></Text>
</View>
<View style={styles.dataRow}>
<Text style={styles.dataText}>
<Text style={styles.dataValue}>{exerciseMinutes}</Text>
<Text style={styles.dataGoal}>/{exerciseMinutesGoal}</Text>
</Text>
<Text style={styles.dataUnit}></Text>
</View>
<View style={styles.dataRow}>
<Text style={styles.dataText}>
<Text style={styles.dataValue}>{standHours}</Text>
<Text style={styles.dataGoal}>/{standHoursGoal}</Text>
</Text>
<Text style={styles.dataUnit}></Text>
</View>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 12,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
},
contentContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
ringsContainer: {
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
ringWrapper: {
position: 'relative',
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'center',
},
ringPosition: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
dataContainer: {
flex: 1,
gap: 3,
},
dataRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
dataText: {
fontSize: 12,
fontWeight: '700',
flex: 1,
},
dataValue: {
color: '#192126',
},
dataGoal: {
color: '#9AA3AE',
},
dataUnit: {
fontSize: 10,
color: '#9AA3AE',
fontWeight: '500',
minWidth: 25,
textAlign: 'right',
},
});

View File

@@ -96,7 +96,7 @@ export function NutritionRecordCard({
</View> </View>
<View style={styles.timelineNode}> <View style={styles.timelineNode}>
<View style={[styles.timelineDot, { backgroundColor: mealTypeColor }]}> <View style={[styles.timelineDot, { backgroundColor: mealTypeColor }]}>
<Ionicons name={mealTypeIcon as any} size={12} color="#FFFFFF" /> <Ionicons name={mealTypeIcon as any} size={10} color="#FFFFFF" />
</View> </View>
{!isLast && ( {!isLast && (
<View style={[styles.timelineLine, { backgroundColor: textSecondaryColor }]} /> <View style={[styles.timelineLine, { backgroundColor: textSecondaryColor }]} />
@@ -197,15 +197,15 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
timelineColumn: { timelineColumn: {
width: 64, width: 52,
alignItems: 'center', alignItems: 'center',
paddingTop: 8, paddingTop: 6,
}, },
timeContainer: { timeContainer: {
marginBottom: 8, marginBottom: 6,
}, },
timeText: { timeText: {
fontSize: 12, fontSize: 11,
fontWeight: '600', fontWeight: '600',
textAlign: 'center', textAlign: 'center',
}, },
@@ -214,22 +214,22 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
timelineDot: { timelineDot: {
width: 24, width: 20,
height: 24, height: 20,
borderRadius: 12, borderRadius: 10,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1, shadowOpacity: 0.08,
shadowRadius: 4, shadowRadius: 3,
elevation: 2, elevation: 1,
}, },
timelineLine: { timelineLine: {
width: 2, width: 1.5,
flex: 1, flex: 1,
marginTop: 8, marginTop: 6,
opacity: 0.3, opacity: 0.25,
}, },
card: { card: {
flex: 1, flex: 1,
@@ -242,7 +242,7 @@ const styles = StyleSheet.create({
elevation: 2, elevation: 2,
}, },
cardWithTimeline: { cardWithTimeline: {
marginLeft: 8, marginLeft: 6,
}, },
mainContent: { mainContent: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -47,7 +47,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
// 计算进度条位置0-100% // 计算进度条位置0-100%
// 压力指数越高,进度条越满 // 压力指数越高,进度条越满
const progressPercentage = value === null ? 0 : value; const progressPercentage = value !== null ? Math.max(0, Math.min(100, value)) : 0;
// 在组件内部添加状态 // 在组件内部添加状态
const [showStressModal, setShowStressModal] = useState(false); const [showStressModal, setShowStressModal] = useState(false);
@@ -68,7 +68,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
<View style={styles.header}> <View style={styles.header}>
<View style={styles.leftSection}> <View style={styles.leftSection}>
<View style={styles.iconContainer}> <View style={styles.iconContainer}>
<Ionicons name="heart" size={16} color="#3B82F6" /> <Ionicons name="heart" size={16} color="red" />
</View> </View>
<Text style={styles.title}></Text> <Text style={styles.title}></Text>
</View> </View>
@@ -85,7 +85,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
<View style={styles.progressContainer}> <View style={styles.progressContainer}>
<View style={styles.progressTrack}> <View style={styles.progressTrack}>
{/* 渐变背景进度条 */} {/* 渐变背景进度条 */}
<View style={[styles.progressBar, { width: `${progressPercentage}%` }]}> <View style={[styles.progressBar, { width: '100%' }]}>
<LinearGradient <LinearGradient
colors={['#10B981', '#FCD34D', '#F97316']} colors={['#10B981', '#FCD34D', '#F97316']}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}

View File

@@ -8,6 +8,7 @@ import dayjs from 'dayjs';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Dimensions, Dimensions,
Image,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
@@ -170,7 +171,7 @@ export function WeightHistoryCard() {
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={styles.iconSquare}> <View style={styles.iconSquare}>
<Ionicons name="scale-outline" size={18} color="#192126" /> <Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
</View> </View>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
</View> </View>
@@ -187,7 +188,7 @@ export function WeightHistoryCard() {
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={styles.iconSquare}> <View style={styles.iconSquare}>
<Ionicons name="scale-outline" size={18} color="#192126" /> <Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
</View> </View>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
</View> </View>
@@ -220,7 +221,7 @@ export function WeightHistoryCard() {
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={styles.iconSquare}> <View style={styles.iconSquare}>
<Ionicons name="scale-outline" size={18} color="#192126" /> <Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
</View> </View>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
</View> </View>
@@ -271,7 +272,7 @@ export function WeightHistoryCard() {
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={styles.iconSquare}> <View style={styles.iconSquare}>
<Ionicons name="scale-outline" size={18} color="#192126" /> <Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
</View> </View>
<Text style={styles.cardTitle}></Text> <Text style={styles.cardTitle}></Text>
<View style={styles.headerButtons}> <View style={styles.headerButtons}>
@@ -444,10 +445,10 @@ const styles = StyleSheet.create({
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 10, marginRight: 2,
}, },
cardTitle: { cardTitle: {
fontSize: 18, fontSize: 14,
fontWeight: '800', fontWeight: '800',
color: '#192126', color: '#192126',
flex: 1, flex: 1,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
import { MaterialIcons } from '@expo/vector-icons';
import React from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
interface CustomCheckBoxProps {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
size?: number;
checkedColor?: string;
uncheckedColor?: string;
}
const CustomCheckBox = (props: CustomCheckBoxProps) => {
const {
checked,
onCheckedChange,
size = 16,
checkedColor = '#E91E63',
uncheckedColor = '#999'
} = props;
return (
<TouchableOpacity
style={[styles.container, { width: size + 4, height: size + 4 }]}
onPress={() => onCheckedChange(!checked)}
activeOpacity={0.7}
>
{checked ? (
<MaterialIcons
name="check-box"
size={size}
color={checkedColor}
/>
) : (
<MaterialIcons
name="check-box-outline-blank"
size={size}
color={uncheckedColor}
/>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
});
export default CustomCheckBox;

View File

@@ -0,0 +1,114 @@
import { getDeviceDimensions } from '@/utils/native.utils';
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { ratio } = getDeviceDimensions();
interface SuccessToastProps {
visible: boolean;
message: string;
duration?: number;
backgroundColor?: string;
textColor?: string;
icon?: string;
onHide?: () => void;
}
export default function SuccessToast({
visible,
message,
duration = 2000,
backgroundColor = '#DF42D0', // 默认使用应用主题色
textColor = '#FFFFFF',
icon = '✓',
onHide,
}: SuccessToastProps) {
const animValue = useRef(new Animated.Value(0)).current;
const insets = useSafeAreaInsets();
useEffect(() => {
if (visible) {
// 入场动画
Animated.sequence([
Animated.timing(animValue, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
// 停留时间
Animated.delay(duration - 600), // 减去入场和退场动画时间
// 退场动画
Animated.timing(animValue, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
]).start(() => {
onHide?.();
});
}
}, [visible, duration, animValue, onHide]);
if (!visible) return null;
const translateY = animValue.interpolate({
inputRange: [0, 1],
outputRange: [-(insets.top + 60), 0], // 从安全区域上方滑入
});
const opacity = animValue;
return (
<Animated.View
style={[
styles.container,
{
top: insets.top + 10 * ratio, // 动态计算顶部安全距离
transform: [{ translateY }],
opacity,
}
]}
>
<View style={[styles.content, { backgroundColor }]}>
<Text style={[styles.icon, { color: textColor }]}>{icon}</Text>
<Text style={[styles.text, { color: textColor }]}>{message}</Text>
</View>
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
// top 将由组件内部动态计算
left: 15 * ratio,
right: 15 * ratio,
zIndex: 1000,
alignItems: 'center',
},
content: {
paddingVertical: 12 * ratio,
paddingHorizontal: 20 * ratio,
borderRadius: 25 * ratio,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 10,
},
icon: {
fontSize: 16 * ratio,
fontWeight: 'bold',
marginRight: 8 * ratio,
},
text: {
fontSize: 14 * ratio,
fontWeight: '600',
},
});

107
contexts/ToastContext.tsx Normal file
View File

@@ -0,0 +1,107 @@
import SuccessToast from '@/components/ui/SuccessToast';
import { setToastRef } from '@/utils/toast.utils';
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
interface ToastConfig {
message: string;
duration?: number;
backgroundColor?: string;
textColor?: string;
icon?: string;
}
export interface ToastContextType {
showToast: (config: ToastConfig) => void;
showSuccess: (message: string, duration?: number) => void;
showError: (message: string, duration?: number) => void;
showWarning: (message: string, duration?: number) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [visible, setVisible] = useState(false);
const [config, setConfig] = useState<ToastConfig>({ message: '' });
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const showToast = (toastConfig: ToastConfig) => {
// 如果已有Toast显示先隐藏
if (visible) {
setVisible(false);
// 短暂延迟后显示新Toast
setTimeout(() => {
setConfig(toastConfig);
setVisible(true);
}, 100);
} else {
setConfig(toastConfig);
setVisible(true);
}
};
const showSuccess = (message: string, duration?: number) => {
showToast({
message,
duration,
backgroundColor: '#DF42D0', // 主题色
icon: '✓',
});
};
const showError = (message: string, duration?: number) => {
showToast({
message,
duration,
backgroundColor: '#f44336', // 红色
icon: '✕',
});
};
const showWarning = (message: string, duration?: number) => {
showToast({
message,
duration,
backgroundColor: '#ff9800', // 橙色
icon: '⚠',
});
};
const handleHide = () => {
setVisible(false);
};
const value: ToastContextType = {
showToast,
showSuccess,
showError,
showWarning,
};
// 设置全局引用
useEffect(() => {
setToastRef(value);
}, [value]);
return (
<ToastContext.Provider value={value}>
{children}
<SuccessToast
visible={visible}
message={config.message}
duration={config.duration}
backgroundColor={config.backgroundColor}
textColor={config.textColor}
icon={config.icon}
onHide={handleHide}
/>
</ToastContext.Provider>
);
}
export function useToast(): ToastContextType {
const context = useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}

View File

@@ -113,6 +113,8 @@ PODS:
- libwebp/sharpyuv (1.5.0) - libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0): - libwebp/webp (1.5.0):
- libwebp/sharpyuv - libwebp/sharpyuv
- PurchasesHybridCommon (16.2.2):
- RevenueCat (= 5.34.0)
- QCloudCore (6.5.1): - QCloudCore (6.5.1):
- QCloudCore/Default (= 6.5.1) - QCloudCore/Default (= 6.5.1)
- QCloudCore/Default (6.5.1): - QCloudCore/Default (6.5.1):
@@ -1701,6 +1703,7 @@ PODS:
- React-logger (= 0.79.5) - React-logger (= 0.79.5)
- React-perflogger (= 0.79.5) - React-perflogger (= 0.79.5)
- React-utils (= 0.79.5) - React-utils (= 0.79.5)
- RevenueCat (5.34.0)
- RNAppleHealthKit (1.7.0): - RNAppleHealthKit (1.7.0):
- React - React
- RNCAsyncStorage (2.2.0): - RNCAsyncStorage (2.2.0):
@@ -1730,6 +1733,8 @@ PODS:
- Yoga - Yoga
- RNDateTimePicker (8.4.4): - RNDateTimePicker (8.4.4):
- React-Core - React-Core
- RNDeviceInfo (14.0.4):
- React-Core
- RNExitApp (2.0.0): - RNExitApp (2.0.0):
- React-Core - React-Core
- RNGestureHandler (2.24.0): - RNGestureHandler (2.24.0):
@@ -1755,6 +1760,9 @@ PODS:
- ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- Yoga - Yoga
- RNPurchases (9.2.2):
- PurchasesHybridCommon (= 16.2.2)
- React-Core
- RNReanimated (3.17.5): - RNReanimated (3.17.5):
- DoubleConversion - DoubleConversion
- glog - glog
@@ -1898,6 +1906,30 @@ PODS:
- ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- Yoga - Yoga
- RNSentry (6.20.0):
- DoubleConversion
- glog
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsc
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Sentry/HybridSDK (= 8.53.2)
- Yoga
- RNSVG (15.12.1): - RNSVG (15.12.1):
- React-Core - React-Core
- SDWebImage (5.21.1): - SDWebImage (5.21.1):
@@ -1911,6 +1943,7 @@ PODS:
- SDWebImageWebPCoder (0.14.6): - SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.53.2)
- SocketRocket (0.7.1) - SocketRocket (0.7.1)
- Yoga (0.0.0) - Yoga (0.0.0)
@@ -2012,10 +2045,13 @@ DEPENDENCIES:
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- RNExitApp (from `../node_modules/react-native-exit-app`) - RNExitApp (from `../node_modules/react-native-exit-app`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNPurchases (from `../node_modules/react-native-purchases`)
- RNReanimated (from `../node_modules/react-native-reanimated`) - RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`) - RNScreens (from `../node_modules/react-native-screens`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
- RNSVG (from `../node_modules/react-native-svg`) - RNSVG (from `../node_modules/react-native-svg`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -2024,13 +2060,16 @@ SPEC REPOS:
- libavif - libavif
- libdav1d - libdav1d
- libwebp - libwebp
- PurchasesHybridCommon
- QCloudCore - QCloudCore
- QCloudCOSXML - QCloudCOSXML
- QCloudTrack - QCloudTrack
- RevenueCat
- SDWebImage - SDWebImage
- SDWebImageAVIFCoder - SDWebImageAVIFCoder
- SDWebImageSVGCoder - SDWebImageSVGCoder
- SDWebImageWebPCoder - SDWebImageWebPCoder
- Sentry
- SocketRocket - SocketRocket
EXTERNAL SOURCES: EXTERNAL SOURCES:
@@ -2224,14 +2263,20 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view" :path: "../node_modules/@react-native-masked-view/masked-view"
RNDateTimePicker: RNDateTimePicker:
:path: "../node_modules/@react-native-community/datetimepicker" :path: "../node_modules/@react-native-community/datetimepicker"
RNDeviceInfo:
:path: "../node_modules/react-native-device-info"
RNExitApp: RNExitApp:
:path: "../node_modules/react-native-exit-app" :path: "../node_modules/react-native-exit-app"
RNGestureHandler: RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler" :path: "../node_modules/react-native-gesture-handler"
RNPurchases:
:path: "../node_modules/react-native-purchases"
RNReanimated: RNReanimated:
:path: "../node_modules/react-native-reanimated" :path: "../node_modules/react-native-reanimated"
RNScreens: RNScreens:
:path: "../node_modules/react-native-screens" :path: "../node_modules/react-native-screens"
RNSentry:
:path: "../node_modules/@sentry/react-native"
RNSVG: RNSVG:
:path: "../node_modules/react-native-svg" :path: "../node_modules/react-native-svg"
Yoga: Yoga:
@@ -2267,6 +2312,7 @@ SPEC CHECKSUMS:
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798 QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8 QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
@@ -2335,19 +2381,24 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: f3e842e6cb5a825b6918a74a38402ba1409411f8 ReactAppDependencyProvider: f3e842e6cb5a825b6918a74a38402ba1409411f8
ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5 ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9 RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96 RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4 RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
RNGestureHandler: 6e640921d207f070e4bbcf79f4e6d0eabf323389 RNGestureHandler: 6e640921d207f070e4bbcf79f4e6d0eabf323389
RNPurchases: 7993b33416e67d5863140b5c62c682b34719f475
RNReanimated: 34e90d19560aebd52a2ad583fdc2de2cf7651bbb RNReanimated: 34e90d19560aebd52a2ad583fdc2de2cf7651bbb
RNScreens: 241cfe8fc82737f3e132dd45779f9512928075b8 RNScreens: 241cfe8fc82737f3e132dd45779f9512928075b8
RNSentry: 7fbd30d392b5ac268cdebe085bfd7830c735a4d6
RNSVG: 3544def7b3ddc43c7ba69dade91bacf99f10ec46 RNSVG: 3544def7b3ddc43c7ba69dade91bacf99f10ec46
SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: adb397651e1c00672c12e9495babca70777e411e Yoga: adb397651e1c00672c12e9495babca70777e411e

View File

@@ -268,13 +268,17 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/QCloudCOSXML/QCloudCOSXML.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/QCloudCOSXML/QCloudCOSXML.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
); );
@@ -284,13 +288,17 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QCloudCOSXML.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QCloudCOSXML.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
); );

406
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@sentry/react-native": "^6.20.0",
"cos-js-sdk-v5": "^1.6.0", "cos-js-sdk-v5": "^1.6.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"expo": "~53.0.20", "expo": "~53.0.20",
@@ -38,12 +39,14 @@
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-cos-sdk": "^1.2.1", "react-native-cos-sdk": "^1.2.1",
"react-native-device-info": "^14.0.4",
"react-native-exit-app": "^2.0.0", "react-native-exit-app": "^2.0.0",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-health": "^1.19.0", "react-native-health": "^1.19.0",
"react-native-image-viewing": "^0.2.2", "react-native-image-viewing": "^0.2.2",
"react-native-markdown-display": "^7.0.2", "react-native-markdown-display": "^7.0.2",
"react-native-modal-datetime-picker": "^18.0.0", "react-native-modal-datetime-picker": "^18.0.0",
"react-native-purchases": "^9.2.2",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-render-html": "^6.3.4", "react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
@@ -3307,6 +3310,27 @@
} }
} }
}, },
"node_modules/@revenuecat/purchases-js": {
"version": "1.11.1",
"resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js/-/purchases-js-1.11.1.tgz",
"integrity": "sha512-P0jxwUBWOIFSZQ1/NIMpbOXG3brraNDGYoCnES1r5w97yonhAw1brpKwhFKUhlq+DvAUDCG1q1d8FdTzI+MgXg==",
"license": "MIT"
},
"node_modules/@revenuecat/purchases-js-hybrid-mappings": {
"version": "16.2.1",
"resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js-hybrid-mappings/-/purchases-js-hybrid-mappings-16.2.1.tgz",
"integrity": "sha512-TXYw6lh5rg/kGI44kayU4TGSXKDcc35TdB0vBuZfllSokY1tnyYmP8Pm2eZamLN8ycrTuCysoPxknW2Klh1H1g==",
"license": "MIT",
"dependencies": {
"@revenuecat/purchases-js": "1.11.1"
}
},
"node_modules/@revenuecat/purchases-typescript-internal": {
"version": "16.2.1",
"resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-typescript-internal/-/purchases-typescript-internal-16.2.1.tgz",
"integrity": "sha512-g7FhNA6nxr9686klimlfueMQqQl34pHUHXeCKXqeuaPJOOsFc7qcOGhGZdyLGulIAgpkctrvcAbeDyBk7t5QRg==",
"license": "MIT"
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -3314,6 +3338,349 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sentry-internal/browser-utils": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz",
"integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry-internal/feedback/-/feedback-8.55.0.tgz",
"integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry-internal/replay/-/replay-8.55.0.tgz",
"integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.55.0",
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz",
"integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "8.55.0",
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/babel-plugin-component-annotate": {
"version": "4.1.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.1.1.tgz",
"integrity": "sha512-HUpqrCK7zDVojTV6KL6BO9ZZiYrEYQqvYQrscyMsq04z+WCupXaH6YEliiNRvreR8DBJgdsG3lBRpebhUGmvfA==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/@sentry/browser": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry/browser/-/browser-8.55.0.tgz",
"integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "8.55.0",
"@sentry-internal/feedback": "8.55.0",
"@sentry-internal/replay": "8.55.0",
"@sentry-internal/replay-canvas": "8.55.0",
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/cli": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli/-/cli-2.51.1.tgz",
"integrity": "sha512-FU+54kNcKJABU0+ekvtnoXHM9zVrDe1zXVFbQT7mS0On0m1P0zFRGdzbnWe2XzpzuEAJXtK6aog/W+esRU9AIA==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"https-proxy-agent": "^5.0.0",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
},
"bin": {
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.51.1",
"@sentry/cli-linux-arm": "2.51.1",
"@sentry/cli-linux-arm64": "2.51.1",
"@sentry/cli-linux-i686": "2.51.1",
"@sentry/cli-linux-x64": "2.51.1",
"@sentry/cli-win32-arm64": "2.51.1",
"@sentry/cli-win32-i686": "2.51.1",
"@sentry/cli-win32-x64": "2.51.1"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-darwin/-/cli-darwin-2.51.1.tgz",
"integrity": "sha512-R1u8IQdn/7Rr8sf6bVVr0vJT4OqwCFdYsS44Y3OoWGVJW2aAQTWRJOTlV4ueclVLAyUQzmgBjfR8AtiUhd/M5w==",
"license": "BSD-3-Clause",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-arm/-/cli-linux-arm-2.51.1.tgz",
"integrity": "sha512-Klro17OmSSKOOSaxVKBBNPXet2+HrIDZUTSp8NRl4LQsIubdc1S/aQ79cH/g52Muwzpl3aFwPxyXw+46isfEgA==",
"cpu": [
"arm"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.51.1.tgz",
"integrity": "sha512-nvA/hdhsw4bKLhslgbBqqvETjXwN1FVmwHLOrRvRcejDO6zeIKUElDiL5UOjGG0NC+62AxyNw5ri8Wzp/7rg9Q==",
"cpu": [
"arm64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-i686/-/cli-linux-i686-2.51.1.tgz",
"integrity": "sha512-jp4TmR8VXBdT9dLo6mHniQHN0xKnmJoPGVz9h9VDvO2Vp/8o96rBc555D4Am5wJOXmfuPlyjGcmwHlB3+kQRWw==",
"cpu": [
"x86",
"ia32"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-x64/-/cli-linux-x64-2.51.1.tgz",
"integrity": "sha512-JuLt0MXM2KHNFmjqXjv23sly56mJmUQzGBWktkpY3r+jE08f5NLKPd5wQ6W/SoLXGIOKnwLz0WoUg7aBVyQdeQ==",
"cpu": [
"x64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"linux",
"freebsd",
"android"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-arm64": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.51.1.tgz",
"integrity": "sha512-PiwjTdIFDazTQCTyDCutiSkt4omggYSKnO3HE1+LDjElsFrWY9pJs4fU3D40WAyE2oKu0MarjNH/WxYGdqEAlg==",
"cpu": [
"arm64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-i686/-/cli-win32-i686-2.51.1.tgz",
"integrity": "sha512-TMvZZpeiI2HmrDFNVQ0uOiTuYKvjEGOZdmUxe3WlhZW82A/2Oka7sQ24ljcOovbmBOj5+fjCHRUMYvLMCWiysA==",
"cpu": [
"x86",
"ia32"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.51.1",
"resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-x64/-/cli-win32-x64-2.51.1.tgz",
"integrity": "sha512-v2hreYUPPTNK1/N7+DeX7XBN/zb7p539k+2Osf0HFyVBaoUC3Y3+KBwSf4ASsnmgTAK7HCGR+X0NH1vP+icw4w==",
"cpu": [
"x64"
],
"license": "BSD-3-Clause",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://mirrors.tencent.com/npm/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@sentry/cli/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://mirrors.tencent.com/npm/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@sentry/core": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry/core/-/core-8.55.0.tgz",
"integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/react": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry/react/-/react-8.55.0.tgz",
"integrity": "sha512-/qNBvFLpvSa/Rmia0jpKfJdy16d4YZaAnH/TuKLAtm0BWlsPQzbXCU4h8C5Hsst0Do0zG613MEtEmWpWrVOqWA==",
"license": "MIT",
"dependencies": {
"@sentry/browser": "8.55.0",
"@sentry/core": "8.55.0",
"hoist-non-react-statics": "^3.3.2"
},
"engines": {
"node": ">=14.18"
},
"peerDependencies": {
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@sentry/react-native": {
"version": "6.20.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry/react-native/-/react-native-6.20.0.tgz",
"integrity": "sha512-YngSba14Hsb5t/ZNMOyxb/HInmYRL5pQ74BkoMBQ/UBBM5kWHgSILxoO2XkKYtaaJXrkSJj+kBalELHblz9h5g==",
"license": "MIT",
"dependencies": {
"@sentry/babel-plugin-component-annotate": "4.1.1",
"@sentry/browser": "8.55.0",
"@sentry/cli": "2.51.1",
"@sentry/core": "8.55.0",
"@sentry/react": "8.55.0",
"@sentry/types": "8.55.0",
"@sentry/utils": "8.55.0"
},
"bin": {
"sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js"
},
"peerDependencies": {
"expo": ">=49.0.0",
"react": ">=17.0.0",
"react-native": ">=0.65.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"node_modules/@sentry/types": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry/types/-/types-8.55.0.tgz",
"integrity": "sha512-6LRT0+r6NWQ+RtllrUW2yQfodST0cJnkOmdpHA75vONgBUhpKwiJ4H7AmgfoTET8w29pU6AnntaGOe0LJbOmog==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sentry/utils": {
"version": "8.55.0",
"resolved": "https://mirrors.tencent.com/npm/@sentry/utils/-/utils-8.55.0.tgz",
"integrity": "sha512-cYcl39+xcOivBpN9d8ZKbALl+DxZKo/8H0nueJZ0PO4JA+MJGhSm6oHakXxLPaiMoNLTX7yor8ndnQIuFg+vmQ==",
"license": "MIT",
"dependencies": {
"@sentry/core": "8.55.0"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -10618,6 +10985,12 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://mirrors.tencent.com/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -10873,6 +11246,15 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-device-info": {
"version": "14.0.4",
"resolved": "https://mirrors.tencent.com/npm/react-native-device-info/-/react-native-device-info-14.0.4.tgz",
"integrity": "sha512-NX0wMAknSDBeFnEnSFQ8kkAcQrFHrG4Cl0mVjoD+0++iaKrOupiGpBXqs8xR0SeJyPC5zpdPl4h/SaBGly6UxA==",
"license": "MIT",
"peerDependencies": {
"react-native": "*"
}
},
"node_modules/react-native-edge-to-edge": { "node_modules/react-native-edge-to-edge": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz",
@@ -11132,6 +11514,30 @@
"react-native": ">=0.65.0" "react-native": ">=0.65.0"
} }
}, },
"node_modules/react-native-purchases": {
"version": "9.2.2",
"resolved": "https://mirrors.tencent.com/npm/react-native-purchases/-/react-native-purchases-9.2.2.tgz",
"integrity": "sha512-j376mva8G6SLA2HPTROpUGoivfLMZVWPM7mj2bcgTS8y6NzbyQJ20Npe8V3nWc0N5YFTuknTF8pl0tWc6FqYbA==",
"license": "MIT",
"workspaces": [
"examples/purchaseTesterTypescript",
"react-native-purchases-ui"
],
"dependencies": {
"@revenuecat/purchases-js-hybrid-mappings": "16.2.1",
"@revenuecat/purchases-typescript-internal": "16.2.1"
},
"peerDependencies": {
"react": ">= 16.6.3",
"react-native": ">= 0.73.0",
"react-native-web": "*"
},
"peerDependenciesMeta": {
"react-native-web": {
"optional": true
}
}
},
"node_modules/react-native-reanimated": { "node_modules/react-native-reanimated": {
"version": "3.17.5", "version": "3.17.5",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",

View File

@@ -19,6 +19,7 @@
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@sentry/react-native": "^6.20.0",
"cos-js-sdk-v5": "^1.6.0", "cos-js-sdk-v5": "^1.6.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"expo": "~53.0.20", "expo": "~53.0.20",
@@ -41,12 +42,14 @@
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-cos-sdk": "^1.2.1", "react-native-cos-sdk": "^1.2.1",
"react-native-device-info": "^14.0.4",
"react-native-exit-app": "^2.0.0", "react-native-exit-app": "^2.0.0",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-health": "^1.19.0", "react-native-health": "^1.19.0",
"react-native-image-viewing": "^0.2.2", "react-native-image-viewing": "^0.2.2",
"react-native-markdown-display": "^7.0.2", "react-native-markdown-display": "^7.0.2",
"react-native-modal-datetime-picker": "^18.0.0", "react-native-modal-datetime-picker": "^18.0.0",
"react-native-purchases": "^9.2.2",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-render-html": "^6.3.4", "react-native-render-html": "^6.3.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",

View File

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