703 lines
20 KiB
TypeScript
703 lines
20 KiB
TypeScript
import { DateSelector } from '@/components/DateSelector';
|
||
import { FloatingSelectionModal, SelectionItem } from '@/components/ui/FloatingSelectionModal';
|
||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||
import { fetchCircumferenceAnalysis, selectCircumferenceData, selectCircumferenceError, selectCircumferenceLoading } from '@/store/circumferenceSlice';
|
||
import { selectUserProfile, updateUserBodyMeasurements, UserProfile } from '@/store/userSlice';
|
||
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
|
||
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, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
import { LineChart } from 'react-native-chart-kit';
|
||
|
||
dayjs.extend(weekOfYear);
|
||
|
||
// 围度类型数据
|
||
const CIRCUMFERENCE_TYPES = [
|
||
{ key: 'chestCircumference', label: '胸围', color: '#FF6B6B' },
|
||
{ key: 'waistCircumference', label: '腰围', color: '#4ECDC4' },
|
||
{ key: 'upperHipCircumference', label: '上臀围', color: '#45B7D1' },
|
||
{ key: 'armCircumference', label: '臂围', color: '#96CEB4' },
|
||
{ key: 'thighCircumference', label: '大腿围', color: '#FFEAA7' },
|
||
{ key: 'calfCircumference', label: '小腿围', color: '#DDA0DD' },
|
||
];
|
||
|
||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||
import { CircumferencePeriod } from '@/services/circumferenceAnalysis';
|
||
|
||
type TabType = CircumferencePeriod;
|
||
|
||
export default function CircumferenceDetailScreen() {
|
||
const safeAreaTop = useSafeAreaTop()
|
||
const dispatch = useAppDispatch();
|
||
const userProfile = useAppSelector(selectUserProfile);
|
||
const { ensureLoggedIn } = useAuthGuard();
|
||
|
||
// 日期相关状态
|
||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||
const [activeTab, setActiveTab] = useState<TabType>('week');
|
||
|
||
// 弹窗状态
|
||
const [modalVisible, setModalVisible] = useState(false);
|
||
const [selectedMeasurement, setSelectedMeasurement] = useState<{
|
||
key: string;
|
||
label: string;
|
||
currentValue?: number;
|
||
} | null>(null);
|
||
|
||
// Redux状态
|
||
const chartData = useAppSelector(state => selectCircumferenceData(state, activeTab));
|
||
const isLoading = useAppSelector(state => selectCircumferenceLoading(state, activeTab));
|
||
const error = useAppSelector(selectCircumferenceError);
|
||
|
||
console.log('chartData', chartData);
|
||
|
||
|
||
// 图例显示状态 - 控制哪些维度显示在图表中
|
||
const [visibleTypes, setVisibleTypes] = useState<Set<string>>(
|
||
new Set(CIRCUMFERENCE_TYPES.map(type => type.key))
|
||
);
|
||
|
||
// 获取当前选中日期
|
||
const currentSelectedDate = useMemo(() => {
|
||
const days = getMonthDaysZh();
|
||
return days[selectedIndex]?.date?.toDate() ?? new Date();
|
||
}, [selectedIndex]);
|
||
|
||
// 判断选中日期是否是今天
|
||
const isSelectedDateToday = useMemo(() => {
|
||
const today = new Date();
|
||
const selectedDate = currentSelectedDate;
|
||
return dayjs(selectedDate).isSame(today, 'day');
|
||
}, [currentSelectedDate]);
|
||
|
||
// 当前围度数据
|
||
const measurements = [
|
||
{
|
||
key: 'chestCircumference',
|
||
label: '胸围',
|
||
value: userProfile?.chestCircumference,
|
||
color: '#FF6B6B',
|
||
},
|
||
{
|
||
key: 'waistCircumference',
|
||
label: '腰围',
|
||
value: userProfile?.waistCircumference,
|
||
color: '#4ECDC4',
|
||
},
|
||
{
|
||
key: 'upperHipCircumference',
|
||
label: '上臀围',
|
||
value: userProfile?.upperHipCircumference,
|
||
color: '#45B7D1',
|
||
},
|
||
{
|
||
key: 'armCircumference',
|
||
label: '臂围',
|
||
value: userProfile?.armCircumference,
|
||
color: '#96CEB4',
|
||
},
|
||
{
|
||
key: 'thighCircumference',
|
||
label: '大腿围',
|
||
value: userProfile?.thighCircumference,
|
||
color: '#FFEAA7',
|
||
},
|
||
{
|
||
key: 'calfCircumference',
|
||
label: '小腿围',
|
||
value: userProfile?.calfCircumference,
|
||
color: '#DDA0DD',
|
||
},
|
||
];
|
||
|
||
// 日期选择回调
|
||
const onSelectDate = (index: number) => {
|
||
setSelectedIndex(index);
|
||
};
|
||
|
||
// Tab切换
|
||
const handleTabPress = useCallback((tab: TabType) => {
|
||
setActiveTab(tab);
|
||
// 切换tab时重新获取数据
|
||
dispatch(fetchCircumferenceAnalysis(tab));
|
||
}, [dispatch]);
|
||
|
||
// 初始化加载数据
|
||
useEffect(() => {
|
||
dispatch(fetchCircumferenceAnalysis(activeTab));
|
||
}, [dispatch, activeTab]);
|
||
|
||
// 处理图例点击,切换显示/隐藏
|
||
const handleLegendPress = (typeKey: string) => {
|
||
const newVisibleTypes = new Set(visibleTypes);
|
||
if (newVisibleTypes.has(typeKey)) {
|
||
// 至少保留一个维度显示
|
||
if (newVisibleTypes.size > 1) {
|
||
newVisibleTypes.delete(typeKey);
|
||
}
|
||
} else {
|
||
newVisibleTypes.add(typeKey);
|
||
}
|
||
setVisibleTypes(newVisibleTypes);
|
||
};
|
||
|
||
// 根据不同围度类型获取合理的默认值
|
||
const getDefaultCircumferenceValue = (measurementKey: string, userProfile?: UserProfile): number => {
|
||
// 如果用户已有该围度数据,直接使用
|
||
const existingValue = userProfile?.[measurementKey as keyof UserProfile] as number;
|
||
if (existingValue) {
|
||
return existingValue;
|
||
}
|
||
|
||
// 根据性别设置合理的默认值
|
||
const isMale = userProfile?.gender === 'male';
|
||
|
||
switch (measurementKey) {
|
||
case 'chestCircumference':
|
||
// 胸围:男性 85-110cm,女性 75-95cm
|
||
return isMale ? 95 : 80;
|
||
case 'waistCircumference':
|
||
// 腰围:男性 70-90cm,女性 60-80cm
|
||
return isMale ? 80 : 70;
|
||
case 'upperHipCircumference':
|
||
// 上臀围:
|
||
return 30;
|
||
case 'armCircumference':
|
||
// 臂围:男性 25-35cm,女性 20-30cm
|
||
return isMale ? 30 : 25;
|
||
case 'thighCircumference':
|
||
// 大腿围:男性 45-60cm,女性 40-55cm
|
||
return isMale ? 50 : 45;
|
||
case 'calfCircumference':
|
||
// 小腿围:男性 30-40cm,女性 25-35cm
|
||
return isMale ? 35 : 30;
|
||
default:
|
||
return 70; // 默认70cm
|
||
}
|
||
};
|
||
|
||
// Generate circumference options (30-150 cm)
|
||
const circumferenceOptions: SelectionItem[] = Array.from({ length: 121 }, (_, i) => {
|
||
const value = i + 30;
|
||
return {
|
||
label: `${value} cm`,
|
||
value: value,
|
||
};
|
||
});
|
||
|
||
// 处理围度数据点击
|
||
const handleMeasurementPress = async (measurement: typeof measurements[0]) => {
|
||
// 只有选中今天日期才能编辑
|
||
if (!isSelectedDateToday) {
|
||
return;
|
||
}
|
||
|
||
const isLoggedIn = await ensureLoggedIn();
|
||
if (!isLoggedIn) {
|
||
// 如果未登录,用户会被重定向到登录页面
|
||
return;
|
||
}
|
||
|
||
// 使用智能默认值,如果用户已有数据则使用现有数据,否则使用基于性别的合理默认值
|
||
const defaultValue = getDefaultCircumferenceValue(measurement.key, userProfile);
|
||
|
||
setSelectedMeasurement({
|
||
key: measurement.key,
|
||
label: measurement.label,
|
||
currentValue: measurement.value || defaultValue,
|
||
});
|
||
setModalVisible(true);
|
||
};
|
||
|
||
// 处理围度数据更新
|
||
const handleUpdateMeasurement = (value: string | number) => {
|
||
if (!selectedMeasurement) return;
|
||
|
||
const updateData = {
|
||
[selectedMeasurement.key]: Number(value),
|
||
};
|
||
|
||
dispatch(updateUserBodyMeasurements(updateData));
|
||
setModalVisible(false);
|
||
setSelectedMeasurement(null);
|
||
};
|
||
|
||
// 处理图表数据
|
||
const processedChartData = useMemo(() => {
|
||
if (!chartData || chartData.length === 0) {
|
||
return { labels: [], datasets: [] };
|
||
}
|
||
|
||
// 根据activeTab生成标签
|
||
const labels = chartData.map(item => {
|
||
switch (activeTab) {
|
||
case 'week':
|
||
// 将YYYY-MM-DD格式转换为星期几
|
||
const weekDay = dayjs(item.label).format('dd');
|
||
return weekDay;
|
||
case 'month':
|
||
// 将YYYY-MM-DD格式转换为第几周
|
||
const weekOfYear = dayjs(item.label).week();
|
||
const firstWeekOfMonth = dayjs(item.label).startOf('month').week();
|
||
return `第${weekOfYear - firstWeekOfMonth + 1}周`;
|
||
case 'year':
|
||
// 将YYYY-MM格式转换为月份
|
||
return dayjs(item.label).format('M月');
|
||
default:
|
||
return item.label;
|
||
}
|
||
});
|
||
|
||
// 为每个可见的围度类型生成数据集
|
||
const datasets: any[] = [];
|
||
CIRCUMFERENCE_TYPES.forEach((type) => {
|
||
if (visibleTypes.has(type.key)) {
|
||
const data = chartData.map(item => {
|
||
const value = item[type.key as keyof typeof item] as number | null;
|
||
return value || 0; // null值转换为0,图表会自动处理
|
||
});
|
||
|
||
// 只有数据中至少有一个非零值才添加到数据集
|
||
if (data.some(value => value > 0)) {
|
||
datasets.push({
|
||
data,
|
||
color: () => type.color,
|
||
strokeWidth: 2,
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
return { labels, datasets };
|
||
}, [chartData, activeTab, visibleTypes]);
|
||
|
||
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
|
||
/>
|
||
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={{
|
||
paddingBottom: 60,
|
||
paddingHorizontal: 20,
|
||
paddingTop: safeAreaTop
|
||
}}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* 日期选择器 */}
|
||
<View style={styles.dateContainer}>
|
||
<DateSelector
|
||
selectedIndex={selectedIndex}
|
||
onDateSelect={onSelectDate}
|
||
showMonthTitle={false}
|
||
disableFutureDates={true}
|
||
/>
|
||
</View>
|
||
|
||
{/* 当前日期围度数据 */}
|
||
<View style={styles.currentDataCard}>
|
||
<View style={styles.measurementsContainer}>
|
||
{measurements.map((measurement, index) => (
|
||
<TouchableOpacity
|
||
key={index}
|
||
style={[
|
||
styles.measurementItem,
|
||
!isSelectedDateToday && styles.measurementItemDisabled
|
||
]}
|
||
onPress={() => handleMeasurementPress(measurement)}
|
||
activeOpacity={isSelectedDateToday ? 0.7 : 1}
|
||
disabled={!isSelectedDateToday}
|
||
>
|
||
<View style={[styles.colorIndicator, { backgroundColor: measurement.color }]} />
|
||
<Text style={styles.label}>{measurement.label}</Text>
|
||
<View style={styles.valueContainer}>
|
||
<Text style={styles.value}>
|
||
{measurement.value ? measurement.value.toString() : '--'}
|
||
</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</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>
|
||
<TouchableOpacity
|
||
style={[styles.tab, activeTab === 'year' && styles.activeTab]}
|
||
onPress={() => handleTabPress('year')}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[styles.tabText, activeTab === 'year' && styles.activeTabText]}>
|
||
按年
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* 图例 - 支持点击切换显示/隐藏 */}
|
||
<View style={styles.legendContainer}>
|
||
{CIRCUMFERENCE_TYPES.map((type, index) => {
|
||
const isVisible = visibleTypes.has(type.key);
|
||
return (
|
||
<TouchableOpacity
|
||
key={index}
|
||
style={[styles.legendItem, !isVisible && styles.legendItemHidden]}
|
||
onPress={() => handleLegendPress(type.key)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<View style={[
|
||
styles.legendColor,
|
||
{ backgroundColor: isVisible ? type.color : '#E0E0E0' }
|
||
]} />
|
||
<Text style={[
|
||
styles.legendText,
|
||
!isVisible && styles.legendTextHidden
|
||
]}>
|
||
{type.label}
|
||
</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={() => dispatch(fetchCircumferenceAnalysis(activeTab))}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={styles.retryText}>重试</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
) : processedChartData.datasets.length > 0 ? (
|
||
<LineChart
|
||
data={{
|
||
labels: processedChartData.labels,
|
||
datasets: processedChartData.datasets,
|
||
}}
|
||
width={Dimensions.get('window').width - 80}
|
||
height={220}
|
||
yAxisSuffix="cm"
|
||
chartConfig={{
|
||
backgroundColor: '#ffffff',
|
||
backgroundGradientFrom: '#ffffff',
|
||
backgroundGradientTo: '#ffffff',
|
||
fillShadowGradientFromOpacity: 0,
|
||
fillShadowGradientToOpacity: 0,
|
||
decimalPlaces: 0,
|
||
color: (opacity = 1) => `rgba(100, 100, 100, ${opacity * 0.8})`,
|
||
labelColor: (opacity = 1) => `rgba(60, 60, 60, ${opacity})`,
|
||
style: {
|
||
borderRadius: 16,
|
||
},
|
||
propsForDots: {
|
||
r: "3",
|
||
strokeWidth: "2",
|
||
stroke: "#ffffff"
|
||
},
|
||
propsForBackgroundLines: {
|
||
strokeDasharray: "2,2",
|
||
stroke: "#E0E0E0",
|
||
strokeWidth: 1
|
||
},
|
||
}}
|
||
bezier
|
||
style={styles.chart}
|
||
/>
|
||
) : (
|
||
<View style={styles.emptyChart}>
|
||
<Text style={styles.emptyChartText}>
|
||
{processedChartData.datasets.length === 0 && !isLoading && !error
|
||
? '暂无数据'
|
||
: '请选择要显示的围度数据'
|
||
}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* 围度编辑弹窗 */}
|
||
<FloatingSelectionModal
|
||
visible={modalVisible}
|
||
onClose={() => {
|
||
setModalVisible(false);
|
||
setSelectedMeasurement(null);
|
||
}}
|
||
title={selectedMeasurement ? `设置${selectedMeasurement.label}` : '设置围度'}
|
||
items={circumferenceOptions}
|
||
selectedValue={selectedMeasurement?.currentValue}
|
||
onValueChange={() => { }} // Real-time update not needed
|
||
onConfirm={handleUpdateMeasurement}
|
||
confirmButtonText="确认"
|
||
pickerHeight={180}
|
||
/>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#FFFFFF',
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
},
|
||
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,
|
||
},
|
||
currentDataTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 16,
|
||
textAlign: 'center',
|
||
},
|
||
measurementsContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
flexWrap: 'wrap',
|
||
},
|
||
measurementItem: {
|
||
alignItems: 'center',
|
||
width: '16%',
|
||
marginBottom: 12,
|
||
},
|
||
measurementItemDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
colorIndicator: {
|
||
width: 12,
|
||
height: 12,
|
||
borderRadius: 6,
|
||
marginBottom: 4,
|
||
},
|
||
label: {
|
||
fontSize: 12,
|
||
color: '#888',
|
||
marginBottom: 8,
|
||
textAlign: 'center',
|
||
},
|
||
valueContainer: {
|
||
backgroundColor: '#F5F5F7',
|
||
borderRadius: 10,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 6,
|
||
minWidth: 32,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
value: {
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
color: '#192126',
|
||
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',
|
||
},
|
||
legendContainer: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
marginBottom: 16,
|
||
gap: 6,
|
||
},
|
||
legendItem: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: 4,
|
||
paddingHorizontal: 8,
|
||
borderRadius: 8,
|
||
},
|
||
legendItemHidden: {
|
||
opacity: 0.5,
|
||
},
|
||
legendColor: {
|
||
width: 4,
|
||
height: 4,
|
||
borderRadius: 4,
|
||
marginRight: 4,
|
||
},
|
||
legendText: {
|
||
fontSize: 10,
|
||
color: '#666',
|
||
fontWeight: '500',
|
||
},
|
||
legendTextHidden: {
|
||
color: '#999',
|
||
},
|
||
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',
|
||
},
|
||
}); |