Files
digital-pilates/app/checkin/index.tsx
richarjiang f3e6250505 feat: 添加训练计划和打卡功能
- 新增训练计划页面,允许用户制定个性化的训练计划
- 集成打卡功能,用户可以记录每日的训练情况
- 更新 Redux 状态管理,添加训练计划相关的 reducer
- 在首页中添加训练计划卡片,支持用户点击跳转
- 更新样式和布局,以适应新功能的展示和交互
- 添加日期选择器和相关依赖,支持用户选择训练日期
2025-08-13 09:10:00 +08:00

111 lines
6.4 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 { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { removeExercise, setCurrentDate, toggleExerciseCompleted } from '@/store/checkinSlice';
import { useRouter } from 'expo-router';
import React, { useEffect, useMemo } from 'react';
import { FlatList, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
function formatDate(d: Date) {
const y = d.getFullYear();
const m = `${d.getMonth() + 1}`.padStart(2, '0');
const day = `${d.getDate()}`.padStart(2, '0');
return `${y}-${m}-${day}`;
}
export default function CheckinHome() {
const dispatch = useAppDispatch();
const router = useRouter();
const today = useMemo(() => formatDate(new Date()), []);
const checkin = useAppSelector((s) => (s as any).checkin);
const record = checkin?.byDate?.[today];
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
useEffect(() => {
dispatch(setCurrentDate(today));
}, [dispatch, today]);
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
<View pointerEvents="none" style={styles.bgOrnaments}>
<View style={[styles.blob, { backgroundColor: colorTokens.ornamentPrimary, top: -60, right: -60 }]} />
<View style={[styles.blob, { backgroundColor: colorTokens.ornamentAccent, bottom: -70, left: -70 }]} />
</View>
<HeaderBar title="今日打卡" onBack={() => router.back()} withSafeTop={false} transparent />
<View style={[styles.hero, { backgroundColor: colorTokens.heroSurfaceTint }]}>
<Text style={[styles.title, { color: colorTokens.text }]}>{today}</Text>
<Text style={[styles.subtitle, { color: colorTokens.textMuted }]}></Text>
</View>
<View style={styles.actionRow}>
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]} onPress={() => router.push('/checkin/select')}>
<Text style={[styles.primaryBtnText, { color: colorTokens.onPrimary }]}></Text>
</TouchableOpacity>
</View>
<FlatList
data={record?.items || []}
keyExtractor={(item) => item.key}
contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 20 }}
ListEmptyComponent={
<View style={[styles.emptyBox, { backgroundColor: colorTokens.card }]}>
<Text style={[styles.emptyText, { color: colorTokens.textMuted }]}></Text>
</View>
}
renderItem={({ item }) => (
<View style={[styles.card, { backgroundColor: colorTokens.card }]}>
<View style={{ flex: 1 }}>
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{item.name}</Text>
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{item.category}</Text>
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}> {item.sets}{item.reps ? ` · 每组 ${item.reps}` : ''}{item.durationSec ? ` · 每组 ${item.durationSec}s` : ''}</Text>
</View>
<TouchableOpacity style={[styles.doneBtn, { backgroundColor: item.completed ? colorTokens.primary : colorTokens.border }]} onPress={() => dispatch(toggleExerciseCompleted({ date: today, key: item.key }))}>
<Text style={[styles.doneBtnText, { color: item.completed ? colorTokens.onPrimary : colorTokens.text, fontWeight: item.completed ? '800' : '700' }]}>{item.completed ? '已完成' : '完成'}</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.removeBtn, { backgroundColor: colorTokens.border }]} onPress={() => dispatch(removeExercise({ date: today, key: item.key }))}>
<Text style={[styles.removeBtnText, { color: colorTokens.text }]}></Text>
</TouchableOpacity>
</View>
)}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: '#F7F8FA' },
container: { flex: 1, backgroundColor: '#F7F8FA' },
header: { paddingHorizontal: 20, paddingTop: 12, paddingBottom: 8 },
headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 },
backButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', backgroundColor: '#E5E7EB' },
hero: { backgroundColor: 'rgba(187,242,70,0.18)', borderRadius: 16, padding: 14 },
title: { fontSize: 24, fontWeight: '800', color: '#111827' },
subtitle: { marginTop: 6, fontSize: 12, color: '#6B7280' },
bgOrnaments: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 },
blob: { position: 'absolute', width: 260, height: 260, borderRadius: 999 },
blobPrimary: { backgroundColor: '#00000000' },
blobPurple: { backgroundColor: '#00000000' },
actionRow: { paddingHorizontal: 20, marginTop: 8 },
primaryBtn: { backgroundColor: '#111827', paddingVertical: 10, borderRadius: 10, alignItems: 'center' },
primaryBtnText: { color: '#FFFFFF', fontWeight: '800' },
emptyBox: { marginTop: 16, backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginHorizontal: 20 },
emptyText: { color: '#6B7280' },
card: { marginTop: 12, marginHorizontal: 20, backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, flexDirection: 'row', alignItems: 'center', gap: 12, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3 },
cardTitle: { fontSize: 16, fontWeight: '800', color: '#111827' },
cardMeta: { marginTop: 4, fontSize: 12, color: '#6B7280' },
removeBtn: { backgroundColor: '#F3F4F6', paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8 },
removeBtnText: { color: '#111827', fontWeight: '700' },
doneBtn: { backgroundColor: '#E5E7EB', paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8, marginRight: 8 },
doneBtnActive: { backgroundColor: '#10B981' },
doneBtnText: { color: '#111827', fontWeight: '700' },
doneBtnTextActive: { color: '#FFFFFF', fontWeight: '800' },
});