feat: 更新应用版本和主题设置

- 将应用版本更新至 1.0.3,修改相关配置文件
- 强制全局使用浅色主题,确保一致的用户体验
- 在训练计划功能中新增激活计划的 API 接口,支持用户激活训练计划
- 优化打卡功能,支持自动同步打卡记录至服务器
- 更新样式以适应新功能的展示和交互
This commit is contained in:
2025-08-14 22:23:45 +08:00
parent 56d4c7fd7f
commit 807e185761
21 changed files with 677 additions and 141 deletions

View File

@@ -2,9 +2,11 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { addExercise, syncCheckin } from '@/store/checkinSlice';
import { EXERCISE_LIBRARY, getCategories, searchExercises } from '@/utils/exerciseLibrary';
import { fetchExerciseConfig, normalizeToLibraryItems } from '@/services/exercises';
import { addExercise } from '@/store/checkinSlice';
import { EXERCISE_LIBRARY, getCategories } from '@/utils/exerciseLibrary';
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Haptics from 'expo-haptics';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useEffect, useMemo, useRef, useState } from 'react';
@@ -34,6 +36,8 @@ export default function SelectExerciseScreen() {
const [showCustomReps, setShowCustomReps] = useState(false);
const [customRepsInput, setCustomRepsInput] = useState('');
const [showCategoryPicker, setShowCategoryPicker] = useState(false);
const [serverLibrary, setServerLibrary] = useState<{ key: string; name: string; description: string; category: string }[] | null>(null);
const [serverCategories, setServerCategories] = useState<string[] | null>(null);
const controlsOpacity = useRef(new Animated.Value(0)).current;
@@ -42,8 +46,40 @@ export default function SelectExerciseScreen() {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}, []);
useEffect(() => {
let aborted = false;
const CACHE_KEY = '@exercise_config_v1';
(async () => {
try {
const cached = await AsyncStorage.getItem(CACHE_KEY);
if (cached && !aborted) {
const parsed = JSON.parse(cached);
const items = normalizeToLibraryItems(parsed);
if (items.length) {
setServerLibrary(items);
const cats = Array.from(new Set(items.map((i) => i.category)));
setServerCategories(cats);
}
}
} catch {}
try {
const resp = await fetchExerciseConfig();
console.log('fetchExerciseConfig', resp);
if (aborted) return;
const items = normalizeToLibraryItems(resp);
setServerLibrary(items);
const cats = Array.from(new Set(items.map((i) => i.category)));
setServerCategories(cats);
try { await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(resp)); } catch {}
} catch (err) {}
})();
return () => { aborted = true; };
}, []);
const categories = useMemo(() => ['全部', ...getCategories()], []);
const categories = useMemo(() => {
const base = serverCategories ?? getCategories();
return ['全部', ...base];
}, [serverCategories]);
const mainCategories = useMemo(() => {
const preferred = ['全部', '核心与腹部', '脊柱与后链', '侧链与髋', '平衡与支撑'];
const exists = (name: string) => categories.includes(name);
@@ -53,13 +89,17 @@ export default function SelectExerciseScreen() {
while (picked.length < 5 && rest.length) picked.push(rest.shift() as string);
return picked;
}, [categories]);
const library = useMemo(() => serverLibrary ?? EXERCISE_LIBRARY, [serverLibrary]);
const filtered = useMemo(() => {
const base = searchExercises(keyword);
const kw = keyword.trim().toLowerCase();
const base = kw
? library.filter((e) => e.name.toLowerCase().includes(kw) || (e.description || '').toLowerCase().includes(kw))
: library;
if (category === '全部') return base;
return base.filter((e) => e.category === category);
}, [keyword, category]);
}, [keyword, category, library]);
const selected = useMemo(() => EXERCISE_LIBRARY.find((e) => e.key === selectedKey) || null, [selectedKey]);
const selected = useMemo(() => library.find((e) => e.key === selectedKey) || null, [selectedKey, library]);
useEffect(() => {
Animated.timing(controlsOpacity, {
@@ -82,20 +122,7 @@ export default function SelectExerciseScreen() {
},
}));
console.log('addExercise', currentDate, selected.key, sets, reps);
// 同步到后端(读取最新 store 需要在返回后由首页触发 load或此处直接上报
// 简单做法:直接上报新增项(其余项由后端合并/覆盖)
dispatch(syncCheckin({
date: currentDate,
items: [
{
key: selected.key,
name: selected.name,
category: selected.category,
sets: Math.max(1, sets),
reps: reps && reps > 0 ? reps : undefined,
},
],
}));
// 自动同步将由中间件处理,无需手动调用 syncCheckin
router.back();
};