- 在个人信息页面中修改用户姓名字段为“name”,并添加注销帐号功能,支持用户删除帐号及相关数据 - 在打卡页面中集成从后端获取当天打卡列表的功能,确保用户数据的实时同步 - 更新 Redux 状态管理,支持打卡记录的同步和更新 - 新增打卡服务,提供创建、更新和删除打卡记录的 API 接口 - 优化样式以适应新功能的展示和交互
157 lines
8.3 KiB
TypeScript
157 lines
8.3 KiB
TypeScript
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||
import type { CheckinExercise } from '@/store/checkinSlice';
|
||
import { getDailyCheckins, removeExercise, setCurrentDate, syncCheckin, toggleExerciseCompleted } from '@/store/checkinSlice';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { useFocusEffect } from '@react-navigation/native';
|
||
import { useRouter } from 'expo-router';
|
||
import React, { useEffect, useMemo } from 'react';
|
||
import { Alert, 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(getDailyCheckins(today)).unwrap().catch((err: any) => {
|
||
Alert.alert('获取打卡失败', err?.message || '请稍后重试');
|
||
});
|
||
}, [dispatch, today]);
|
||
|
||
useFocusEffect(
|
||
React.useCallback(() => {
|
||
// 返回本页时确保与后端同步(若本地有内容则上报,后台 upsert)
|
||
if (record?.items && Array.isArray(record.items)) {
|
||
dispatch(syncCheckin({ date: today, items: record.items as CheckinExercise[], note: record?.note }));
|
||
}
|
||
return () => { };
|
||
}, [dispatch, today, record?.items])
|
||
);
|
||
|
||
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
|
||
accessibilityRole="button"
|
||
accessibilityLabel={item.completed ? '已完成,点击取消完成' : '未完成,点击标记完成'}
|
||
style={styles.doneIconBtn}
|
||
onPress={() => {
|
||
dispatch(toggleExerciseCompleted({ date: today, key: item.key }));
|
||
const nextItems: CheckinExercise[] = (record?.items || []).map((it: CheckinExercise) =>
|
||
it.key === item.key ? { ...it, completed: !it.completed } : it
|
||
);
|
||
dispatch(syncCheckin({ date: today, items: nextItems, note: record?.note }));
|
||
}}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
>
|
||
<Ionicons
|
||
name={item.completed ? 'checkmark-circle' : 'checkmark-circle-outline'}
|
||
size={24}
|
||
color={item.completed ? colorTokens.primary : colorTokens.textMuted}
|
||
/>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[styles.removeBtn, { backgroundColor: colorTokens.border }]}
|
||
onPress={() =>
|
||
Alert.alert('确认移除', '确定要移除该动作吗?', [
|
||
{ text: '取消', style: 'cancel' },
|
||
{
|
||
text: '移除',
|
||
style: 'destructive',
|
||
onPress: () => {
|
||
dispatch(removeExercise({ date: today, key: item.key }));
|
||
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== item.key);
|
||
dispatch(syncCheckin({ date: today, items: nextItems, note: record?.note }));
|
||
},
|
||
},
|
||
])
|
||
}
|
||
>
|
||
<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: 0 },
|
||
emptyText: { color: '#6B7280' },
|
||
card: { marginTop: 12, marginHorizontal: 0, 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' },
|
||
doneIconBtn: { paddingHorizontal: 4, paddingVertical: 4, borderRadius: 16, marginRight: 8 },
|
||
|
||
});
|
||
|
||
|