feat: 支持围度数据图表
This commit is contained in:
700
app/circumference-detail.tsx
Normal file
700
app/circumference-detail.tsx
Normal file
@@ -0,0 +1,700 @@
|
||||
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 { CircumferencePeriod } from '@/services/circumferenceAnalysis';
|
||||
|
||||
type TabType = CircumferencePeriod;
|
||||
|
||||
export default function CircumferenceDetailScreen() {
|
||||
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
|
||||
}}
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user