- 新增基础代谢详情页面,包含图表展示、数据缓存和防抖机制 - 优化HRV数据获取逻辑,支持实时、近期和历史数据的智能获取 - 移除WaterIntakeCard和WaterSettings中的登录验证逻辑 - 更新饮水数据管理hook,直接使用HealthKit数据 - 添加饮水目标存储和获取功能 - 更新依赖包版本
820 lines
23 KiB
TypeScript
820 lines
23 KiB
TypeScript
import { DateSelector } from '@/components/DateSelector';
|
||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { useAppSelector } from '@/hooks/redux';
|
||
import { selectUserAge, selectUserProfile } from '@/store/userSlice';
|
||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||
import { fetchBasalEnergyBurned } from '@/utils/health';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import dayjs from 'dayjs';
|
||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import { ActivityIndicator, Dimensions, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
import { BarChart } from 'react-native-chart-kit';
|
||
|
||
dayjs.extend(weekOfYear);
|
||
|
||
type TabType = 'week' | 'month';
|
||
|
||
type BasalMetabolismData = {
|
||
date: Date;
|
||
value: number | null;
|
||
};
|
||
|
||
export default function BasalMetabolismDetailScreen() {
|
||
const userProfile = useAppSelector(selectUserProfile);
|
||
const userAge = useAppSelector(selectUserAge);
|
||
|
||
// 日期相关状态
|
||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||
const [activeTab, setActiveTab] = useState<TabType>('week');
|
||
|
||
// 说明弹窗状态
|
||
const [infoModalVisible, setInfoModalVisible] = useState(false);
|
||
|
||
// 数据状态
|
||
const [chartData, setChartData] = useState<BasalMetabolismData[]>([]);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// 缓存和防抖相关,参照BasalMetabolismCard
|
||
const [cacheRef] = useState(() => new Map<string, { data: BasalMetabolismData[]; timestamp: number }>());
|
||
const [loadingRef] = useState(() => new Map<string, Promise<BasalMetabolismData[]>>());
|
||
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
|
||
|
||
console.log('basal metabolism chartData', chartData);
|
||
|
||
// 生成日期范围的函数
|
||
const generateDateRange = useCallback((tab: TabType): Date[] => {
|
||
const today = new Date();
|
||
const dates: Date[] = [];
|
||
|
||
switch (tab) {
|
||
case 'week':
|
||
// 获取最近7天
|
||
for (let i = 6; i >= 0; i--) {
|
||
const date = dayjs(today).subtract(i, 'day').toDate();
|
||
dates.push(date);
|
||
}
|
||
break;
|
||
case 'month':
|
||
// 获取最近30天,按周分组
|
||
for (let i = 3; i >= 0; i--) {
|
||
const date = dayjs(today).subtract(i * 7, 'day').toDate();
|
||
dates.push(date);
|
||
}
|
||
break;
|
||
}
|
||
|
||
return dates;
|
||
}, []);
|
||
|
||
// 优化的数据获取函数,包含缓存和去重复请求
|
||
const fetchBasalMetabolismData = useCallback(async (tab: TabType): Promise<BasalMetabolismData[]> => {
|
||
const cacheKey = `${tab}-${dayjs().format('YYYY-MM-DD')}`;
|
||
const now = Date.now();
|
||
|
||
// 检查缓存
|
||
const cached = cacheRef.get(cacheKey);
|
||
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
||
return cached.data;
|
||
}
|
||
|
||
// 检查是否已经在请求中(防止重复请求)
|
||
const existingRequest = loadingRef.get(cacheKey);
|
||
if (existingRequest) {
|
||
return existingRequest;
|
||
}
|
||
|
||
// 创建新的请求
|
||
const request = (async () => {
|
||
try {
|
||
const dates = generateDateRange(tab);
|
||
const results: BasalMetabolismData[] = [];
|
||
|
||
// 并行获取所有日期的数据
|
||
const promises = dates.map(async (date) => {
|
||
try {
|
||
const options = {
|
||
startDate: dayjs(date).startOf('day').toDate().toISOString(),
|
||
endDate: dayjs(date).endOf('day').toDate().toISOString()
|
||
};
|
||
const basalEnergy = await fetchBasalEnergyBurned(options);
|
||
return {
|
||
date,
|
||
value: basalEnergy || null
|
||
};
|
||
} catch (error) {
|
||
console.error('获取单日基础代谢数据失败:', error);
|
||
return {
|
||
date,
|
||
value: null
|
||
};
|
||
}
|
||
});
|
||
|
||
const data = await Promise.all(promises);
|
||
results.push(...data);
|
||
|
||
// 更新缓存
|
||
cacheRef.set(cacheKey, { data: results, timestamp: now });
|
||
|
||
return results;
|
||
} catch (error) {
|
||
console.error('获取基础代谢数据失败:', error);
|
||
return [];
|
||
} finally {
|
||
// 清理请求记录
|
||
loadingRef.delete(cacheKey);
|
||
}
|
||
})();
|
||
|
||
// 记录请求
|
||
loadingRef.set(cacheKey, request);
|
||
|
||
return request;
|
||
}, [generateDateRange, cacheRef, loadingRef, CACHE_DURATION]);
|
||
|
||
// 获取当前选中日期
|
||
const currentSelectedDate = useMemo(() => {
|
||
const days = getMonthDaysZh();
|
||
return days[selectedIndex]?.date?.toDate() ?? new Date();
|
||
}, [selectedIndex]);
|
||
|
||
|
||
// 计算BMR范围
|
||
const bmrRange = useMemo(() => {
|
||
const { gender, weight, height } = userProfile;
|
||
|
||
// 检查是否有足够的信息来计算BMR
|
||
if (!gender || !weight || !height || !userAge) {
|
||
return null;
|
||
}
|
||
|
||
// 将体重和身高转换为数字
|
||
const weightNum = parseFloat(weight);
|
||
const heightNum = parseFloat(height);
|
||
|
||
if (isNaN(weightNum) || isNaN(heightNum) || weightNum <= 0 || heightNum <= 0 || userAge <= 0) {
|
||
return null;
|
||
}
|
||
|
||
// 使用Mifflin-St Jeor公式计算BMR
|
||
let bmr: number;
|
||
if (gender === 'male') {
|
||
bmr = 10 * weightNum + 6.25 * heightNum - 5 * userAge + 5;
|
||
} else {
|
||
bmr = 10 * weightNum + 6.25 * heightNum - 5 * userAge - 161;
|
||
}
|
||
|
||
// 计算正常范围(±15%)
|
||
const minBMR = Math.round(bmr * 0.85);
|
||
const maxBMR = Math.round(bmr * 1.15);
|
||
|
||
return { min: minBMR, max: maxBMR, base: Math.round(bmr) };
|
||
}, [userProfile.gender, userProfile.weight, userProfile.height, userAge]);
|
||
|
||
// 获取单个日期的代谢数据
|
||
const fetchSingleDateData = useCallback(async (date: Date): Promise<BasalMetabolismData> => {
|
||
try {
|
||
const options = {
|
||
startDate: dayjs(date).startOf('day').toDate().toISOString(),
|
||
endDate: dayjs(date).endOf('day').toDate().toISOString()
|
||
};
|
||
const basalEnergy = await fetchBasalEnergyBurned(options);
|
||
return {
|
||
date,
|
||
value: basalEnergy || null
|
||
};
|
||
} catch (error) {
|
||
console.error('获取单日基础代谢数据失败:', error);
|
||
return {
|
||
date,
|
||
value: null
|
||
};
|
||
}
|
||
}, []);
|
||
|
||
// 日期选择回调
|
||
const onSelectDate = useCallback(async (index: number) => {
|
||
setSelectedIndex(index);
|
||
|
||
// 获取选中日期
|
||
const days = getMonthDaysZh();
|
||
const selectedDate = days[index]?.date?.toDate();
|
||
|
||
if (selectedDate) {
|
||
// 检查是否已经有该日期的数据
|
||
const existingData = chartData.find(item =>
|
||
dayjs(item.date).isSame(selectedDate, 'day')
|
||
);
|
||
|
||
// 如果没有数据,则获取该日期的数据
|
||
if (!existingData) {
|
||
try {
|
||
const newData = await fetchSingleDateData(selectedDate);
|
||
// 更新chartData,添加新数据并按日期排序
|
||
setChartData(prevData => {
|
||
const updatedData = [...prevData, newData];
|
||
return updatedData.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||
});
|
||
} catch (error) {
|
||
console.error('获取选中日期数据失败:', error);
|
||
}
|
||
}
|
||
}
|
||
}, [chartData, fetchSingleDateData]);
|
||
|
||
// Tab切换
|
||
const handleTabPress = useCallback((tab: TabType) => {
|
||
setActiveTab(tab);
|
||
}, []);
|
||
|
||
// 初始化和Tab切换时加载数据
|
||
useEffect(() => {
|
||
let isCancelled = false;
|
||
|
||
const loadData = async () => {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
try {
|
||
const data = await fetchBasalMetabolismData(activeTab);
|
||
if (!isCancelled) {
|
||
setChartData(data);
|
||
}
|
||
} catch (err) {
|
||
if (!isCancelled) {
|
||
setError(err instanceof Error ? err.message : '获取数据失败');
|
||
}
|
||
} finally {
|
||
if (!isCancelled) {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
loadData();
|
||
|
||
// 清理函数,防止组件卸载后的状态更新
|
||
return () => {
|
||
isCancelled = true;
|
||
};
|
||
}, [activeTab, fetchBasalMetabolismData]);
|
||
|
||
// 处理图表数据
|
||
const processedChartData = useMemo(() => {
|
||
if (!chartData || chartData.length === 0) {
|
||
return { labels: [], datasets: [] };
|
||
}
|
||
|
||
// 根据activeTab生成标签和数据
|
||
const labels = chartData.map(item => {
|
||
switch (activeTab) {
|
||
case 'week':
|
||
// 显示星期几
|
||
return dayjs(item.date).format('dd');
|
||
case 'month':
|
||
// 显示周数
|
||
const weekOfYear = dayjs(item.date).week();
|
||
const firstWeekOfYear = dayjs(item.date).startOf('year').week();
|
||
return `第${weekOfYear - firstWeekOfYear + 1}周`;
|
||
default:
|
||
return dayjs(item.date).format('MM-DD');
|
||
}
|
||
});
|
||
|
||
// 生成基础代谢数据集
|
||
const data = chartData.map(item => {
|
||
const value = item.value;
|
||
if (value === null || value === undefined) {
|
||
return 0; // 明确处理null/undefined值
|
||
}
|
||
// 对于非常小的正值,保证至少显示1,但对于0值保持为0
|
||
const roundedValue = Math.round(value);
|
||
return value > 0 && roundedValue === 0 ? 1 : roundedValue;
|
||
});
|
||
|
||
console.log('processedChartData:', { labels, data, originalValues: chartData.map(item => item.value) });
|
||
|
||
return {
|
||
labels,
|
||
datasets: [{
|
||
data
|
||
}]
|
||
};
|
||
}, [chartData, activeTab]);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
{/* 背景渐变 */}
|
||
<LinearGradient
|
||
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
/>
|
||
|
||
{/* 头部导航 */}
|
||
<HeaderBar
|
||
title="基础代谢"
|
||
transparent
|
||
right={
|
||
<TouchableOpacity
|
||
onPress={() => setInfoModalVisible(true)}
|
||
style={styles.infoButton}
|
||
>
|
||
<Ionicons name="information-circle-outline" size={24} color="#666" />
|
||
</TouchableOpacity>
|
||
}
|
||
/>
|
||
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={{
|
||
paddingBottom: 60,
|
||
paddingHorizontal: 20
|
||
}}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* 日期选择器 */}
|
||
<View style={styles.dateContainer}>
|
||
<DateSelector
|
||
selectedIndex={selectedIndex}
|
||
onDateSelect={onSelectDate}
|
||
showMonthTitle={false}
|
||
disableFutureDates={true}
|
||
/>
|
||
</View>
|
||
|
||
{/* 当前日期基础代谢显示 */}
|
||
<View style={styles.currentDataCard}>
|
||
<Text style={styles.currentDataTitle}>
|
||
{dayjs(currentSelectedDate).format('M月D日')} 基础代谢
|
||
</Text>
|
||
<View style={styles.currentValueContainer}>
|
||
<Text style={styles.currentValue}>
|
||
{(() => {
|
||
const selectedDateData = chartData.find(item =>
|
||
dayjs(item.date).isSame(currentSelectedDate, 'day')
|
||
);
|
||
if (selectedDateData?.value) {
|
||
return Math.round(selectedDateData.value).toString();
|
||
}
|
||
return '--';
|
||
})()}
|
||
</Text>
|
||
<Text style={styles.currentUnit}>千卡</Text>
|
||
</View>
|
||
{bmrRange && (
|
||
<Text style={styles.rangeText}>
|
||
正常范围: {bmrRange.min}-{bmrRange.max} 千卡
|
||
</Text>
|
||
)}
|
||
</View>
|
||
|
||
{/* 基础代谢统计 */}
|
||
<View style={styles.statsCard}>
|
||
<Text style={styles.statsTitle}>基础代谢统计</Text>
|
||
|
||
{/* Tab 切换 */}
|
||
<View style={styles.tabContainer}>
|
||
<TouchableOpacity
|
||
style={[styles.tab, activeTab === 'week' && styles.activeTab]}
|
||
onPress={() => handleTabPress('week')}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[styles.tabText, activeTab === 'week' && styles.activeTabText]}>
|
||
按周
|
||
</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[styles.tab, activeTab === 'month' && styles.activeTab]}
|
||
onPress={() => handleTabPress('month')}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[styles.tabText, activeTab === 'month' && styles.activeTabText]}>
|
||
按月
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* 柱状图 */}
|
||
{isLoading ? (
|
||
<View style={styles.loadingChart}>
|
||
<ActivityIndicator size="large" color="#4ECDC4" />
|
||
<Text style={styles.loadingText}>加载中...</Text>
|
||
</View>
|
||
) : error ? (
|
||
<View style={styles.errorChart}>
|
||
<Text style={styles.errorText}>加载失败: {error}</Text>
|
||
<TouchableOpacity
|
||
style={styles.retryButton}
|
||
onPress={() => {
|
||
// 重新加载数据
|
||
setIsLoading(true);
|
||
setError(null);
|
||
fetchBasalMetabolismData(activeTab).then(data => {
|
||
setChartData(data);
|
||
setIsLoading(false);
|
||
}).catch(err => {
|
||
setError(err instanceof Error ? err.message : '获取数据失败');
|
||
setIsLoading(false);
|
||
});
|
||
}}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={styles.retryText}>重试</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
) : processedChartData.datasets.length > 0 && processedChartData.datasets[0].data.length > 0 ? (
|
||
<BarChart
|
||
data={{
|
||
labels: processedChartData.labels,
|
||
datasets: processedChartData.datasets,
|
||
}}
|
||
width={Dimensions.get('window').width - 80}
|
||
height={220}
|
||
yAxisLabel=""
|
||
yAxisSuffix="千卡"
|
||
chartConfig={{
|
||
backgroundColor: '#ffffff',
|
||
backgroundGradientFrom: '#ffffff',
|
||
backgroundGradientTo: '#ffffff',
|
||
decimalPlaces: 0,
|
||
color: (opacity = 1) => `${Colors.light.primary}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`, // 使用主题紫色
|
||
labelColor: (opacity = 1) => `${Colors.light.text}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`, // 使用主题文字颜色
|
||
style: {
|
||
borderRadius: 16,
|
||
},
|
||
barPercentage: 0.7, // 增加柱体宽度
|
||
propsForBackgroundLines: {
|
||
strokeDasharray: "2,2",
|
||
stroke: Colors.light.border, // 使用主题边框颜色
|
||
strokeWidth: 1
|
||
},
|
||
propsForLabels: {
|
||
fontSize: 12,
|
||
fontWeight: '500',
|
||
},
|
||
}}
|
||
style={styles.chart}
|
||
showValuesOnTopOfBars={true}
|
||
fromZero={false}
|
||
segments={4}
|
||
/>
|
||
) : (
|
||
<View style={styles.emptyChart}>
|
||
<Text style={styles.emptyChartText}>暂无数据</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* 基础代谢说明弹窗 */}
|
||
<Modal
|
||
animationType="fade"
|
||
transparent={true}
|
||
visible={infoModalVisible}
|
||
onRequestClose={() => setInfoModalVisible(false)}
|
||
>
|
||
<View style={styles.modalOverlay}>
|
||
<View style={styles.modalContent}>
|
||
{/* 关闭按钮 */}
|
||
<TouchableOpacity
|
||
style={styles.closeButton}
|
||
onPress={() => setInfoModalVisible(false)}
|
||
>
|
||
<Text style={styles.closeButtonText}>×</Text>
|
||
</TouchableOpacity>
|
||
|
||
{/* 标题 */}
|
||
<Text style={styles.modalTitle}>基础代谢</Text>
|
||
|
||
{/* 基础代谢定义 */}
|
||
<Text style={styles.modalDescription}>
|
||
基础代谢,也称基础代谢率(BMR),是指人体在完全静息状态下维持基本生命功能(心跳、呼吸、体温调节等)所需的最低能量消耗,通常以卡路里为单位。
|
||
</Text>
|
||
|
||
{/* 为什么重要 */}
|
||
<Text style={styles.sectionTitle}>为什么重要?</Text>
|
||
<Text style={styles.sectionContent}>
|
||
基础代谢占总能量消耗的60-75%,是能量平衡的基础。了解您的基础代谢有助于制定科学的营养计划、优化体重管理策略,以及评估代谢健康状态。
|
||
</Text>
|
||
|
||
{/* 正常范围 */}
|
||
<Text style={styles.sectionTitle}>正常范围</Text>
|
||
<Text style={styles.formulaText}>
|
||
- 男性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 + 5
|
||
</Text>
|
||
<Text style={styles.formulaText}>
|
||
- 女性:BMR = 10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄 - 161
|
||
</Text>
|
||
|
||
{bmrRange ? (
|
||
<>
|
||
<Text style={styles.rangeText}>您的正常区间:{bmrRange.min}-{bmrRange.max}千卡/天</Text>
|
||
<Text style={styles.rangeNote}>
|
||
(在公式基础计算值上下浮动15%都属于正常范围)
|
||
</Text>
|
||
<Text style={styles.userInfoText}>
|
||
基于您的信息:{userProfile.gender === 'male' ? '男性' : '女性'},{userAge}岁,{userProfile.height}cm,{userProfile.weight}kg
|
||
</Text>
|
||
</>
|
||
) : (
|
||
<Text style={styles.rangeText}>请完善基本信息以计算您的代谢率</Text>
|
||
)}
|
||
|
||
{/* 提高代谢率的策略 */}
|
||
<Text style={styles.sectionTitle}>提高代谢率的策略</Text>
|
||
<Text style={styles.strategyText}>科学研究支持以下方法:</Text>
|
||
|
||
<View style={styles.strategyList}>
|
||
<Text style={styles.strategyItem}>1.增加肌肉量 (每周2-3次力量训练)</Text>
|
||
<Text style={styles.strategyItem}>2.高强度间歇训练 (HIIT)</Text>
|
||
<Text style={styles.strategyItem}>3.充分蛋白质摄入 (体重每公斤1.6-2.2g)</Text>
|
||
<Text style={styles.strategyItem}>4.保证充足睡眠 (7-9小时/晚)</Text>
|
||
<Text style={styles.strategyItem}>5.避免过度热量限制 (不低于BMR的80%)</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#FFFFFF',
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
},
|
||
infoButton: {
|
||
padding: 4,
|
||
},
|
||
dateContainer: {
|
||
marginTop: 16,
|
||
marginBottom: 20,
|
||
},
|
||
currentDataCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 20,
|
||
marginBottom: 20,
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 4,
|
||
},
|
||
shadowOpacity: 0.12,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
alignItems: 'center',
|
||
},
|
||
currentDataTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 16,
|
||
textAlign: 'center',
|
||
},
|
||
currentValueContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'baseline',
|
||
marginBottom: 8,
|
||
},
|
||
currentValue: {
|
||
fontSize: 36,
|
||
fontWeight: '700',
|
||
color: '#4ECDC4',
|
||
},
|
||
currentUnit: {
|
||
fontSize: 16,
|
||
color: '#666',
|
||
marginLeft: 8,
|
||
},
|
||
rangeText: {
|
||
fontSize: 14,
|
||
color: '#059669',
|
||
textAlign: 'center',
|
||
},
|
||
statsCard: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 16,
|
||
padding: 20,
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 4,
|
||
},
|
||
shadowOpacity: 0.12,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
statsTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 16,
|
||
},
|
||
tabContainer: {
|
||
flexDirection: 'row',
|
||
backgroundColor: '#F5F5F7',
|
||
borderRadius: 12,
|
||
padding: 4,
|
||
marginBottom: 20,
|
||
},
|
||
tab: {
|
||
flex: 1,
|
||
paddingVertical: 8,
|
||
paddingHorizontal: 16,
|
||
borderRadius: 8,
|
||
alignItems: 'center',
|
||
},
|
||
activeTab: {
|
||
backgroundColor: '#FFFFFF',
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: 1,
|
||
},
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 2,
|
||
elevation: 2,
|
||
},
|
||
tabText: {
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#888',
|
||
},
|
||
activeTabText: {
|
||
color: '#192126',
|
||
},
|
||
chart: {
|
||
marginVertical: 8,
|
||
borderRadius: 16,
|
||
},
|
||
emptyChart: {
|
||
height: 220,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
backgroundColor: '#F8F9FA',
|
||
borderRadius: 16,
|
||
marginVertical: 8,
|
||
},
|
||
emptyChartText: {
|
||
fontSize: 14,
|
||
color: '#999',
|
||
fontWeight: '500',
|
||
},
|
||
loadingChart: {
|
||
height: 220,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
backgroundColor: '#F8F9FA',
|
||
borderRadius: 16,
|
||
marginVertical: 8,
|
||
},
|
||
loadingText: {
|
||
fontSize: 14,
|
||
color: '#666',
|
||
marginTop: 8,
|
||
fontWeight: '500',
|
||
},
|
||
errorChart: {
|
||
height: 220,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
backgroundColor: '#FFF5F5',
|
||
borderRadius: 16,
|
||
marginVertical: 8,
|
||
padding: 20,
|
||
},
|
||
errorText: {
|
||
fontSize: 14,
|
||
color: '#E53E3E',
|
||
textAlign: 'center',
|
||
marginBottom: 12,
|
||
},
|
||
retryButton: {
|
||
backgroundColor: '#4ECDC4',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 8,
|
||
borderRadius: 8,
|
||
},
|
||
retryText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
},
|
||
// Modal styles
|
||
modalOverlay: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||
justifyContent: 'flex-end',
|
||
},
|
||
modalContent: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderTopLeftRadius: 20,
|
||
borderTopRightRadius: 20,
|
||
padding: 24,
|
||
maxHeight: '90%',
|
||
width: '100%',
|
||
shadowColor: '#000',
|
||
shadowOffset: {
|
||
width: 0,
|
||
height: -5,
|
||
},
|
||
shadowOpacity: 0.25,
|
||
shadowRadius: 20,
|
||
elevation: 10,
|
||
},
|
||
closeButton: {
|
||
position: 'absolute',
|
||
top: 16,
|
||
right: 16,
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 16,
|
||
backgroundColor: '#F1F5F9',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1,
|
||
},
|
||
closeButtonText: {
|
||
fontSize: 20,
|
||
color: '#64748B',
|
||
fontWeight: '600',
|
||
},
|
||
modalTitle: {
|
||
fontSize: 24,
|
||
fontWeight: '700',
|
||
color: '#0F172A',
|
||
marginBottom: 16,
|
||
textAlign: 'center',
|
||
},
|
||
modalDescription: {
|
||
fontSize: 15,
|
||
color: '#475569',
|
||
lineHeight: 22,
|
||
marginBottom: 24,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: '#0F172A',
|
||
marginBottom: 12,
|
||
marginTop: 8,
|
||
},
|
||
sectionContent: {
|
||
fontSize: 15,
|
||
color: '#475569',
|
||
lineHeight: 22,
|
||
marginBottom: 20,
|
||
},
|
||
formulaText: {
|
||
fontSize: 14,
|
||
color: '#64748B',
|
||
fontFamily: 'monospace',
|
||
marginBottom: 4,
|
||
paddingLeft: 8,
|
||
},
|
||
rangeNote: {
|
||
fontSize: 12,
|
||
color: '#9CA3AF',
|
||
textAlign: 'center',
|
||
marginBottom: 20,
|
||
},
|
||
userInfoText: {
|
||
fontSize: 13,
|
||
color: '#6B7280',
|
||
textAlign: 'center',
|
||
marginTop: 8,
|
||
marginBottom: 16,
|
||
fontStyle: 'italic',
|
||
},
|
||
strategyText: {
|
||
fontSize: 15,
|
||
color: '#475569',
|
||
marginBottom: 12,
|
||
},
|
||
strategyList: {
|
||
marginBottom: 20,
|
||
},
|
||
strategyItem: {
|
||
fontSize: 14,
|
||
color: '#64748B',
|
||
lineHeight: 20,
|
||
marginBottom: 8,
|
||
paddingLeft: 8,
|
||
},
|
||
}); |