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:
@@ -1,6 +1,6 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { BMICard } from '@/components/BMICard';
|
||||
import { CircularRing } from '@/components/CircularRing';
|
||||
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
||||
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
||||
import { ProgressBar } from '@/components/ProgressBar';
|
||||
import { StressMeter } from '@/components/StressMeter';
|
||||
@@ -52,13 +52,21 @@ export default function ExploreScreen() {
|
||||
// 日期条自动滚动到选中项
|
||||
const daysScrollRef = useRef<import('react-native').ScrollView | null>(null);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const DAY_PILL_WIDTH = 68;
|
||||
const DAY_PILL_SPACING = 12;
|
||||
const DAY_PILL_WIDTH = 48;
|
||||
const DAY_PILL_SPACING = 8;
|
||||
|
||||
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));
|
||||
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(() => {
|
||||
@@ -68,6 +76,14 @@ export default function ExploreScreen() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [scrollWidth]);
|
||||
|
||||
// 当选中索引变化时,滚动到对应位置
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedIndex]);
|
||||
|
||||
// HealthKit: 每次页面聚焦都拉取今日数据
|
||||
const [stepCount, setStepCount] = useState<number | null>(null);
|
||||
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
||||
@@ -76,6 +92,15 @@ export default function ExploreScreen() {
|
||||
// HRV数据
|
||||
const [hrvValue, setHrvValue] = useState<number>(0);
|
||||
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);
|
||||
// 用于触发动画重置的 token(当日期或数据变化时更新)
|
||||
@@ -124,6 +149,15 @@ export default function ExploreScreen() {
|
||||
setStepCount(data.steps);
|
||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||
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;
|
||||
setHrvValue(hrv);
|
||||
@@ -195,7 +229,6 @@ export default function ExploreScreen() {
|
||||
// 日期点击时,加载对应日期数据
|
||||
const onSelectDate = (index: number) => {
|
||||
setSelectedIndex(index);
|
||||
scrollToIndex(index);
|
||||
const target = days[index]?.date?.toDate();
|
||||
if (target) {
|
||||
loadHealthData(target);
|
||||
@@ -320,19 +353,17 @@ export default function ExploreScreen() {
|
||||
// compact={true}
|
||||
/>
|
||||
|
||||
<View style={[styles.masonryCard, styles.trainingCard]}>
|
||||
<Text style={styles.cardTitleSecondary}>训练时间</Text>
|
||||
<View style={styles.trainingContent}>
|
||||
<CircularRing
|
||||
size={120}
|
||||
strokeWidth={12}
|
||||
trackColor="#E2D9FD"
|
||||
progressColor="#8B74F3"
|
||||
progress={trainingProgress}
|
||||
resetToken={animToken}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FitnessRingsCard
|
||||
activeCalories={fitnessRingsData.activeCalories}
|
||||
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
|
||||
exerciseMinutes={fitnessRingsData.exerciseMinutes}
|
||||
exerciseMinutesGoal={fitnessRingsData.exerciseMinutesGoal}
|
||||
standHours={fitnessRingsData.standHours}
|
||||
standHoursGoal={fitnessRingsData.standHoursGoal}
|
||||
resetToken={animToken}
|
||||
style={styles.masonryCard}
|
||||
/>
|
||||
|
||||
|
||||
<View style={[styles.masonryCard, styles.sleepCard]}>
|
||||
<View style={styles.cardHeaderRow}>
|
||||
@@ -396,13 +427,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
dayItemWrapper: {
|
||||
alignItems: 'center',
|
||||
width: 68,
|
||||
marginRight: 12,
|
||||
width: 48,
|
||||
marginRight: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
borderRadius: 18,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -413,16 +444,16 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: lightColors.datePickerSelected,
|
||||
},
|
||||
dayLabel: {
|
||||
fontSize: 16,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
marginBottom: 2,
|
||||
marginBottom: 1,
|
||||
},
|
||||
dayLabelSelected: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
dayDate: {
|
||||
fontSize: 16,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
@@ -430,12 +461,12 @@ const styles = StyleSheet.create({
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
selectedDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: 2.5,
|
||||
backgroundColor: lightColors.datePickerSelected,
|
||||
marginTop: 10,
|
||||
marginBottom: 4,
|
||||
marginTop: 6,
|
||||
marginBottom: 2,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
sectionTitle: {
|
||||
@@ -481,13 +512,13 @@ const styles = StyleSheet.create({
|
||||
|
||||
cardTitleSecondary: {
|
||||
color: '#9AA3AE',
|
||||
fontSize: 14,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
marginBottom: 10,
|
||||
},
|
||||
caloriesValue: {
|
||||
color: '#192126',
|
||||
fontSize: 22,
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
trainingContent: {
|
||||
@@ -569,8 +600,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 12,
|
||||
},
|
||||
iconSquare: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#FFFFFF',
|
||||
alignItems: 'center',
|
||||
@@ -578,7 +609,7 @@ const styles = StyleSheet.create({
|
||||
marginRight: 10,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
@@ -606,7 +637,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#FFE4B8',
|
||||
},
|
||||
stepsValue: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
color: '#7A6A42',
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
|
||||
@@ -12,9 +12,9 @@ import { store } from '@/store';
|
||||
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||
import React from 'react';
|
||||
import RNExitApp from 'react-native-exit-app';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||||
import { ToastProvider } from '@/contexts/ToastContext';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
@@ -58,7 +58,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||
onAgree={handlePrivacyAgree}
|
||||
onDisagree={handlePrivacyDisagree}
|
||||
/>
|
||||
<Toast />
|
||||
</DialogProvider>
|
||||
);
|
||||
}
|
||||
@@ -77,27 +76,28 @@ export default function RootLayout() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Bootstrapper>
|
||||
<ThemeProvider value={DefaultTheme}>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="onboarding" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="challenge" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile/edit" />
|
||||
<Stack.Screen name="profile/goals" options={{ headerShown: false }} />
|
||||
<ToastProvider>
|
||||
<ThemeProvider value={DefaultTheme}>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="onboarding" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="challenge" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="profile/edit" />
|
||||
<Stack.Screen name="profile/goals" options={{ headerShown: false }} />
|
||||
|
||||
<Stack.Screen name="ai-posture-assessment" />
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="dark" />
|
||||
<Toast />
|
||||
</ThemeProvider>
|
||||
<Stack.Screen name="ai-posture-assessment" />
|
||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="dark" />
|
||||
</ThemeProvider>
|
||||
</ToastProvider>
|
||||
</Bootstrapper>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -43,21 +43,37 @@ export default function NutritionRecordsScreen() {
|
||||
// 日期滚动相关
|
||||
const daysScrollRef = useRef<ScrollView | null>(null);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const DAY_PILL_WIDTH = 68;
|
||||
const DAY_PILL_SPACING = 12;
|
||||
const DAY_PILL_WIDTH = 60; // 48px width + 12px marginRight = 60px total per item
|
||||
const DAY_PILL_SPACING = 0; // spacing is included in the width above
|
||||
|
||||
// 日期滚动控制
|
||||
const scrollToIndex = (index: number, animated = true) => {
|
||||
const baseOffset = index * (DAY_PILL_WIDTH + DAY_PILL_SPACING);
|
||||
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
|
||||
if (scrollWidth <= 0) return;
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
// 初始化时滚动到选中位置
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, false);
|
||||
// 延迟滚动以确保ScrollView已经完全渲染
|
||||
setTimeout(() => {
|
||||
scrollToIndex(selectedIndex, false);
|
||||
}, 100);
|
||||
}
|
||||
}, [scrollWidth, selectedIndex]);
|
||||
}, [scrollWidth]);
|
||||
|
||||
// 选中日期变化时滚动
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, true);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 加载记录数据
|
||||
const loadRecords = async (isRefresh = false, loadMore = false) => {
|
||||
@@ -194,7 +210,6 @@ export default function NutritionRecordsScreen() {
|
||||
onPress={() => {
|
||||
if (!isDisabled) {
|
||||
setSelectedIndex(index);
|
||||
scrollToIndex(index);
|
||||
}
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
|
||||
BIN
assets/images/icons/iconWeight.png
Normal file
BIN
assets/images/icons/iconWeight.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -128,9 +128,6 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<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>
|
||||
</View>
|
||||
{!compact && (
|
||||
@@ -219,7 +216,7 @@ export function BMICard({ weight, height, style, compact = false }: BMICardProps
|
||||
|
||||
{/* BMI 分类标准 */}
|
||||
<Text style={styles.newSectionTitle}>BMI 分类标准</Text>
|
||||
|
||||
|
||||
<View style={styles.newStatsCard}>
|
||||
{BMI_CATEGORIES.map((category, index) => {
|
||||
const colors = [
|
||||
@@ -320,7 +317,7 @@ const styles = StyleSheet.create({
|
||||
marginRight: 10,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
@@ -380,7 +377,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
bmiValue: {
|
||||
fontSize: 32,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
marginRight: 12,
|
||||
},
|
||||
@@ -390,16 +387,16 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
bmiDescription: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
},
|
||||
encouragementText: {
|
||||
fontSize: 13,
|
||||
fontSize: 10,
|
||||
color: '#6B7280',
|
||||
fontWeight: '500',
|
||||
lineHeight: 18,
|
||||
|
||||
182
components/FitnessRingsCard.tsx
Normal file
182
components/FitnessRingsCard.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
@@ -96,7 +96,7 @@ export function NutritionRecordCard({
|
||||
</View>
|
||||
<View style={styles.timelineNode}>
|
||||
<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>
|
||||
{!isLast && (
|
||||
<View style={[styles.timelineLine, { backgroundColor: textSecondaryColor }]} />
|
||||
@@ -197,15 +197,15 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 12,
|
||||
},
|
||||
timelineColumn: {
|
||||
width: 64,
|
||||
width: 52,
|
||||
alignItems: 'center',
|
||||
paddingTop: 8,
|
||||
paddingTop: 6,
|
||||
},
|
||||
timeContainer: {
|
||||
marginBottom: 8,
|
||||
marginBottom: 6,
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
@@ -214,22 +214,22 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
timelineDot: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 3,
|
||||
elevation: 1,
|
||||
},
|
||||
timelineLine: {
|
||||
width: 2,
|
||||
width: 1.5,
|
||||
flex: 1,
|
||||
marginTop: 8,
|
||||
opacity: 0.3,
|
||||
marginTop: 6,
|
||||
opacity: 0.25,
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
@@ -242,7 +242,7 @@ const styles = StyleSheet.create({
|
||||
elevation: 2,
|
||||
},
|
||||
cardWithTimeline: {
|
||||
marginLeft: 8,
|
||||
marginLeft: 6,
|
||||
},
|
||||
mainContent: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -47,7 +47,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
||||
|
||||
// 计算进度条位置(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);
|
||||
@@ -68,7 +68,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
||||
<View style={styles.header}>
|
||||
<View style={styles.leftSection}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="heart" size={16} color="#3B82F6" />
|
||||
<Ionicons name="heart" size={16} color="red" />
|
||||
</View>
|
||||
<Text style={styles.title}>压力</Text>
|
||||
</View>
|
||||
@@ -85,7 +85,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressTrack}>
|
||||
{/* 渐变背景进度条 */}
|
||||
<View style={[styles.progressBar, { width: `${progressPercentage}%` }]}>
|
||||
<View style={[styles.progressBar, { width: '100%' }]}>
|
||||
<LinearGradient
|
||||
colors={['#10B981', '#FCD34D', '#F97316']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
@@ -229,4 +229,4 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'right',
|
||||
marginTop: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import dayjs from 'dayjs';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -170,7 +171,7 @@ export function WeightHistoryCard() {
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<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>
|
||||
<Text style={styles.cardTitle}>体重记录</Text>
|
||||
</View>
|
||||
@@ -187,7 +188,7 @@ export function WeightHistoryCard() {
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<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>
|
||||
<Text style={styles.cardTitle}>体重记录</Text>
|
||||
</View>
|
||||
@@ -220,7 +221,7 @@ export function WeightHistoryCard() {
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<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>
|
||||
<Text style={styles.cardTitle}>体重记录</Text>
|
||||
</View>
|
||||
@@ -271,7 +272,7 @@ export function WeightHistoryCard() {
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<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>
|
||||
<Text style={styles.cardTitle}>体重记录</Text>
|
||||
<View style={styles.headerButtons}>
|
||||
@@ -444,10 +445,10 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 10,
|
||||
marginRight: 2,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
flex: 1,
|
||||
|
||||
1176
components/model/MembershipModal.tsx
Normal file
1176
components/model/MembershipModal.tsx
Normal file
File diff suppressed because it is too large
Load Diff
53
components/ui/CheckBox.tsx
Normal file
53
components/ui/CheckBox.tsx
Normal 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;
|
||||
114
components/ui/SuccessToast.tsx
Normal file
114
components/ui/SuccessToast.tsx
Normal 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
107
contexts/ToastContext.tsx
Normal 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;
|
||||
}
|
||||
@@ -113,6 +113,8 @@ PODS:
|
||||
- libwebp/sharpyuv (1.5.0)
|
||||
- libwebp/webp (1.5.0):
|
||||
- libwebp/sharpyuv
|
||||
- PurchasesHybridCommon (16.2.2):
|
||||
- RevenueCat (= 5.34.0)
|
||||
- QCloudCore (6.5.1):
|
||||
- QCloudCore/Default (= 6.5.1)
|
||||
- QCloudCore/Default (6.5.1):
|
||||
@@ -1701,6 +1703,7 @@ PODS:
|
||||
- React-logger (= 0.79.5)
|
||||
- React-perflogger (= 0.79.5)
|
||||
- React-utils (= 0.79.5)
|
||||
- RevenueCat (5.34.0)
|
||||
- RNAppleHealthKit (1.7.0):
|
||||
- React
|
||||
- RNCAsyncStorage (2.2.0):
|
||||
@@ -1730,6 +1733,8 @@ PODS:
|
||||
- Yoga
|
||||
- RNDateTimePicker (8.4.4):
|
||||
- React-Core
|
||||
- RNDeviceInfo (14.0.4):
|
||||
- React-Core
|
||||
- RNExitApp (2.0.0):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.24.0):
|
||||
@@ -1755,6 +1760,9 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNPurchases (9.2.2):
|
||||
- PurchasesHybridCommon (= 16.2.2)
|
||||
- React-Core
|
||||
- RNReanimated (3.17.5):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
@@ -1898,6 +1906,30 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- 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):
|
||||
- React-Core
|
||||
- SDWebImage (5.21.1):
|
||||
@@ -1911,6 +1943,7 @@ PODS:
|
||||
- SDWebImageWebPCoder (0.14.6):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- Sentry/HybridSDK (8.53.2)
|
||||
- SocketRocket (0.7.1)
|
||||
- Yoga (0.0.0)
|
||||
|
||||
@@ -2012,10 +2045,13 @@ DEPENDENCIES:
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
- RNExitApp (from `../node_modules/react-native-exit-app`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNPurchases (from `../node_modules/react-native-purchases`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
@@ -2024,13 +2060,16 @@ SPEC REPOS:
|
||||
- libavif
|
||||
- libdav1d
|
||||
- libwebp
|
||||
- PurchasesHybridCommon
|
||||
- QCloudCore
|
||||
- QCloudCOSXML
|
||||
- QCloudTrack
|
||||
- RevenueCat
|
||||
- SDWebImage
|
||||
- SDWebImageAVIFCoder
|
||||
- SDWebImageSVGCoder
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
- SocketRocket
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -2224,14 +2263,20 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@react-native-masked-view/masked-view"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNDeviceInfo:
|
||||
:path: "../node_modules/react-native-device-info"
|
||||
RNExitApp:
|
||||
:path: "../node_modules/react-native-exit-app"
|
||||
RNGestureHandler:
|
||||
:path: "../node_modules/react-native-gesture-handler"
|
||||
RNPurchases:
|
||||
:path: "../node_modules/react-native-purchases"
|
||||
RNReanimated:
|
||||
:path: "../node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
:path: "../node_modules/react-native-screens"
|
||||
RNSentry:
|
||||
:path: "../node_modules/@sentry/react-native"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
Yoga:
|
||||
@@ -2267,6 +2312,7 @@ SPEC CHECKSUMS:
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d
|
||||
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
||||
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||
@@ -2335,19 +2381,24 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: f3e842e6cb5a825b6918a74a38402ba1409411f8
|
||||
ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe
|
||||
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
|
||||
RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada
|
||||
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
||||
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
||||
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
||||
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
|
||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
||||
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
|
||||
RNGestureHandler: 6e640921d207f070e4bbcf79f4e6d0eabf323389
|
||||
RNPurchases: 7993b33416e67d5863140b5c62c682b34719f475
|
||||
RNReanimated: 34e90d19560aebd52a2ad583fdc2de2cf7651bbb
|
||||
RNScreens: 241cfe8fc82737f3e132dd45779f9512928075b8
|
||||
RNSentry: 7fbd30d392b5ac268cdebe085bfd7830c735a4d6
|
||||
RNSVG: 3544def7b3ddc43c7ba69dade91bacf99f10ec46
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||
|
||||
|
||||
@@ -268,13 +268,17 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_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}/RCT-Folly/RCT-Folly_privacy.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}/React-Core/React-Core_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}/Sentry/Sentry.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_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}/ExpoFileSystem_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}/RCT-Folly_privacy.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}/React-Core_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}/Sentry.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||
);
|
||||
|
||||
406
package-lock.json
generated
406
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/react-native": "^6.20.0",
|
||||
"cos-js-sdk-v5": "^1.6.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"expo": "~53.0.20",
|
||||
@@ -38,12 +39,14 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.5",
|
||||
"react-native-cos-sdk": "^1.2.1",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-exit-app": "^2.0.0",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-health": "^1.19.0",
|
||||
"react-native-image-viewing": "^0.2.2",
|
||||
"react-native-markdown-display": "^7.0.2",
|
||||
"react-native-modal-datetime-picker": "^18.0.0",
|
||||
"react-native-purchases": "^9.2.2",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -3314,6 +3338,349 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@@ -10618,6 +10985,12 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -10873,6 +11246,15 @@
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/react-native": "^6.20.0",
|
||||
"cos-js-sdk-v5": "^1.6.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"expo": "~53.0.20",
|
||||
@@ -41,12 +42,14 @@
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.5",
|
||||
"react-native-cos-sdk": "^1.2.1",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-exit-app": "^2.0.0",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-health": "^1.19.0",
|
||||
"react-native-image-viewing": "^0.2.2",
|
||||
"react-native-markdown-display": "^7.0.2",
|
||||
"react-native-modal-datetime-picker": "^18.0.0",
|
||||
"react-native-purchases": "^9.2.2",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
|
||||
@@ -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
24
utils/native.utils.ts
Normal 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
113
utils/sentry.utils.ts
Normal 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
58
utils/toast.utils.ts
Normal 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');
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user