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 { 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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
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.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,
|
||||||
|
|||||||
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>
|
||||||
<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',
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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/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
|
||||||
|
|
||||||
|
|||||||
@@ -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
406
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
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