Files
digital-pilates/app/workout/history.tsx
2025-10-14 16:31:19 +08:00

806 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
SectionList,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { IntensityBadge, WorkoutDetailModal } from '@/components/workout/WorkoutDetailModal';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { getWorkoutDetailMetrics, WorkoutDetailMetrics } from '@/services/workoutDetail';
import {
addHealthPermissionListener,
checkHealthPermissionStatus,
ensureHealthPermissions,
fetchWorkoutsForDateRange,
getHealthPermissionStatus,
getWorkoutTypeDisplayName,
HealthPermissionStatus,
removeHealthPermissionListener,
WorkoutActivityType,
WorkoutData,
} from '@/utils/health';
type WorkoutSection = {
title: string;
data: WorkoutData[];
};
const ICON_MAP: Partial<Record<WorkoutActivityType, keyof typeof MaterialCommunityIcons.glyphMap>> = {
// 球类运动
[WorkoutActivityType.AmericanFootball]: 'football',
[WorkoutActivityType.Archery]: 'target',
[WorkoutActivityType.AustralianFootball]: 'football',
[WorkoutActivityType.Badminton]: 'tennis',
[WorkoutActivityType.Baseball]: 'baseball',
[WorkoutActivityType.Basketball]: 'basketball',
[WorkoutActivityType.Bowling]: 'bowling',
[WorkoutActivityType.Boxing]: 'boxing-glove',
[WorkoutActivityType.Cricket]: 'cricket',
[WorkoutActivityType.Fencing]: 'sword',
[WorkoutActivityType.Golf]: 'golf',
[WorkoutActivityType.Handball]: 'basketball',
[WorkoutActivityType.Hockey]: 'hockey-sticks',
[WorkoutActivityType.Lacrosse]: 'tennis',
[WorkoutActivityType.Racquetball]: 'tennis',
[WorkoutActivityType.Soccer]: 'soccer',
[WorkoutActivityType.Softball]: 'baseball',
[WorkoutActivityType.Squash]: 'tennis',
[WorkoutActivityType.TableTennis]: 'table-tennis',
[WorkoutActivityType.Tennis]: 'tennis',
[WorkoutActivityType.Volleyball]: 'volleyball',
[WorkoutActivityType.WaterPolo]: 'swim',
[WorkoutActivityType.Pickleball]: 'tennis',
// 水上运动
[WorkoutActivityType.Swimming]: 'swim',
[WorkoutActivityType.Sailing]: 'sail-boat',
[WorkoutActivityType.SurfingSports]: 'waves',
[WorkoutActivityType.WaterFitness]: 'swim',
[WorkoutActivityType.WaterSports]: 'swim',
[WorkoutActivityType.UnderwaterDiving]: 'swim',
// 跑步和步行
[WorkoutActivityType.Running]: 'run',
[WorkoutActivityType.Walking]: 'walk',
[WorkoutActivityType.Hiking]: 'hiking',
[WorkoutActivityType.StairClimbing]: 'stairs',
[WorkoutActivityType.Stairs]: 'stairs',
// 骑行
[WorkoutActivityType.Cycling]: 'bike',
[WorkoutActivityType.HandCycling]: 'bike',
// 滑雪和滑冰
[WorkoutActivityType.CrossCountrySkiing]: 'ski',
[WorkoutActivityType.DownhillSkiing]: 'ski',
[WorkoutActivityType.Snowboarding]: 'snowboard',
[WorkoutActivityType.SkatingSports]: 'skateboarding',
[WorkoutActivityType.SnowSports]: 'ski',
// 力量训练
[WorkoutActivityType.FunctionalStrengthTraining]: 'weight-lifter',
[WorkoutActivityType.TraditionalStrengthTraining]: 'dumbbell',
[WorkoutActivityType.CrossTraining]: 'arm-flex',
[WorkoutActivityType.CoreTraining]: 'arm-flex',
// 有氧运动
[WorkoutActivityType.Elliptical]: 'bike',
[WorkoutActivityType.Rowing]: 'rowing',
[WorkoutActivityType.MixedCardio]: 'heart-pulse',
[WorkoutActivityType.MixedMetabolicCardioTraining]: 'heart-pulse',
[WorkoutActivityType.HighIntensityIntervalTraining]: 'run-fast',
[WorkoutActivityType.JumpRope]: 'skip-forward',
[WorkoutActivityType.StepTraining]: 'stairs',
// 舞蹈和身心训练
[WorkoutActivityType.Dance]: 'music',
[WorkoutActivityType.DanceInspiredTraining]: 'music',
[WorkoutActivityType.CardioDance]: 'music',
[WorkoutActivityType.SocialDance]: 'music',
[WorkoutActivityType.Yoga]: 'meditation',
[WorkoutActivityType.MindAndBody]: 'meditation',
[WorkoutActivityType.TaiChi]: 'meditation',
[WorkoutActivityType.Pilates]: 'meditation',
[WorkoutActivityType.Barre]: 'meditation',
[WorkoutActivityType.Flexibility]: 'meditation',
[WorkoutActivityType.Cooldown]: 'meditation',
[WorkoutActivityType.PreparationAndRecovery]: 'meditation',
// 户外运动
[WorkoutActivityType.Climbing]: 'hiking',
[WorkoutActivityType.EquestrianSports]: 'horse',
[WorkoutActivityType.Fishing]: 'target',
[WorkoutActivityType.Hunting]: 'target',
[WorkoutActivityType.PaddleSports]: 'rowing',
// 综合运动
[WorkoutActivityType.SwimBikeRun]: 'run-fast',
[WorkoutActivityType.Transition]: 'swap-horizontal-variant',
[WorkoutActivityType.Play]: 'gamepad-variant',
[WorkoutActivityType.FitnessGaming]: 'gamepad-variant',
[WorkoutActivityType.DiscSports]: 'target',
// 其他
[WorkoutActivityType.Other]: 'arm-flex',
[WorkoutActivityType.MartialArts]: 'karate',
[WorkoutActivityType.Kickboxing]: 'boxing-glove',
[WorkoutActivityType.Gymnastics]: 'human',
[WorkoutActivityType.TrackAndField]: 'run-fast',
[WorkoutActivityType.WheelchairWalkPace]: 'wheelchair',
[WorkoutActivityType.WheelchairRunPace]: 'wheelchair',
[WorkoutActivityType.Curling]: 'target',
};
type ActivitySummary = {
type: WorkoutActivityType;
duration: number;
count: number;
displayName: string;
iconName: keyof typeof MaterialCommunityIcons.glyphMap;
};
type MonthlyStatsInfo = {
items: ActivitySummary[];
totalDuration: number;
totalCount: number;
monthStart: string;
monthEnd: string;
snapshotDate: string;
};
const MONTHLY_STAT_COLORS = [
{ background: '#FDE9F4', pill: '#F8CDE2', bar: '#F9CFE3', icon: '#2F2965', label: '#5A648C' },
{ background: '#FFF3D6', pill: '#FFE3A4', bar: '#FFE0A6', icon: '#2F2965', label: '#5A648C' },
{ background: '#E3F5F3', pill: '#CBEAE4', bar: '#D7EEE8', icon: '#2F2965', label: '#5A648C' },
];
function formatDurationShort(durationInSeconds: number): string {
const totalMinutes = Math.max(Math.round(durationInSeconds / 60), 1);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours > 0) {
return minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`;
}
return `${totalMinutes}m`;
}
// 扩展 dayjs 以支持 isBetween 插件
dayjs.extend(isBetween);
function computeMonthlyStats(workouts: WorkoutData[]): MonthlyStatsInfo | null {
const now = dayjs();
const monthStart = now.startOf('month');
const monthEnd = now.endOf('month');
const monthlyEntries = workouts.filter((workout) => {
const workoutDate = dayjs(workout.startDate || workout.endDate);
if (!workoutDate.isValid()) {
return false;
}
return workoutDate.isBetween(monthStart, monthEnd, 'day', '[]');
});
if (monthlyEntries.length === 0) {
return null;
}
const summaryMap = monthlyEntries.reduce<Record<string, ActivitySummary>>((acc, workout) => {
const type = workout.workoutActivityType;
if (type === undefined || type === null) {
return acc;
}
const mapKey = String(type);
if (!acc[mapKey]) {
acc[mapKey] = {
type,
duration: 0,
count: 0,
displayName: getWorkoutTypeDisplayName(type),
iconName: ICON_MAP[type as WorkoutActivityType] || 'run',
};
}
acc[mapKey].duration += workout.duration || 0;
acc[mapKey].count += 1;
return acc;
}, {});
const items = Object.values(summaryMap).sort((a, b) => b.duration - a.duration);
const totalDuration = monthlyEntries.reduce((sum, workout) => sum + (workout.duration || 0), 0);
return {
items,
totalDuration,
totalCount: monthlyEntries.length,
monthStart: monthStart.format('YYYY-MM-DD'),
monthEnd: monthEnd.format('YYYY-MM-DD'),
snapshotDate: now.format('YYYY-MM-DD'),
};
}
function getIntensityBadge(totalCalories?: number, durationInSeconds?: number) {
if (!totalCalories || !durationInSeconds) {
return { label: '低强度', color: '#7C85A3', background: '#E4E7F2' };
}
const minutes = Math.max(durationInSeconds / 60, 1);
const caloriesPerMinute = totalCalories / minutes;
if (caloriesPerMinute >= 9) {
return { label: '高强度', color: '#F85959', background: '#FFE6E6' };
}
if (caloriesPerMinute >= 5) {
return { label: '中强度', color: '#0EAF71', background: '#E4F6EF' };
}
return { label: '低强度', color: '#5966FF', background: '#E7EBFF' };
}
function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] {
const grouped = workouts.reduce<Record<string, WorkoutData[]>>((acc, workout) => {
const dateKey = dayjs(workout.startDate || workout.endDate).format('YYYY-MM-DD');
if (!acc[dateKey]) {
acc[dateKey] = [];
}
acc[dateKey].push(workout);
return acc;
}, {});
return Object.keys(grouped)
.sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf())
.map((dateKey) => ({
title: dayjs(dateKey).format('M月D日'),
data: grouped[dateKey]
.sort((a, b) => dayjs(b.startDate || b.endDate).valueOf() - dayjs(a.startDate || a.endDate).valueOf()),
}));
}
export default function WorkoutHistoryScreen() {
const [sections, setSections] = useState<WorkoutSection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedWorkout, setSelectedWorkout] = useState<WorkoutData | null>(null);
const [isDetailVisible, setIsDetailVisible] = useState(false);
const [detailMetrics, setDetailMetrics] = useState<WorkoutDetailMetrics | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [detailError, setDetailError] = useState<string | null>(null);
const [selectedIntensity, setSelectedIntensity] = useState<IntensityBadge | null>(null);
const [monthOccurrenceText, setMonthOccurrenceText] = useState<string | null>(null);
const [monthlyStats, setMonthlyStats] = useState<MonthlyStatsInfo | null>(null);
const safeAreaTop = useSafeAreaTop()
const loadHistory = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
let permissionStatus = getHealthPermissionStatus();
if (permissionStatus !== HealthPermissionStatus.Authorized) {
permissionStatus = await checkHealthPermissionStatus(true);
}
let hasPermission = permissionStatus === HealthPermissionStatus.Authorized;
if (!hasPermission) {
hasPermission = await ensureHealthPermissions();
}
if (!hasPermission) {
setSections([]);
setError('尚未授予健康数据权限');
setMonthlyStats(null);
return;
}
const end = dayjs();
const start = end.subtract(1, 'month');
const workouts = await fetchWorkoutsForDateRange(start.toDate(), end.toDate(), 200);
const filteredWorkouts = workouts.filter((workout) => workout.duration && workout.duration > 0);
setMonthlyStats(computeMonthlyStats(filteredWorkouts));
setSections(groupWorkouts(filteredWorkouts));
} catch (err) {
console.error('加载锻炼历史失败:', err);
setError('加载锻炼记录失败,请稍后再试');
setSections([]);
setMonthlyStats(null);
} finally {
setIsLoading(false);
}
}, []);
useFocusEffect(
useCallback(() => {
loadHistory();
}, [loadHistory])
);
React.useEffect(() => {
const handlePermissionGranted = () => {
loadHistory();
};
addHealthPermissionListener('permissionGranted', handlePermissionGranted);
return () => {
removeHealthPermissionListener('permissionGranted', handlePermissionGranted);
};
}, [loadHistory]);
const headerComponent = useMemo(() => {
const statsItems = monthlyStats?.items.slice(0, 3) ?? [];
const monthEndDay = monthlyStats
? dayjs(monthlyStats.monthEnd).date()
: dayjs().endOf('month').date();
const snapshotLabel = monthlyStats
? dayjs(monthlyStats.snapshotDate).format('M月D日')
: dayjs().format('M月D日');
const overviewText = monthlyStats
? `截至${snapshotLabel},你已完成${monthlyStats.totalCount}次锻炼,累计${formatDurationShort(monthlyStats.totalDuration)}`
: '本月还没有锻炼记录,动起来收集第一条吧!';
const periodText = `统计周期1日 - ${monthEndDay}日(本月)`;
const maxDuration = statsItems[0]?.duration || 1;
return (
<View style={styles.headerContainer}>
{/* <Text style={styles.headerTitle}>历史</Text>
<Text style={styles.headerSubtitle}>最近一个月的锻炼记录</Text> */}
<View style={styles.monthlyStatsWrapper}>
{/* <Text style={styles.monthlyStatsTitle}>统计</Text> */}
<View style={styles.monthlyStatsCardShell}>
<LinearGradient
colors={['#FFFFFF', '#F5F6FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.monthlyStatsCard}
>
<Text style={styles.statSectionLabel}></Text>
<Text style={styles.statPeriodText}>{periodText}</Text>
<Text style={styles.statDescription}>{overviewText}</Text>
{statsItems.length > 0 ? (
statsItems.map((item, index) => {
const palette = MONTHLY_STAT_COLORS[index % MONTHLY_STAT_COLORS.length];
const ratio = Math.max(Math.min(item.duration / maxDuration, 1), 0.18);
return (
<View key={String(item.type)} style={styles.summaryRowWrapper}>
<View style={[styles.summaryRowBackground, { backgroundColor: palette.background }]}>
<View
style={[
styles.summaryRowFill,
{ width: `${ratio * 100}%`, backgroundColor: palette.bar },
]}
/>
<View style={styles.summaryRowInner}>
<View style={[styles.summaryBadge, { backgroundColor: palette.pill }]}>
<MaterialCommunityIcons name={item.iconName} size={20} color={palette.icon} />
<Text style={[styles.summaryCount, { color: palette.icon }]}>X{item.count}</Text>
</View>
<View style={styles.summaryRowContent}>
<Text style={[styles.summaryDuration, { color: palette.icon }]}>{formatDurationShort(item.duration)}</Text>
<Text style={[styles.summaryActivity, { color: palette.label }]}>{item.displayName}</Text>
</View>
</View>
</View>
</View>
);
})
) : (
<View style={styles.statEmptyState}>
<MaterialCommunityIcons name="calendar-blank" size={20} color="#7C85A3" />
<Text style={styles.statEmptyText}></Text>
</View>
)}
</LinearGradient>
</View>
</View>
</View>
);
}, [monthlyStats]);
const emptyComponent = useMemo(() => (
<View style={styles.emptyContainer}>
<MaterialCommunityIcons name="calendar-blank" size={40} color="#9AA4C4" />
<Text style={styles.emptyText}></Text>
<Text style={styles.emptySubText}></Text>
</View>
), []);
const computeMonthlyOccurrenceText = useCallback((workout: WorkoutData): string | null => {
const workoutDate = dayjs(workout.startDate || workout.endDate);
if (!workoutDate.isValid() || sections.length === 0) {
return null;
}
const sameMonthWorkouts = sections
.flatMap((section) => section.data)
.filter((entry) => {
const entryDate = dayjs(entry.startDate || entry.endDate);
return (
entryDate.isValid() &&
entryDate.isSame(workoutDate, 'month') &&
entry.workoutActivityType === workout.workoutActivityType
);
});
const ascending = sameMonthWorkouts.some((entry) => entry.id === workout.id)
? sameMonthWorkouts
: [...sameMonthWorkouts, workout];
ascending.sort(
(a, b) =>
dayjs(a.startDate || a.endDate).valueOf() - dayjs(b.startDate || b.endDate).valueOf()
);
const index = ascending.findIndex((entry) => entry.id === workout.id);
if (index === -1) {
return null;
}
const activityLabel = getWorkoutTypeDisplayName(workout.workoutActivityType);
return `这是你${workoutDate.format('M月')}的第 ${index + 1}${activityLabel}`;
}, [sections]);
const loadWorkoutDetail = useCallback(async (workout: WorkoutData) => {
setDetailLoading(true);
setDetailError(null);
try {
const metrics = await getWorkoutDetailMetrics(workout);
setDetailMetrics(metrics);
} catch (err) {
console.error('加载锻炼详情失败:', err);
setDetailMetrics(null);
setDetailError('加载锻炼详情失败,请稍后再试');
} finally {
setDetailLoading(false);
}
}, []);
const handleWorkoutPress = useCallback((workout: WorkoutData) => {
const intensity = getIntensityBadge(workout.totalEnergyBurned, workout.duration || 0);
setSelectedIntensity(intensity);
setSelectedWorkout(workout);
setDetailMetrics(null);
setDetailError(null);
setMonthOccurrenceText(computeMonthlyOccurrenceText(workout));
setIsDetailVisible(true);
loadWorkoutDetail(workout);
}, [computeMonthlyOccurrenceText, loadWorkoutDetail]);
const handleRetryDetail = useCallback(() => {
if (selectedWorkout) {
loadWorkoutDetail(selectedWorkout);
}
}, [selectedWorkout, loadWorkoutDetail]);
const handleCloseDetail = useCallback(() => {
setIsDetailVisible(false);
}, []);
const renderItem = useCallback(({ item }: { item: WorkoutData }) => {
const calories = Math.round(item.totalEnergyBurned || 0);
const minutes = Math.max(Math.round((item.duration || 0) / 60), 1);
const intensity = getIntensityBadge(item.totalEnergyBurned, item.duration || 0);
const iconName = ICON_MAP[item.workoutActivityType as WorkoutActivityType] || 'arm-flex';
const time = dayjs(item.startDate || item.endDate).format('HH:mm');
const activityLabel = getWorkoutTypeDisplayName(item.workoutActivityType);
return (
<TouchableOpacity
style={styles.historyCard}
activeOpacity={0.85}
onPress={() => handleWorkoutPress(item)}
>
<View style={styles.cardIconWrapper}>
<MaterialCommunityIcons name={iconName} size={28} color="#5C55FF" />
</View>
<View style={styles.cardContent}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{calories} · {minutes}</Text>
<View style={[styles.intensityBadge, { backgroundColor: intensity.background }]}>
<Text style={[styles.intensityText, { color: intensity.color }]}>{intensity.label}</Text>
</View>
</View>
<Text style={styles.cardSubtitle}>{activityLabel}{time}</Text>
</View>
{/* <Ionicons name="chevron-forward" size={20} color="#9AA4C4" /> */}
</TouchableOpacity>
);
}, [handleWorkoutPress]);
const renderSectionHeader = useCallback(({ section }: { section: WorkoutSection }) => (
<Text style={styles.sectionHeader}>{section.title}</Text>
), []);
return (
<View style={styles.safeArea}>
<LinearGradient
colors={["#F3F5FF", "#FFFFFF"]}
style={StyleSheet.absoluteFill}
/>
<HeaderBar title="锻炼总结" variant="minimal" transparent={true} />
{isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#5C55FF" />
<Text style={styles.loadingText}>...</Text>
</View>
) : (
<SectionList
style={[styles.sectionList, {
paddingTop: safeAreaTop
}]}
sections={sections}
keyExtractor={(item) => item.id}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
ListHeaderComponent={headerComponent}
ListEmptyComponent={error ? (
<View style={styles.emptyContainer}>
<MaterialCommunityIcons name="alert-circle" size={40} color="#F85959" />
<Text style={[styles.emptyText, { color: '#F85959' }]}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={loadHistory}>
<Text style={styles.retryText}></Text>
</TouchableOpacity>
</View>
) : emptyComponent}
contentContainerStyle={styles.listContent}
stickySectionHeadersEnabled={false}
showsVerticalScrollIndicator={false}
/>
)}
<WorkoutDetailModal
visible={isDetailVisible}
onClose={handleCloseDetail}
workout={selectedWorkout}
metrics={detailMetrics}
loading={detailLoading}
intensityBadge={selectedIntensity || undefined}
monthOccurrenceText={monthOccurrenceText || undefined}
onRetry={detailError ? handleRetryDetail : undefined}
errorMessage={detailError}
/>
</View>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: 'transparent',
},
sectionList: {
flex: 1,
},
headerContainer: {
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: 16,
},
headerTitle: {
fontSize: 26,
fontWeight: '700',
color: '#1F2355',
marginBottom: 6,
},
headerSubtitle: {
fontSize: 14,
color: '#677086',
},
monthlyStatsWrapper: {
},
monthlyStatsTitle: {
fontSize: 20,
fontWeight: '700',
color: '#1F2355',
marginBottom: 14,
},
monthlyStatsCardShell: {
borderRadius: 28,
shadowColor: '#5460E54D',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.12,
shadowRadius: 18,
elevation: 8,
},
monthlyStatsCard: {
paddingHorizontal: 20,
paddingVertical: 22,
borderRadius: 28,
overflow: 'hidden',
gap: 12,
},
statSectionLabel: {
fontSize: 15,
fontWeight: '600',
color: '#8289A9',
},
statPeriodText: {
fontSize: 12,
color: '#8C95B0',
},
statDescription: {
marginTop: 2,
fontSize: 13,
color: '#525A7A',
lineHeight: 18,
},
summaryRowWrapper: {
marginTop: 12,
},
summaryRowBackground: {
borderRadius: 24,
overflow: 'hidden',
position: 'relative',
},
summaryRowFill: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
},
summaryRowInner: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 18,
gap: 14,
},
summaryBadge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 14,
paddingVertical: 9,
borderRadius: 999,
gap: 6,
},
summaryRowContent: {
flex: 1,
gap: 4,
},
summaryCount: {
fontSize: 14,
fontWeight: '700',
color: '#2F2965',
},
summaryDuration: {
fontSize: 16,
fontWeight: '700',
color: '#2F2965',
},
summaryActivity: {
fontSize: 13,
fontWeight: '500',
color: '#565F7F',
},
statEmptyState: {
marginTop: 14,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statEmptyText: {
fontSize: 13,
color: '#7C85A3',
},
listContent: {
paddingBottom: 40,
},
sectionHeader: {
fontSize: 14,
color: '#8087A2',
fontWeight: '600',
marginTop: 18,
paddingHorizontal: 20,
},
historyCard: {
marginTop: 12,
marginHorizontal: 16,
paddingVertical: 18,
paddingHorizontal: 18,
backgroundColor: '#FFFFFF',
borderRadius: 26,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#5460E54D',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.1,
shadowRadius: 16,
elevation: 6,
},
cardIconWrapper: {
width: 46,
height: 46,
borderRadius: 23,
backgroundColor: '#EEF0FF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
},
cardContent: {
flex: 1,
},
cardTitleRow: {
flexDirection: 'row',
alignItems: 'center',
},
cardTitle: {
fontSize: 16,
fontWeight: '700',
color: '#1F2355',
flexShrink: 1,
},
intensityBadge: {
marginLeft: 8,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 10,
},
intensityText: {
fontSize: 12,
fontWeight: '600',
},
cardSubtitle: {
marginTop: 8,
fontSize: 13,
color: '#6B7693',
},
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
loadingText: {
fontSize: 14,
color: '#596182',
},
emptyContainer: {
marginTop: 60,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
gap: 12,
},
emptyText: {
fontSize: 16,
fontWeight: '600',
color: '#596182',
},
emptySubText: {
fontSize: 13,
color: '#8F96AF',
textAlign: 'center',
lineHeight: 18,
},
retryButton: {
marginTop: 8,
paddingHorizontal: 18,
paddingVertical: 8,
borderRadius: 16,
backgroundColor: '#5C55FF',
},
retryText: {
color: '#FFFFFF',
fontSize: 13,
fontWeight: '600',
},
});