refactor(coach): 重构教练组件,统一导入并简化UI实现与类型定义

This commit is contained in:
richarjiang
2025-08-28 09:46:14 +08:00
parent ba2d829e02
commit 5a59508b88
17 changed files with 2400 additions and 866 deletions

View File

@@ -1,31 +1,33 @@
import NumberKeyboard from '@/components/NumberKeyboard';
import { WeightRecordCard } from '@/components/weight/WeightRecordCard';
import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { fetchWeightHistory, updateUserProfile, WeightHistoryItem } from '@/store/userSlice';
import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Alert,
Modal,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
export default function WeightRecordsPage() {
const dispatch = useAppDispatch();
const userProfile = useAppSelector((s) => s.user.profile);
const weightHistory = useAppSelector((s) => s.user.weightHistory);
const [showWeightPicker, setShowWeightPicker] = useState(false);
const [pickerType, setPickerType] = useState<'current' | 'initial' | 'target'>('current');
const [pickerType, setPickerType] = useState<'current' | 'initial' | 'target' | 'edit'>('current');
const [inputWeight, setInputWeight] = useState('');
const [editingRecord, setEditingRecord] = useState<WeightHistoryItem | null>(null);
const colorScheme = useColorScheme();
const themeColors = Colors[colorScheme ?? 'light'];
@@ -74,6 +76,23 @@ export default function WeightRecordsPage() {
setShowWeightPicker(true);
};
const handleEditWeightRecord = (record: WeightHistoryItem) => {
setPickerType('edit');
setEditingRecord(record);
initializeInput(parseFloat(record.weight));
setShowWeightPicker(true);
};
const handleDeleteWeightRecord = async (id: string) => {
try {
await dispatch(deleteWeightRecord(id) as any);
await loadWeightHistory();
} catch (error) {
console.error('删除体重记录失败:', error);
Alert.alert('错误', '删除体重记录失败,请重试');
}
};
const handleWeightSave = async () => {
const weight = parseFloat(inputWeight);
if (isNaN(weight) || weight <= 0 || weight > 500) {
@@ -88,20 +107,45 @@ export default function WeightRecordsPage() {
} else if (pickerType === 'initial') {
// Update initial weight in profile
console.log('更新初始体重');
await dispatch(updateUserProfile({ initialWeight: weight }) as any);
} else if (pickerType === 'target') {
// Update target weight in profile
await dispatch(updateUserProfile({ targetWeight: weight }) as any);
} else if (pickerType === 'edit' && editingRecord) {
await dispatch(updateWeightRecord({ id: editingRecord.id, weight }) as any);
}
setShowWeightPicker(false);
setInputWeight('');
setEditingRecord(null);
await loadWeightHistory();
} catch (error) {
console.error('保存体重失败:', error);
Alert.alert('错误', '保存体重失败,请重试');
}
};
const handleNumberPress = (number: string) => {
setInputWeight(prev => {
// 防止输入多个0开头
if (prev === '0' && number === '0') return prev;
// 如果当前是0输入非0数字时替换
if (prev === '0' && number !== '0') return number;
return prev + number;
});
};
const handleDeletePress = () => {
setInputWeight(prev => prev.slice(0, -1));
};
const handleDecimalPress = () => {
setInputWeight(prev => {
if (prev.includes('.')) return prev;
// 如果没有输入任何数字自动添加0
if (!prev) return '0.';
return prev + '.';
});
};
// Process weight history data
const sortedHistory = [...weightHistory]
@@ -210,38 +254,13 @@ export default function WeightRecordsPage() {
parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0;
return (
<View key={`${record.createdAt}-${recordIndex}`} style={styles.recordCard}>
<View style={styles.recordHeader}>
<Text style={styles.recordDateTime}>
{dayjs(record.createdAt).format('MM月DD日 HH:mm')}
</Text>
{/* <TouchableOpacity style={styles.recordEditButton}>
<Ionicons name="create-outline" size={16} color="#FF9500" />
</TouchableOpacity> */}
</View>
<View style={styles.recordContent}>
<Text style={styles.recordWeightLabel}></Text>
<Text style={styles.recordWeightValue}>{record.weight}kg</Text>
{Math.abs(weightChange) > 0 && (
<View style={[
styles.weightChangeTag,
{ backgroundColor: weightChange < 0 ? '#E8F5E8' : '#FFF2E8' }
]}>
<Ionicons
name={weightChange < 0 ? "arrow-down" : "arrow-up"}
size={12}
color={weightChange < 0 ? Colors.light.accentGreen : '#FF9500'}
/>
<Text style={[
styles.weightChangeText,
{ color: weightChange < 0 ? Colors.light.accentGreen : '#FF9500' }
]}>
{Math.abs(weightChange).toFixed(1)}
</Text>
</View>
)}
</View>
</View>
<WeightRecordCard
key={`${record.createdAt}-${recordIndex}`}
record={record}
onPress={handleEditWeightRecord}
onDelete={handleDeleteWeightRecord}
weightChange={weightChange}
/>
);
})}
</View>
@@ -263,90 +282,99 @@ export default function WeightRecordsPage() {
transparent
onRequestClose={() => setShowWeightPicker(false)}
>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={() => setShowWeightPicker(false)}
/>
<View style={[styles.modalSheet, { backgroundColor: themeColors.background }]}>
{/* Header */}
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setShowWeightPicker(false)}>
<Ionicons name="close" size={24} color={themeColors.text} />
</TouchableOpacity>
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
{pickerType === 'current' && '记录体重'}
{pickerType === 'initial' && '编辑初始体重'}
{pickerType === 'target' && '编辑目标体重'}
</Text>
<View style={{ width: 24 }} />
</View>
<View style={styles.modalContent}>
{/* Weight Input Section */}
<View style={styles.inputSection}>
<View style={styles.weightInputContainer}>
<View style={styles.weightIcon}>
<Ionicons name="scale-outline" size={20} color="#6366F1" />
</View>
<View style={styles.inputWrapper}>
<TextInput
style={[styles.weightInput, { color: themeColors.text }]}
placeholder="输入体重"
placeholderTextColor={themeColors.textSecondary}
value={inputWeight}
onChangeText={setInputWeight}
keyboardType="decimal-pad"
autoFocus={true}
selectTextOnFocus={true}
/>
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>kg</Text>
</View>
</View>
{/* Weight Range Hint */}
<Text style={[styles.hintText, { color: themeColors.textSecondary }]}>
0-500
<View style={styles.modalContainer}>
<TouchableOpacity
style={styles.modalBackdrop}
activeOpacity={1}
onPress={() => setShowWeightPicker(false)}
/>
<View style={[styles.modalSheet, { backgroundColor: themeColors.background }]}>
{/* Header */}
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => setShowWeightPicker(false)}>
<Ionicons name="close" size={24} color={themeColors.text} />
</TouchableOpacity>
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
{pickerType === 'current' && '记录体重'}
{pickerType === 'initial' && '编辑初始体重'}
{pickerType === 'target' && '编辑目标体重'}
{pickerType === 'edit' && '编辑体重记录'}
</Text>
<View style={{ width: 24 }} />
</View>
{/* Quick Selection */}
<View style={styles.quickSelectionSection}>
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}></Text>
<View style={styles.quickButtons}>
{[50, 60, 70, 80, 90].map((weight) => (
<TouchableOpacity
key={weight}
style={[
styles.quickButton,
inputWeight === weight.toString() && styles.quickButtonSelected
]}
onPress={() => setInputWeight(weight.toString())}
>
<Text style={[
styles.quickButtonText,
inputWeight === weight.toString() && styles.quickButtonTextSelected
]}>
{weight}kg
</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
{/* Save Button */}
<View style={styles.modalFooter}>
<TouchableOpacity
style={[
styles.saveButton,
{ opacity: !inputWeight.trim() ? 0.5 : 1 }
]}
onPress={handleWeightSave}
disabled={!inputWeight.trim()}
<ScrollView
style={styles.modalContent}
showsVerticalScrollIndicator={false}
>
<Text style={styles.saveButtonText}></Text>
</TouchableOpacity>
{/* Weight Display Section */}
<View style={styles.inputSection}>
<View style={styles.weightInputContainer}>
<View style={styles.weightIcon}>
<Ionicons name="scale-outline" size={20} color="#6366F1" />
</View>
<View style={styles.inputWrapper}>
<Text style={[styles.weightDisplay, { color: inputWeight ? themeColors.text : themeColors.textSecondary }]}>
{inputWeight || '输入体重'}
</Text>
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>kg</Text>
</View>
</View>
{/* Weight Range Hint */}
<Text style={[styles.hintText, { color: themeColors.textSecondary }]}>
0-500
</Text>
</View>
{/* Quick Selection */}
<View style={styles.quickSelectionSection}>
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}></Text>
<View style={styles.quickButtons}>
{[50, 60, 70, 80, 90].map((weight) => (
<TouchableOpacity
key={weight}
style={[
styles.quickButton,
inputWeight === weight.toString() && styles.quickButtonSelected
]}
onPress={() => setInputWeight(weight.toString())}
>
<Text style={[
styles.quickButtonText,
inputWeight === weight.toString() && styles.quickButtonTextSelected
]}>
{weight}kg
</Text>
</TouchableOpacity>
))}
</View>
</View>
</ScrollView>
{/* Custom Number Keyboard */}
<NumberKeyboard
onNumberPress={handleNumberPress}
onDeletePress={handleDeletePress}
onDecimalPress={handleDecimalPress}
hasDecimal={inputWeight.includes('.')}
maxLength={6}
currentValue={inputWeight}
/>
{/* Save Button */}
<View style={styles.modalFooter}>
<TouchableOpacity
style={[
styles.saveButton,
{ opacity: !inputWeight.trim() ? 0.5 : 1 }
]}
onPress={handleWeightSave}
disabled={!inputWeight.trim()}
>
<Text style={styles.saveButtonText}></Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
@@ -486,62 +514,7 @@ const styles = StyleSheet.create({
fontWeight: '700',
color: '#192126',
},
recordCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
recordHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
recordDateTime: {
fontSize: 14,
color: '#687076',
fontWeight: '500',
},
recordEditButton: {
padding: 6,
borderRadius: 8,
backgroundColor: 'rgba(255, 149, 0, 0.1)',
},
recordContent: {
flexDirection: 'row',
alignItems: 'center',
},
recordWeightLabel: {
fontSize: 16,
color: '#687076',
fontWeight: '500',
},
recordWeightValue: {
fontSize: 18,
fontWeight: '700',
color: '#192126',
marginLeft: 4,
flex: 1,
},
weightChangeTag: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
marginLeft: 12,
},
weightChangeText: {
fontSize: 12,
fontWeight: '600',
marginLeft: 2,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
@@ -562,6 +535,9 @@ const styles = StyleSheet.create({
color: '#687076',
},
// Modal Styles
modalContainer: {
flex: 1,
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.35)',
@@ -573,7 +549,8 @@ const styles = StyleSheet.create({
bottom: 0,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: '80%',
maxHeight: '85%',
minHeight: 500,
},
modalHeader: {
flexDirection: 'row',
@@ -588,8 +565,8 @@ const styles = StyleSheet.create({
fontWeight: '600',
},
modalContent: {
flex: 1,
paddingHorizontal: 20,
paddingBottom: 10,
},
inputSection: {
backgroundColor: '#FFFFFF',
@@ -619,11 +596,12 @@ const styles = StyleSheet.create({
borderBottomColor: '#E5E7EB',
paddingBottom: 6,
},
weightInput: {
weightDisplay: {
flex: 1,
fontSize: 24,
fontWeight: '600',
textAlign: 'center',
paddingVertical: 4,
},
unitLabel: {
fontSize: 18,
@@ -637,7 +615,7 @@ const styles = StyleSheet.create({
},
quickSelectionSection: {
paddingHorizontal: 4,
marginBottom: 8,
marginBottom: 20,
},
quickSelectionTitle: {
fontSize: 16,
@@ -675,7 +653,8 @@ const styles = StyleSheet.create({
fontWeight: '600',
},
modalFooter: {
padding: 20,
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: 25,
},
saveButton: {