feat: 更新文章功能和相关依赖
- 新增文章详情页面,支持根据文章 ID 加载和展示文章内容 - 添加文章卡片组件,展示推荐文章的标题、封面和阅读量 - 更新文章服务,支持获取文章列表和根据 ID 获取文章详情 - 集成腾讯云 COS SDK,支持文件上传功能 - 优化打卡功能,支持按日期加载和展示打卡记录 - 更新相关依赖,确保项目兼容性和功能完整性 - 调整样式以适应新功能的展示和交互
This commit is contained in:
@@ -6,7 +6,7 @@ import { getTabBarBottomPadding } from '@/constants/TabBar';
|
|||||||
import { useAppSelector } from '@/hooks/redux';
|
import { useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||||
import { ensureHealthPermissions, fetchHealthDataForDate, fetchTodayHealthData } from '@/utils/health';
|
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
@@ -65,6 +65,11 @@ export default function ExploreScreen() {
|
|||||||
const [animToken, setAnimToken] = useState(0);
|
const [animToken, setAnimToken] = useState(0);
|
||||||
const [trainingProgress, setTrainingProgress] = useState(0.8); // 暂定静态80%
|
const [trainingProgress, setTrainingProgress] = useState(0.8); // 暂定静态80%
|
||||||
|
|
||||||
|
// 记录最近一次请求的“日期键”,避免旧请求覆盖新结果
|
||||||
|
const latestRequestKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const getDateKey = (d: Date) => `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
||||||
|
|
||||||
const loadHealthData = async (targetDate?: Date) => {
|
const loadHealthData = async (targetDate?: Date) => {
|
||||||
try {
|
try {
|
||||||
console.log('=== 开始HealthKit初始化流程 ===');
|
console.log('=== 开始HealthKit初始化流程 ===');
|
||||||
@@ -77,13 +82,23 @@ export default function ExploreScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('权限获取成功,开始获取健康数据...');
|
// 若未显式传入日期,按当前选中索引推导日期
|
||||||
const data = targetDate ? await fetchHealthDataForDate(targetDate) : await fetchTodayHealthData();
|
const derivedDate = targetDate ?? days[selectedIndex]?.date?.toDate() ?? new Date();
|
||||||
|
const requestKey = getDateKey(derivedDate);
|
||||||
|
latestRequestKeyRef.current = requestKey;
|
||||||
|
|
||||||
|
console.log('权限获取成功,开始获取健康数据...', derivedDate);
|
||||||
|
const data = await fetchHealthDataForDate(derivedDate);
|
||||||
|
|
||||||
console.log('设置UI状态:', data);
|
console.log('设置UI状态:', data);
|
||||||
|
// 仅当该请求仍是最新时,才应用结果
|
||||||
|
if (latestRequestKeyRef.current === requestKey) {
|
||||||
setStepCount(data.steps);
|
setStepCount(data.steps);
|
||||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||||
setAnimToken((t) => t + 1);
|
setAnimToken((t) => t + 1);
|
||||||
|
} else {
|
||||||
|
console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current);
|
||||||
|
}
|
||||||
console.log('=== HealthKit数据获取完成 ===');
|
console.log('=== HealthKit数据获取完成 ===');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -95,8 +110,9 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
|
// 聚焦时按当前选中的日期加载,避免与用户手动选择的日期不一致
|
||||||
loadHealthData();
|
loadHealthData();
|
||||||
}, [])
|
}, [selectedIndex])
|
||||||
);
|
);
|
||||||
|
|
||||||
// 日期点击时,加载对应日期数据
|
// 日期点击时,加载对应日期数据
|
||||||
@@ -147,7 +163,7 @@ export default function ExploreScreen() {
|
|||||||
|
|
||||||
{/* 打卡入口 */}
|
{/* 打卡入口 */}
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 24, marginBottom: 8 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 24, marginBottom: 8 }}>
|
||||||
<Text style={styles.sectionTitle}>今日报告</Text>
|
<Text style={styles.sectionTitle}>每日报告</Text>
|
||||||
<TouchableOpacity onPress={() => router.push('/checkin/calendar')} accessibilityRole="button">
|
<TouchableOpacity onPress={() => router.push('/checkin/calendar')} accessibilityRole="button">
|
||||||
<Text style={{ color: '#6B7280', fontWeight: '700' }}>查看打卡日历</Text>
|
<Text style={{ color: '#6B7280', fontWeight: '700' }}>查看打卡日历</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { ArticleCard } from '@/components/ArticleCard';
|
||||||
import { PlanCard } from '@/components/PlanCard';
|
import { PlanCard } from '@/components/PlanCard';
|
||||||
import { SearchBox } from '@/components/SearchBox';
|
import { SearchBox } from '@/components/SearchBox';
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
import { ThemedView } from '@/components/ThemedView';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { listRecommendedArticles } from '@/services/articles';
|
||||||
|
import { fetchRecommendations, RecommendationType } from '@/services/recommendations';
|
||||||
// Removed WorkoutCard import since we no longer use the horizontal carousel
|
// Removed WorkoutCard import since we no longer use the horizontal carousel
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { getChineseGreeting } from '@/utils/date';
|
import { getChineseGreeting } from '@/utils/date';
|
||||||
@@ -16,7 +19,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
@@ -72,6 +75,107 @@ export default function HomeScreen() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}), [coachSize.height, coachSize.width, insets.bottom, insets.top, pan, windowHeight, windowWidth, router]);
|
}), [coachSize.height, coachSize.width, insets.bottom, insets.top, pan, windowHeight, windowWidth, router]);
|
||||||
|
// 推荐项类型(本地 UI 使用)
|
||||||
|
type RecommendItem =
|
||||||
|
| {
|
||||||
|
type: 'plan';
|
||||||
|
key: string;
|
||||||
|
image: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
level?: '初学者' | '中级' | '高级';
|
||||||
|
progress: number;
|
||||||
|
onPress?: () => void;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'article';
|
||||||
|
key: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
coverImage: string;
|
||||||
|
publishedAt: string;
|
||||||
|
readCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打底数据(接口不可用时)
|
||||||
|
const getFallbackItems = React.useCallback((): RecommendItem[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'plan',
|
||||||
|
key: 'assess',
|
||||||
|
image:
|
||||||
|
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
|
||||||
|
title: '体态评估',
|
||||||
|
subtitle: '评估你的体态,制定训练计划',
|
||||||
|
level: '初学者',
|
||||||
|
progress: 0,
|
||||||
|
onPress: () => router.push('/ai-posture-assessment'),
|
||||||
|
},
|
||||||
|
...listRecommendedArticles().map((a) => ({
|
||||||
|
type: 'article' as const,
|
||||||
|
key: `article-${a.id}`,
|
||||||
|
id: a.id,
|
||||||
|
title: a.title,
|
||||||
|
coverImage: a.coverImage,
|
||||||
|
publishedAt: a.publishedAt,
|
||||||
|
readCount: a.readCount,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const [items, setItems] = React.useState<RecommendItem[]>(() => getFallbackItems());
|
||||||
|
|
||||||
|
// 拉取推荐接口(已登录时)
|
||||||
|
React.useEffect(() => {
|
||||||
|
let canceled = false;
|
||||||
|
async function load() {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
console.log('fetchRecommendations not logged in');
|
||||||
|
setItems(getFallbackItems());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const cards = await fetchRecommendations();
|
||||||
|
|
||||||
|
console.log('fetchRecommendations', cards);
|
||||||
|
if (canceled) return;
|
||||||
|
const mapped: RecommendItem[] = [];
|
||||||
|
for (const c of cards || []) {
|
||||||
|
if (c.type === RecommendationType.Article) {
|
||||||
|
const publishedAt = (c.extra && (c.extra.publishedDate || c.extra.published_at)) || new Date().toISOString();
|
||||||
|
const readCount = (c.extra && (c.extra.readCount ?? c.extra.read_count)) || 0;
|
||||||
|
mapped.push({
|
||||||
|
type: 'article',
|
||||||
|
key: c.id,
|
||||||
|
id: c.articleId || c.id,
|
||||||
|
title: c.title || '',
|
||||||
|
coverImage: c.coverUrl,
|
||||||
|
publishedAt,
|
||||||
|
readCount,
|
||||||
|
});
|
||||||
|
} else if (c.type === RecommendationType.Checkin) {
|
||||||
|
mapped.push({
|
||||||
|
type: 'plan',
|
||||||
|
key: c.id || 'checkin',
|
||||||
|
image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
|
||||||
|
title: c.title || '今日打卡',
|
||||||
|
subtitle: c.subtitle || '完成一次普拉提训练,记录你的坚持',
|
||||||
|
progress: 0,
|
||||||
|
onPress: () => pushIfAuthedElseLogin('/checkin'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 若接口返回空,也回退到打底
|
||||||
|
setItems(mapped.length > 0 ? mapped : getFallbackItems());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('fetchRecommendations error', e);
|
||||||
|
setItems(getFallbackItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
return () => { canceled = true; };
|
||||||
|
}, [isLoggedIn, pushIfAuthedElseLogin, getFallbackItems]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
<ThemedView style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<ThemedView style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
@@ -124,7 +228,7 @@ export default function HomeScreen() {
|
|||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<ThemedText style={styles.greeting}>{getChineseGreeting()} 🔥</ThemedText>
|
<ThemedText style={styles.greeting}>{getChineseGreeting()}</ThemedText>
|
||||||
<ThemedText style={styles.userName}>新学员,欢迎你</ThemedText>
|
<ThemedText style={styles.userName}>新学员,欢迎你</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -142,9 +246,6 @@ export default function HomeScreen() {
|
|||||||
>
|
>
|
||||||
<ThemedText style={styles.featureTitle}>AI体态评估</ThemedText>
|
<ThemedText style={styles.featureTitle}>AI体态评估</ThemedText>
|
||||||
<ThemedText style={styles.featureSubtitle}>3分钟获取体态报告</ThemedText>
|
<ThemedText style={styles.featureSubtitle}>3分钟获取体态报告</ThemedText>
|
||||||
<View style={styles.featureCta}>
|
|
||||||
<ThemedText style={styles.featureCtaText}>开始评估</ThemedText>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -153,9 +254,22 @@ export default function HomeScreen() {
|
|||||||
>
|
>
|
||||||
<ThemedText style={styles.featureTitle}>在线教练</ThemedText>
|
<ThemedText style={styles.featureTitle}>在线教练</ThemedText>
|
||||||
<ThemedText style={styles.featureSubtitle}>认证教练 · 1对1即时解答</ThemedText>
|
<ThemedText style={styles.featureSubtitle}>认证教练 · 1对1即时解答</ThemedText>
|
||||||
<View style={styles.featureCta}>
|
</Pressable>
|
||||||
<ThemedText style={styles.featureCtaText}>立即咨询</ThemedText>
|
|
||||||
</View>
|
<Pressable
|
||||||
|
style={[styles.featureCard, styles.featureCardTertiary]}
|
||||||
|
onPress={() => pushIfAuthedElseLogin('/checkin')}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.featureTitle}>每日打卡</ThemedText>
|
||||||
|
<ThemedText style={styles.featureSubtitle}>自选动作 · 记录完成</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
style={[styles.featureCard, styles.featureCardQuaternary]}
|
||||||
|
onPress={() => pushIfAuthedElseLogin('/training-plan')}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.featureTitle}>训练计划制定</ThemedText>
|
||||||
|
<ThemedText style={styles.featureSubtitle}>按周安排 · 个性化目标</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -165,40 +279,36 @@ export default function HomeScreen() {
|
|||||||
<ThemedText style={styles.sectionTitle}>为你推荐</ThemedText>
|
<ThemedText style={styles.sectionTitle}>为你推荐</ThemedText>
|
||||||
|
|
||||||
<View style={styles.planList}>
|
<View style={styles.planList}>
|
||||||
<PlanCard
|
{items.map((item) => {
|
||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg'}
|
if (item.type === 'article') {
|
||||||
title="体态评估"
|
return (
|
||||||
subtitle="评估你的体态,制定训练计划"
|
<ArticleCard
|
||||||
level="初学者"
|
key={item.key}
|
||||||
progress={0}
|
id={item.id}
|
||||||
|
title={item.title}
|
||||||
|
coverImage={item.coverImage}
|
||||||
|
publishedAt={item.publishedAt}
|
||||||
|
readCount={item.readCount}
|
||||||
/>
|
/>
|
||||||
{/* 原“每周打卡”改为进入打卡日历 */}
|
);
|
||||||
<Pressable onPress={() => router.push('/checkin/calendar')}>
|
}
|
||||||
|
const card = (
|
||||||
<PlanCard
|
<PlanCard
|
||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/Image30play@2x.png'}
|
image={item.image}
|
||||||
title="打卡日历"
|
title={item.title}
|
||||||
subtitle="查看每日打卡记录(点亮日期)"
|
subtitle={item.subtitle}
|
||||||
progress={0.75}
|
level={item.level}
|
||||||
/>
|
progress={item.progress}
|
||||||
</Pressable>
|
|
||||||
<Pressable onPress={() => pushIfAuthedElseLogin('/training-plan')}>
|
|
||||||
<PlanCard
|
|
||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/Image30play@2x.png'}
|
|
||||||
title="训练计划制定"
|
|
||||||
subtitle="按周安排/次数·常见目标·个性化选项"
|
|
||||||
level="初学者"
|
|
||||||
progress={0}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable onPress={() => pushIfAuthedElseLogin('/checkin')}>
|
|
||||||
<PlanCard
|
|
||||||
image={'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/ImageCheck.jpeg'}
|
|
||||||
title="每日打卡(自选动作)"
|
|
||||||
subtitle="选择动作,设置组数/次数,记录完成"
|
|
||||||
level="初学者"
|
|
||||||
progress={0}
|
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
return item.onPress ? (
|
||||||
|
<Pressable key={item.key} onPress={item.onPress}>
|
||||||
|
{card}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
) : (
|
||||||
|
<View key={item.key}>{card}</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -299,19 +409,20 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
featureCard: {
|
featureCard: {
|
||||||
width: '48%',
|
width: '48%',
|
||||||
borderRadius: 16,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 12,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
// iOS shadow
|
marginBottom: 12,
|
||||||
|
// 轻量阴影,减少臃肿感
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.04,
|
||||||
shadowRadius: 12,
|
shadowRadius: 8,
|
||||||
shadowOffset: { width: 0, height: 6 },
|
shadowOffset: { width: 0, height: 4 },
|
||||||
// Android shadow
|
elevation: 2,
|
||||||
elevation: 4,
|
|
||||||
},
|
},
|
||||||
featureCardPrimary: {
|
featureCardPrimary: {
|
||||||
backgroundColor: '#EEF2FF', // 柔和的靛蓝背景
|
backgroundColor: '#EEF2FF', // 柔和的靛蓝背景
|
||||||
@@ -319,33 +430,26 @@ const styles = StyleSheet.create({
|
|||||||
featureCardSecondary: {
|
featureCardSecondary: {
|
||||||
backgroundColor: '#F0FDFA', // 柔和的青绿背景
|
backgroundColor: '#F0FDFA', // 柔和的青绿背景
|
||||||
},
|
},
|
||||||
|
featureCardTertiary: {
|
||||||
|
backgroundColor: '#FFF7ED', // 柔和的橙色背景
|
||||||
|
},
|
||||||
|
featureCardQuaternary: {
|
||||||
|
backgroundColor: '#F5F3FF', // 柔和的紫色背景
|
||||||
|
},
|
||||||
featureIcon: {
|
featureIcon: {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
featureTitle: {
|
featureTitle: {
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#0F172A',
|
color: '#0F172A',
|
||||||
marginBottom: 6,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
featureSubtitle: {
|
featureSubtitle: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
color: '#6B7280',
|
color: '#6B7280',
|
||||||
lineHeight: 16,
|
lineHeight: 15,
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
featureCta: {
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
backgroundColor: '#0F172A',
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingVertical: 6,
|
|
||||||
borderRadius: 999,
|
|
||||||
},
|
|
||||||
featureCtaText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
},
|
||||||
planList: {
|
planList: {
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||||
|
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
KeyboardAvoidingView,
|
Keyboard,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@@ -17,17 +18,22 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import Markdown from 'react-native-markdown-display';
|
||||||
import Animated, { FadeInDown, FadeInUp, Layout } from 'react-native-reanimated';
|
import Animated, { FadeInDown, FadeInUp, Layout } from 'react-native-reanimated';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppSelector } from '@/hooks/redux';
|
import { buildCosKey, buildPublicUrl } from '@/constants/Cos';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
||||||
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
import { api, getAuthToken, postTextStream } from '@/services/api';
|
import { api, getAuthToken, postTextStream } from '@/services/api';
|
||||||
|
import { uploadWithRetry } from '@/services/cos';
|
||||||
|
import { updateUser as updateUserApi } from '@/services/users';
|
||||||
import type { CheckinRecord } from '@/store/checkinSlice';
|
import type { CheckinRecord } from '@/store/checkinSlice';
|
||||||
|
import { fetchMyProfile, updateProfile } from '@/store/userSlice';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
type Role = 'user' | 'assistant';
|
type Role = 'user' | 'assistant';
|
||||||
@@ -67,14 +73,30 @@ export default function AICoachChatScreen() {
|
|||||||
const didInitialScrollRef = useRef(false);
|
const didInitialScrollRef = useRef(false);
|
||||||
const [composerHeight, setComposerHeight] = useState<number>(80);
|
const [composerHeight, setComposerHeight] = useState<number>(80);
|
||||||
const shouldAutoScrollRef = useRef(false);
|
const shouldAutoScrollRef = useRef(false);
|
||||||
|
const [keyboardOffset, setKeyboardOffset] = useState(0);
|
||||||
|
const pendingAssistantIdRef = useRef<string | null>(null);
|
||||||
|
const [selectedImages, setSelectedImages] = useState<Array<{
|
||||||
|
id: string;
|
||||||
|
localUri: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
progress: number;
|
||||||
|
uploadedKey?: string;
|
||||||
|
uploadedUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
}>>([]);
|
||||||
|
const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
|
||||||
|
|
||||||
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
||||||
const checkin = useAppSelector((s) => (s as any).checkin);
|
const checkin = useAppSelector((s) => (s as any).checkin);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const userProfile = useAppSelector((s) => (s as any)?.user?.profile);
|
||||||
|
|
||||||
const chips = useMemo(() => [
|
const chips = useMemo(() => [
|
||||||
{ key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
{ key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
||||||
{ key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() },
|
{ key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() },
|
||||||
{ key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
|
{ key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
|
||||||
|
{ key: 'weight', label: '记体重', action: () => insertWeightInputCard() },
|
||||||
], [router, planDraft, checkin]);
|
], [router, planDraft, checkin]);
|
||||||
|
|
||||||
const scrollToEnd = useCallback(() => {
|
const scrollToEnd = useCallback(() => {
|
||||||
@@ -132,6 +154,29 @@ export default function AICoachChatScreen() {
|
|||||||
}
|
}
|
||||||
}, [composerHeight, isAtBottom, scrollToEnd]);
|
}, [composerHeight, isAtBottom, scrollToEnd]);
|
||||||
|
|
||||||
|
// 键盘事件:在键盘弹出时,将输入区与悬浮按钮一起上移,避免遮挡
|
||||||
|
useEffect(() => {
|
||||||
|
let showSub: any = null;
|
||||||
|
let hideSub: any = null;
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
showSub = Keyboard.addListener('keyboardWillChangeFrame', (e: any) => {
|
||||||
|
try {
|
||||||
|
const height = Math.max(0, (e.endCoordinates?.height ?? 0) - insets.bottom);
|
||||||
|
setKeyboardOffset(height);
|
||||||
|
} catch { setKeyboardOffset(0); }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showSub = Keyboard.addListener('keyboardDidShow', (e: any) => {
|
||||||
|
try { setKeyboardOffset(Math.max(0, e.endCoordinates?.height ?? 0)); } catch { setKeyboardOffset(0); }
|
||||||
|
});
|
||||||
|
hideSub = Keyboard.addListener('keyboardDidHide', () => setKeyboardOffset(0));
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
try { showSub?.remove?.(); } catch { }
|
||||||
|
try { hideSub?.remove?.(); } catch { }
|
||||||
|
};
|
||||||
|
}, [insets.bottom]);
|
||||||
|
|
||||||
const streamAbortRef = useRef<{ abort: () => void } | null>(null);
|
const streamAbortRef = useRef<{ abort: () => void } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -246,6 +291,7 @@ export default function AICoachChatScreen() {
|
|||||||
const userMsg: ChatMessage = { id: userMsgId, role: 'user', content: text };
|
const userMsg: ChatMessage = { id: userMsgId, role: 'user', content: text };
|
||||||
shouldAutoScrollRef.current = isAtBottom;
|
shouldAutoScrollRef.current = isAtBottom;
|
||||||
setMessages((m) => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
setMessages((m) => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
||||||
|
pendingAssistantIdRef.current = assistantId;
|
||||||
|
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
@@ -281,6 +327,7 @@ export default function AICoachChatScreen() {
|
|||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
streamAbortRef.current = null;
|
streamAbortRef.current = null;
|
||||||
if (cidFromHeader && !conversationId) setConversationId(cidFromHeader);
|
if (cidFromHeader && !conversationId) setConversationId(cidFromHeader);
|
||||||
|
pendingAssistantIdRef.current = null;
|
||||||
try { console.log('[AI_CHAT][api] end', { cidFromHeader, hadChunks: receivedAnyChunk }); } catch { }
|
try { console.log('[AI_CHAT][api] end', { cidFromHeader, hadChunks: receivedAnyChunk }); } catch { }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,6 +336,7 @@ export default function AICoachChatScreen() {
|
|||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
streamAbortRef.current = null;
|
streamAbortRef.current = null;
|
||||||
|
pendingAssistantIdRef.current = null;
|
||||||
// 流式失败时的降级:尝试一次性非流式
|
// 流式失败时的降级:尝试一次性非流式
|
||||||
try {
|
try {
|
||||||
const bodyNoStream = { ...body, stream: false };
|
const bodyNoStream = { ...body, stream: false };
|
||||||
@@ -314,10 +362,59 @@ export default function AICoachChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function send(text: string) {
|
async function send(text: string) {
|
||||||
if (!text.trim() || isSending) return;
|
if (isSending) return;
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed && selectedImages.length === 0) return;
|
||||||
|
|
||||||
|
async function ensureImagesUploaded(): Promise<string[]> {
|
||||||
|
const urls: string[] = [];
|
||||||
|
for (const img of selectedImages) {
|
||||||
|
if (img.uploadedUrl) {
|
||||||
|
urls.push(img.uploadedUrl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resp = await fetch(img.localUri);
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const ext = (() => {
|
||||||
|
const t = (blob.type || '').toLowerCase();
|
||||||
|
if (t.includes('png')) return 'png';
|
||||||
|
if (t.includes('webp')) return 'webp';
|
||||||
|
if (t.includes('heic')) return 'heic';
|
||||||
|
if (t.includes('heif')) return 'heif';
|
||||||
|
return 'jpg';
|
||||||
|
})();
|
||||||
|
const key = buildCosKey({ prefix: 'images/chat', ext });
|
||||||
|
const res = await uploadWithRetry({
|
||||||
|
key,
|
||||||
|
body: blob,
|
||||||
|
contentType: blob.type || 'image/jpeg',
|
||||||
|
onProgress: ({ percent }: { percent?: number }) => {
|
||||||
|
const p = typeof percent === 'number' ? percent : 0;
|
||||||
|
setSelectedImages((prev) => prev.map((it) => it.id === img.id ? { ...it, progress: p } : it));
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
const url = buildPublicUrl(res.key);
|
||||||
|
urls.push(url);
|
||||||
|
setSelectedImages((prev) => prev.map((it) => it.id === img.id ? { ...it, uploadedKey: res.key, uploadedUrl: url, progress: 1 } : it));
|
||||||
|
} catch (e: any) {
|
||||||
|
setSelectedImages((prev) => prev.map((it) => it.id === img.id ? { ...it, error: e?.message || '上传失败' } : it));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urls = await ensureImagesUploaded();
|
||||||
|
const mdImages = urls.map((u) => ``).join('\n\n');
|
||||||
|
const composed = [trimmed, mdImages].filter(Boolean).join('\n\n');
|
||||||
setInput('');
|
setInput('');
|
||||||
await sendStream(trimmed);
|
setSelectedImages([]);
|
||||||
|
await sendStream(composed);
|
||||||
|
} catch (e: any) {
|
||||||
|
Alert.alert('上传失败', e?.message || '图片上传失败,请稍后重试');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQuickPlan() {
|
function handleQuickPlan() {
|
||||||
@@ -378,6 +475,37 @@ export default function AICoachChatScreen() {
|
|||||||
send(prompt);
|
send(prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pickImages = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
allowsMultipleSelection: true,
|
||||||
|
selectionLimit: 4,
|
||||||
|
quality: 0.9,
|
||||||
|
} as any);
|
||||||
|
if ((result as any).canceled) return;
|
||||||
|
const assets = (result as any).assets || [];
|
||||||
|
const next = assets.map((a: any) => ({
|
||||||
|
id: `${a.assetId || a.fileName || a.uri}_${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
localUri: a.uri,
|
||||||
|
width: a.width,
|
||||||
|
height: a.height,
|
||||||
|
progress: 0,
|
||||||
|
}));
|
||||||
|
setSelectedImages((prev) => {
|
||||||
|
const merged = [...prev, ...next];
|
||||||
|
return merged.slice(0, 4);
|
||||||
|
});
|
||||||
|
setTimeout(scrollToEnd, 0);
|
||||||
|
} catch (e: any) {
|
||||||
|
Alert.alert('错误', e?.message || '选择图片失败');
|
||||||
|
}
|
||||||
|
}, [scrollToEnd]);
|
||||||
|
|
||||||
|
const removeSelectedImage = useCallback((id: string) => {
|
||||||
|
setSelectedImages((prev) => prev.filter((it) => it.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
function renderItem({ item }: { item: ChatMessage }) {
|
function renderItem({ item }: { item: ChatMessage }) {
|
||||||
const isUser = item.role === 'user';
|
const isUser = item.role === 'user';
|
||||||
return (
|
return (
|
||||||
@@ -399,12 +527,88 @@ export default function AICoachChatScreen() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.bubbleText, { color: isUser ? theme.onPrimary : '#192126' }]}>{item.content}</Text>
|
{renderBubbleContent(item)}
|
||||||
</View>
|
</View>
|
||||||
|
{false}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBubbleContent(item: ChatMessage) {
|
||||||
|
if (!item.content?.trim() && isStreaming && pendingAssistantIdRef.current === item.id) {
|
||||||
|
return <Text style={[styles.bubbleText, { color: '#687076' }]}>正在思考…</Text>;
|
||||||
|
}
|
||||||
|
if (item.content?.startsWith('__WEIGHT_INPUT_CARD__')) {
|
||||||
|
const preset = (() => {
|
||||||
|
const m = item.content.split('\n')?.[1];
|
||||||
|
const v = parseFloat(m || '');
|
||||||
|
return isNaN(v) ? '' : String(v);
|
||||||
|
})();
|
||||||
|
return (
|
||||||
|
<View style={{ gap: 8 }}>
|
||||||
|
<Text style={[styles.bubbleText, { color: '#192126', fontWeight: '700' }]}>记录今日体重</Text>
|
||||||
|
<View style={styles.weightRow}>
|
||||||
|
<TextInput
|
||||||
|
placeholder="例如 60.5"
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
defaultValue={preset}
|
||||||
|
placeholderTextColor={'#687076'}
|
||||||
|
style={styles.weightInput}
|
||||||
|
onSubmitEditing={(e) => handleSubmitWeight(e.nativeEvent.text)}
|
||||||
|
returnKeyType="done"
|
||||||
|
blurOnSubmit
|
||||||
|
/>
|
||||||
|
<Text style={styles.weightUnit}>kg</Text>
|
||||||
|
<TouchableOpacity accessibilityRole="button" style={styles.weightSaveBtn} onPress={() => handleSubmitWeight((preset || '').toString())}>
|
||||||
|
<Text style={{ color: '#192126', fontWeight: '700' }}>保存</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<Text style={{ color: '#687076', fontSize: 12 }}>按回车或点击保存,即可将该体重同步到账户并发送到对话。</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Markdown style={markdownStyles} mergeStyle>
|
||||||
|
{item.content}
|
||||||
|
</Markdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertWeightInputCard() {
|
||||||
|
const id = `wcard_${Date.now()}`;
|
||||||
|
const preset = userProfile?.weight ? Number(userProfile.weight) : undefined;
|
||||||
|
const payload = `__WEIGHT_INPUT_CARD__\n${preset ?? ''}`;
|
||||||
|
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
||||||
|
setTimeout(scrollToEnd, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitWeight(text?: string) {
|
||||||
|
const val = parseFloat(String(text ?? '').trim());
|
||||||
|
if (isNaN(val) || val <= 0 || val > 500) {
|
||||||
|
Alert.alert('请输入有效体重', '请填写合理的公斤数,例如 60.5');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 本地更新
|
||||||
|
dispatch(updateProfile({ weight: val }));
|
||||||
|
// 后端同步(若有 userId 则更稳妥;后端实现容错)
|
||||||
|
try {
|
||||||
|
const userId = (userProfile as any)?.userId || (userProfile as any)?.id || (userProfile as any)?._id;
|
||||||
|
if (userId) {
|
||||||
|
await updateUserApi({ userId, weight: val });
|
||||||
|
await dispatch(fetchMyProfile() as any);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 不阻断对话体验
|
||||||
|
}
|
||||||
|
// 在对话中插入“确认消息”并发送给教练
|
||||||
|
const textMsg = `我记录了今日体重:${val} kg。请基于这一变化给出训练/营养建议。`;
|
||||||
|
await send(textMsg);
|
||||||
|
} catch (e: any) {
|
||||||
|
Alert.alert('保存失败', e?.message || '请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.screen, { backgroundColor: theme.background }]}>
|
<View style={[styles.screen, { backgroundColor: theme.background }]}>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
@@ -434,7 +638,7 @@ export default function AICoachChatScreen() {
|
|||||||
}}
|
}}
|
||||||
contentContainerStyle={{ paddingHorizontal: 14, paddingTop: 8 }}
|
contentContainerStyle={{ paddingHorizontal: 14, paddingTop: 8 }}
|
||||||
ListFooterComponent={() => (
|
ListFooterComponent={() => (
|
||||||
<View style={{ height: insets.bottom + composerHeight + (isAtBottom ? 0 : 56) + 16 }} />
|
<View style={{ height: insets.bottom + keyboardOffset + composerHeight + (isAtBottom ? 0 : 56) + 16 }} />
|
||||||
)}
|
)}
|
||||||
onContentSizeChange={() => {
|
onContentSizeChange={() => {
|
||||||
// 首次内容变化强制滚底,其余仅在接近底部时滚动
|
// 首次内容变化强制滚底,其余仅在接近底部时滚动
|
||||||
@@ -454,25 +658,63 @@ export default function AICoachChatScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={insets.top}>
|
|
||||||
<BlurView
|
<BlurView
|
||||||
intensity={18}
|
intensity={18}
|
||||||
tint={'light'}
|
tint={'light'}
|
||||||
style={[styles.composerWrap, { paddingBottom: insets.bottom + 10 }]}
|
style={[styles.composerWrap, { paddingBottom: insets.bottom + 10, bottom: keyboardOffset }]}
|
||||||
onLayout={(e) => {
|
onLayout={(e) => {
|
||||||
const h = e.nativeEvent.layout.height;
|
const h = e.nativeEvent.layout.height;
|
||||||
if (h && Math.abs(h - composerHeight) > 0.5) setComposerHeight(h);
|
if (h && Math.abs(h - composerHeight) > 0.5) setComposerHeight(h);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={styles.chipsRow}>
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
decelerationRate="fast"
|
||||||
|
snapToAlignment="start"
|
||||||
|
style={styles.chipsRowScroll}
|
||||||
|
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
|
||||||
|
>
|
||||||
{chips.map((c) => (
|
{chips.map((c) => (
|
||||||
<TouchableOpacity key={c.key} style={[styles.chip, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.12)' }]} onPress={c.action}>
|
<TouchableOpacity key={c.key} style={[styles.chip, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.12)' }]} onPress={c.action}>
|
||||||
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text>
|
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{!!selectedImages.length && (
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
style={styles.imagesRow}
|
||||||
|
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
|
||||||
|
>
|
||||||
|
{selectedImages.map((img) => (
|
||||||
|
<View key={img.id} style={styles.imageThumbWrap}>
|
||||||
|
<TouchableOpacity accessibilityRole="imagebutton" onPress={() => setPreviewImageUri(img.uploadedUrl || img.localUri)}>
|
||||||
|
<Image source={{ uri: img.uploadedUrl || img.localUri }} style={styles.imageThumb} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
{!!(img.progress > 0 && img.progress < 1) && (
|
||||||
|
<View style={styles.imageProgressOverlay}>
|
||||||
|
<Text style={styles.imageProgressText}>{Math.round((img.progress || 0) * 100)}%</Text>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
<TouchableOpacity accessibilityRole="button" onPress={() => removeSelectedImage(img.id)} style={styles.imageRemoveBtn}>
|
||||||
|
<Ionicons name="close" size={12} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={[styles.inputRow, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.08)' }]}>
|
<View style={[styles.inputRow, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.08)' }]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={pickImages}
|
||||||
|
style={[styles.mediaBtn, { backgroundColor: 'rgba(187,242,70,0.16)' }]}
|
||||||
|
>
|
||||||
|
<Ionicons name="image-outline" size={18} color={'#192126'} />
|
||||||
|
</TouchableOpacity>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="问我任何与普拉提相关的问题..."
|
placeholder="问我任何与普拉提相关的问题..."
|
||||||
placeholderTextColor={theme.textMuted}
|
placeholderTextColor={theme.textMuted}
|
||||||
@@ -485,11 +727,11 @@ export default function AICoachChatScreen() {
|
|||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
disabled={!input.trim() || isSending}
|
disabled={(!input.trim() && selectedImages.length === 0) || isSending}
|
||||||
onPress={() => send(input)}
|
onPress={() => send(input)}
|
||||||
style={[
|
style={[
|
||||||
styles.sendBtn,
|
styles.sendBtn,
|
||||||
{ backgroundColor: theme.primary, opacity: input.trim() && !isSending ? 1 : 0.5 }
|
{ backgroundColor: theme.primary, opacity: (input.trim() || selectedImages.length > 0) && !isSending ? 1 : 0.5 }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isSending ? (
|
{isSending ? (
|
||||||
@@ -500,7 +742,6 @@ export default function AICoachChatScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</KeyboardAvoidingView>
|
|
||||||
|
|
||||||
{!isAtBottom && (
|
{!isAtBottom && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -558,6 +799,15 @@ export default function AICoachChatScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal transparent visible={!!previewImageUri} animationType="fade" onRequestClose={() => setPreviewImageUri(null)}>
|
||||||
|
<TouchableOpacity activeOpacity={1} style={styles.previewBackdrop} onPress={() => setPreviewImageUri(null)}>
|
||||||
|
<View style={styles.previewBox}>
|
||||||
|
{previewImageUri ? (
|
||||||
|
<Image source={{ uri: previewImageUri }} style={styles.previewImage} resizeMode="contain" />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -613,6 +863,34 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
},
|
},
|
||||||
|
weightRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
weightInput: {
|
||||||
|
flex: 1,
|
||||||
|
height: 36,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(0,0,0,0.08)',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||||
|
color: '#192126',
|
||||||
|
},
|
||||||
|
weightUnit: {
|
||||||
|
color: '#192126',
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
weightSaveBtn: {
|
||||||
|
height: 36,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(187,242,70,0.6)'
|
||||||
|
},
|
||||||
|
// markdown 基础样式承载容器的字体尺寸保持与气泡一致
|
||||||
composerWrap: {
|
composerWrap: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -624,11 +902,13 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
chipsRow: {
|
chipsRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 8,
|
gap: 8,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
chipsRowScroll: {
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
chip: {
|
chip: {
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
height: 34,
|
height: 34,
|
||||||
@@ -642,6 +922,47 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
imagesRow: {
|
||||||
|
maxHeight: 92,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
imageThumbWrap: {
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.06)'
|
||||||
|
},
|
||||||
|
imageThumb: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
imageRemoveBtn: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 4,
|
||||||
|
top: 4,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.45)'
|
||||||
|
},
|
||||||
|
imageProgressOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
imageProgressText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '700'
|
||||||
|
},
|
||||||
inputRow: {
|
inputRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -650,6 +971,14 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
backgroundColor: 'rgba(0,0,0,0.04)'
|
backgroundColor: 'rgba(0,0,0,0.04)'
|
||||||
},
|
},
|
||||||
|
mediaBtn: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
input: {
|
input: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
@@ -748,6 +1077,66 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
backgroundColor: 'rgba(0,0,0,0.06)'
|
backgroundColor: 'rgba(0,0,0,0.06)'
|
||||||
},
|
},
|
||||||
|
previewBackdrop: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
previewBox: {
|
||||||
|
width: '100%',
|
||||||
|
height: '80%',
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
previewImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const markdownStyles = {
|
||||||
|
body: {
|
||||||
|
color: '#192126',
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
marginTop: 2,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
bullet_list: {
|
||||||
|
marginVertical: 4,
|
||||||
|
},
|
||||||
|
ordered_list: {
|
||||||
|
marginVertical: 4,
|
||||||
|
},
|
||||||
|
list_item: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
code_inline: {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
paddingVertical: 2,
|
||||||
|
},
|
||||||
|
code_block: {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 6,
|
||||||
|
},
|
||||||
|
fence: {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 6,
|
||||||
|
},
|
||||||
|
heading1: { fontSize: 20, fontWeight: '800', marginVertical: 6 },
|
||||||
|
heading2: { fontSize: 18, fontWeight: '800', marginVertical: 6 },
|
||||||
|
heading3: { fontSize: 16, fontWeight: '800', marginVertical: 6 },
|
||||||
|
link: { color: '#246BFD' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
126
app/article/[id].tsx
Normal file
126
app/article/[id].tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { Article, getArticleById } from '@/services/articles';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { ScrollView, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
|
||||||
|
import RenderHTML from 'react-native-render-html';
|
||||||
|
|
||||||
|
export default function ArticleDetailScreen() {
|
||||||
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const router = useRouter();
|
||||||
|
const [article, setArticle] = useState<Article | undefined>(undefined);
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
|
const colorScheme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
|
const theme = Colors[colorScheme];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
getArticleById(id).then((article) => {
|
||||||
|
console.log('article', article);
|
||||||
|
setArticle(article);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<HeaderBar title="文章" onBack={() => router.back()} showBottomBorder />
|
||||||
|
<View style={{ padding: 24 }}>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = { html: wrapHtml(article.htmlContent) };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, backgroundColor: theme.surface }}>
|
||||||
|
<HeaderBar title="文章" onBack={() => router.back()} showBottomBorder />
|
||||||
|
<ScrollView contentContainerStyle={styles.contentContainer} showsVerticalScrollIndicator={false}>
|
||||||
|
<View style={styles.headerMeta}>
|
||||||
|
<Text style={[styles.title, { color: theme.text }]}>{article.title}</Text>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={[styles.metaText, { color: theme.textMuted }]}>{dayjs(article.publishedAt).format('YYYY-MM-DD')}</Text>
|
||||||
|
<Text style={[styles.metaText, styles.dot]}>·</Text>
|
||||||
|
<Text style={[styles.metaText, { color: theme.textMuted }]}>{article.readCount} 阅读</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<RenderHTML
|
||||||
|
contentWidth={width - 48}
|
||||||
|
source={source}
|
||||||
|
baseStyle={{ ...htmlBaseStyles, color: theme.text }}
|
||||||
|
tagsStyles={htmlTagStyles}
|
||||||
|
enableExperimentalMarginCollapsing={true}
|
||||||
|
/>
|
||||||
|
<View style={{ height: 36 }} />
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapHtml(inner: string) {
|
||||||
|
// 为了统一排版与图片自适应
|
||||||
|
return `
|
||||||
|
<div class="article">
|
||||||
|
${inner}
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.article img { max-width: 100%; height: auto; border-radius: 12px; }
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
contentContainer: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingTop: 12,
|
||||||
|
},
|
||||||
|
headerMeta: {
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: '#192126',
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
metaText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#8A8A8E',
|
||||||
|
},
|
||||||
|
dot: {
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const htmlBaseStyles = {
|
||||||
|
color: '#192126',
|
||||||
|
lineHeight: 24,
|
||||||
|
fontSize: 16,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const htmlTagStyles = {
|
||||||
|
h1: { fontSize: 26, fontWeight: '800', marginBottom: 8 },
|
||||||
|
h2: { fontSize: 22, fontWeight: '800', marginTop: 8, marginBottom: 8 },
|
||||||
|
h3: { fontSize: 18, fontWeight: '700', marginTop: 12, marginBottom: 6 },
|
||||||
|
p: { marginBottom: 12 },
|
||||||
|
ol: { marginBottom: 12, paddingLeft: 18 },
|
||||||
|
ul: { marginBottom: 12, paddingLeft: 18 },
|
||||||
|
li: { marginBottom: 6 },
|
||||||
|
img: { marginTop: 8, marginBottom: 8, borderRadius: 12 },
|
||||||
|
em: { fontStyle: 'italic' },
|
||||||
|
strong: { fontWeight: '800' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { DailyStatusItem, fetchDailyStatusRange } from '@/services/checkins';
|
import { DailyStatusItem, fetchDailyStatusRange } from '@/services/checkins';
|
||||||
import { getDailyCheckins, loadMonthCheckins, setCurrentDate } from '@/store/checkinSlice';
|
import { loadMonthCheckins } from '@/store/checkinSlice';
|
||||||
import { getMonthDaysZh } from '@/utils/date';
|
import { getMonthDaysZh } from '@/utils/date';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
@@ -76,9 +76,8 @@ export default function CheckinCalendarScreen() {
|
|||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
dispatch(setCurrentDate(dateStr));
|
// 通过路由参数传入日期,便于目标页初始化
|
||||||
await dispatch(getDailyCheckins(dateStr));
|
router.push({ pathname: '/checkin', params: { date: dateStr } });
|
||||||
router.push('/checkin');
|
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
style={[styles.dayCell, { backgroundColor: colorTokens.card }, hasAny && styles.dayCellCompleted, isToday && styles.dayCellToday]}
|
style={[styles.dayCell, { backgroundColor: colorTokens.card }, hasAny && styles.dayCellCompleted, isToday && styles.dayCellToday]}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import type { CheckinExercise } from '@/store/checkinSlice';
|
import type { CheckinExercise } from '@/store/checkinSlice';
|
||||||
import { getDailyCheckins, loadMonthCheckins, removeExercise, setCurrentDate, syncCheckin, toggleExerciseCompleted } from '@/store/checkinSlice';
|
import { getDailyCheckins, removeExercise, replaceExercises, setCurrentDate, syncCheckin, toggleExerciseCompleted } from '@/store/checkinSlice';
|
||||||
|
import { buildClassicalSession } from '@/utils/classicalSession';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Alert, FlatList, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, FlatList, Modal, SafeAreaView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
function formatDate(d: Date) {
|
function formatDate(d: Date) {
|
||||||
const y = d.getFullYear();
|
const y = d.getFullYear();
|
||||||
@@ -20,33 +21,65 @@ function formatDate(d: Date) {
|
|||||||
export default function CheckinHome() {
|
export default function CheckinHome() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const params = useLocalSearchParams<{ date?: string }>();
|
||||||
const today = useMemo(() => formatDate(new Date()), []);
|
const today = useMemo(() => formatDate(new Date()), []);
|
||||||
const checkin = useAppSelector((s) => (s as any).checkin);
|
const checkin = useAppSelector((s) => (s as any).checkin);
|
||||||
const record = checkin?.byDate?.[today];
|
const routeDateParam = typeof params?.date === 'string' && params.date ? params.date : undefined;
|
||||||
|
const currentDate: string = routeDateParam || (checkin?.currentDate as string) || today;
|
||||||
|
const record = checkin?.byDate?.[currentDate] as (undefined | { items?: CheckinExercise[]; note?: string; raw?: any[] });
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
|
||||||
|
console.log('CheckinHome render', {
|
||||||
|
currentDate,
|
||||||
|
routeDateParam,
|
||||||
|
itemsCount: record?.items?.length || 0,
|
||||||
|
rawCount: (record as any)?.raw?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastFetchedRef = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setCurrentDate(today));
|
// 初始化当前日期:路由参数优先,其次 store,最后今天
|
||||||
// 进入页面立即从后端获取当天打卡列表,回填本地
|
if (currentDate && checkin?.currentDate !== currentDate) {
|
||||||
dispatch(getDailyCheckins(today)).unwrap().catch((err: any) => {
|
dispatch(setCurrentDate(currentDate));
|
||||||
|
}
|
||||||
|
// 仅当切换日期时获取一次,避免重复请求
|
||||||
|
if (currentDate && lastFetchedRef.current !== currentDate) {
|
||||||
|
lastFetchedRef.current = currentDate;
|
||||||
|
dispatch(getDailyCheckins(currentDate)).unwrap().catch((err: any) => {
|
||||||
Alert.alert('获取打卡失败', err?.message || '请稍后重试');
|
Alert.alert('获取打卡失败', err?.message || '请稍后重试');
|
||||||
});
|
});
|
||||||
// 预取本月数据(用于日历视图点亮)
|
}
|
||||||
const now = new Date();
|
}, [dispatch, currentDate]);
|
||||||
dispatch(loadMonthCheckins({ year: now.getFullYear(), month1Based: now.getMonth() + 1 }));
|
|
||||||
}, [dispatch, today]);
|
|
||||||
|
|
||||||
|
const lastSyncSigRef = useRef<string>('');
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
// 返回本页时确保与后端同步(若本地有内容则上报,后台 upsert)
|
// 仅当本地条目发生变更时才上报,避免反复刷写
|
||||||
if (record?.items && Array.isArray(record.items)) {
|
const sig = JSON.stringify(record?.items || []);
|
||||||
dispatch(syncCheckin({ date: today, items: record.items as CheckinExercise[], note: record?.note }));
|
if (record?.items && Array.isArray(record.items) && sig !== lastSyncSigRef.current) {
|
||||||
|
lastSyncSigRef.current = sig;
|
||||||
|
dispatch(syncCheckin({ date: currentDate, items: record.items as CheckinExercise[], note: record?.note }));
|
||||||
}
|
}
|
||||||
return () => { };
|
return () => { };
|
||||||
}, [dispatch, today, record?.items])
|
}, [dispatch, currentDate, record?.items, record?.note])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [genVisible, setGenVisible] = useState(false);
|
||||||
|
const [genLevel, setGenLevel] = useState<'beginner' | 'intermediate' | 'advanced'>('beginner');
|
||||||
|
const [genWithRests, setGenWithRests] = useState(true);
|
||||||
|
const [genWithNotes, setGenWithNotes] = useState(true);
|
||||||
|
const [genRest, setGenRest] = useState('30');
|
||||||
|
|
||||||
|
const onGenerate = () => {
|
||||||
|
const restSec = Math.max(10, Math.min(120, parseInt(genRest || '30', 10)));
|
||||||
|
const { items, note } = buildClassicalSession({ withSectionRests: genWithRests, restSeconds: restSec, withNotes: genWithNotes, level: genLevel });
|
||||||
|
dispatch(replaceExercises({ date: currentDate, items, note }));
|
||||||
|
dispatch(syncCheckin({ date: currentDate, items, note }));
|
||||||
|
setGenVisible(false);
|
||||||
|
Alert.alert('排课已生成', '已为你生成经典普拉提序列,可继续调整。');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
<View style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
|
||||||
@@ -57,51 +90,128 @@ export default function CheckinHome() {
|
|||||||
|
|
||||||
<HeaderBar title="每日打卡" onBack={() => router.back()} withSafeTop={false} transparent />
|
<HeaderBar title="每日打卡" onBack={() => router.back()} withSafeTop={false} transparent />
|
||||||
<View style={[styles.hero, { backgroundColor: colorTokens.heroSurfaceTint }]}>
|
<View style={[styles.hero, { backgroundColor: colorTokens.heroSurfaceTint }]}>
|
||||||
<Text style={[styles.title, { color: colorTokens.text }]}>{today}</Text>
|
<Text style={[styles.title, { color: colorTokens.text }]}>{currentDate}</Text>
|
||||||
<Text style={[styles.subtitle, { color: colorTokens.textMuted }]}>请选择动作并记录完成情况</Text>
|
<Text style={[styles.subtitle, { color: colorTokens.textMuted }]}>请选择动作并记录完成情况</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.actionRow}>
|
<View style={styles.actionRow}>
|
||||||
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]} onPress={() => router.push('/checkin/select')}>
|
<TouchableOpacity
|
||||||
|
style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]}
|
||||||
|
onPress={() => router.push({ pathname: '/checkin/select', params: { date: currentDate } })}
|
||||||
|
>
|
||||||
<Text style={[styles.primaryBtnText, { color: colorTokens.onPrimary }]}>新增动作</Text>
|
<Text style={[styles.primaryBtnText, { color: colorTokens.onPrimary }]}>新增动作</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<View style={{ height: 10 }} />
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.secondaryBtn, { borderColor: colorTokens.primary }]}
|
||||||
|
onPress={() => setGenVisible(true)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.secondaryBtnText, { color: colorTokens.primary }]}>一键排课(经典序列)</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={record?.items || []}
|
data={(record?.items && record.items.length > 0)
|
||||||
keyExtractor={(item) => item.key}
|
? record.items
|
||||||
|
: ((record as any)?.raw || [])}
|
||||||
|
keyExtractor={(item, index) => (item?.key || item?.id || `${currentDate}_${index}`)}
|
||||||
contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 20 }}
|
contentContainerStyle={{ paddingHorizontal: 20, paddingBottom: 20 }}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={[styles.emptyBox, { backgroundColor: colorTokens.card }]}>
|
<View style={[styles.emptyBox, { backgroundColor: colorTokens.card }]}>
|
||||||
<Text style={[styles.emptyText, { color: colorTokens.textMuted }]}>还没有选择任何动作,点击“新增动作”开始吧。</Text>
|
<Text style={[styles.emptyText, { color: colorTokens.textMuted }]}>还没有选择任何动作,点击“新增动作”开始吧。</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => {
|
||||||
|
// 若为后端原始项(无 key),以标题/时间为卡片,禁用交互
|
||||||
|
const isRaw = !item?.key;
|
||||||
|
if (isRaw) {
|
||||||
|
const title = item?.title || '每日训练打卡';
|
||||||
|
const status = item?.status || '';
|
||||||
|
const startedAt = item?.startedAt ? new Date(item.startedAt).toLocaleString() : '';
|
||||||
|
return (
|
||||||
<View style={[styles.card, { backgroundColor: colorTokens.card }]}>
|
<View style={[styles.card, { backgroundColor: colorTokens.card }]}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{item.name}</Text>
|
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{title}</Text>
|
||||||
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{item.category}</Text>
|
{!!status && <Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{status}</Text>}
|
||||||
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>组数 {item.sets}{item.reps ? ` · 每组 ${item.reps} 次` : ''}{item.durationSec ? ` · 每组 ${item.durationSec}s` : ''}</Text>
|
{!!startedAt && <Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{startedAt}</Text>}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const exercise = item as CheckinExercise;
|
||||||
|
const type = exercise.itemType ?? 'exercise';
|
||||||
|
const isRest = type === 'rest';
|
||||||
|
const isNote = type === 'note';
|
||||||
|
const cardStyle = [styles.card, { backgroundColor: colorTokens.card }];
|
||||||
|
if (isRest || isNote) {
|
||||||
|
return (
|
||||||
|
<View style={styles.inlineRow}>
|
||||||
|
<Ionicons name={isRest ? 'time-outline' : 'information-circle-outline'} size={14} color={colorTokens.textMuted} />
|
||||||
|
<View style={[styles.inlineBadge, isRest ? styles.inlineBadgeRest : styles.inlineBadgeNote, { borderColor: colorTokens.border }]}>
|
||||||
|
<Text style={[isNote ? styles.inlineTextItalic : styles.inlineText, { color: colorTokens.textMuted }]}>
|
||||||
|
{isRest ? `间隔休息 ${exercise.restSec ?? 30}s` : (exercise.note || '提示')}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.inlineRemoveBtn}
|
||||||
|
onPress={() =>
|
||||||
|
Alert.alert('确认移除', '确定要移除该条目吗?', [
|
||||||
|
{ text: '取消', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: '移除',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => {
|
||||||
|
dispatch(removeExercise({ date: currentDate, key: exercise.key }));
|
||||||
|
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== exercise.key);
|
||||||
|
dispatch(syncCheckin({ date: currentDate, items: nextItems, note: record?.note }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||||
|
>
|
||||||
|
<Ionicons name="close-outline" size={16} color={colorTokens.textMuted} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={cardStyle as any}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={[styles.cardTitle, { color: colorTokens.text }]}>{exercise.name}</Text>
|
||||||
|
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>{exercise.category}</Text>
|
||||||
|
{isNote && (
|
||||||
|
<Text style={[styles.cardMetaItalic, { color: colorTokens.textMuted }]}>{exercise.note || '提示'}</Text>
|
||||||
|
)}
|
||||||
|
{!isNote && (
|
||||||
|
<Text style={[styles.cardMeta, { color: colorTokens.textMuted }]}>
|
||||||
|
{isRest
|
||||||
|
? `建议休息 ${exercise.restSec ?? 30}s`
|
||||||
|
: `组数 ${exercise.sets}${exercise.reps ? ` · 每组 ${exercise.reps} 次` : ''}${exercise.durationSec ? ` · 每组 ${exercise.durationSec}s` : ''}`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{type === 'exercise' && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={item.completed ? '已完成,点击取消完成' : '未完成,点击标记完成'}
|
accessibilityLabel={exercise.completed ? '已完成,点击取消完成' : '未完成,点击标记完成'}
|
||||||
style={styles.doneIconBtn}
|
style={styles.doneIconBtn}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
dispatch(toggleExerciseCompleted({ date: today, key: item.key }));
|
dispatch(toggleExerciseCompleted({ date: currentDate, key: exercise.key }));
|
||||||
const nextItems: CheckinExercise[] = (record?.items || []).map((it: CheckinExercise) =>
|
const nextItems: CheckinExercise[] = (record?.items || []).map((it: CheckinExercise) =>
|
||||||
it.key === item.key ? { ...it, completed: !it.completed } : it
|
it.key === exercise.key ? { ...it, completed: !it.completed } : it
|
||||||
);
|
);
|
||||||
dispatch(syncCheckin({ date: today, items: nextItems, note: record?.note }));
|
dispatch(syncCheckin({ date: currentDate, items: nextItems, note: record?.note }));
|
||||||
}}
|
}}
|
||||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={item.completed ? 'checkmark-circle' : 'checkmark-circle-outline'}
|
name={exercise.completed ? 'checkmark-circle' : 'checkmark-circle-outline'}
|
||||||
size={24}
|
size={24}
|
||||||
color={item.completed ? colorTokens.primary : colorTokens.textMuted}
|
color={exercise.completed ? colorTokens.primary : colorTokens.textMuted}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.removeBtn, { backgroundColor: colorTokens.border }]}
|
style={[styles.removeBtn, { backgroundColor: colorTokens.border }]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
@@ -111,9 +221,9 @@ export default function CheckinHome() {
|
|||||||
text: '移除',
|
text: '移除',
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
dispatch(removeExercise({ date: today, key: item.key }));
|
dispatch(removeExercise({ date: currentDate, key: exercise.key }));
|
||||||
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== item.key);
|
const nextItems: CheckinExercise[] = (record?.items || []).filter((it: CheckinExercise) => it.key !== exercise.key);
|
||||||
dispatch(syncCheckin({ date: today, items: nextItems, note: record?.note }));
|
dispatch(syncCheckin({ date: currentDate, items: nextItems, note: record?.note }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -122,8 +232,43 @@ export default function CheckinHome() {
|
|||||||
<Text style={[styles.removeBtnText, { color: colorTokens.text }]}>移除</Text>
|
<Text style={[styles.removeBtnText, { color: colorTokens.text }]}>移除</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* 生成配置弹窗 */}
|
||||||
|
<Modal visible={genVisible} transparent animationType="fade" onRequestClose={() => setGenVisible(false)}>
|
||||||
|
<TouchableOpacity activeOpacity={1} style={styles.modalOverlay} onPress={() => setGenVisible(false)}>
|
||||||
|
<TouchableOpacity activeOpacity={1} style={[styles.modalSheet, { backgroundColor: colorTokens.card }]} onPress={(e) => e.stopPropagation() as any}>
|
||||||
|
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>经典排课配置</Text>
|
||||||
|
<Text style={[styles.modalLabel, { color: colorTokens.textMuted }]}>强度水平</Text>
|
||||||
|
<View style={styles.segmentedRow}>
|
||||||
|
{(['beginner', 'intermediate', 'advanced'] as const).map((lv) => (
|
||||||
|
<TouchableOpacity key={lv} style={[styles.segment, genLevel === lv && { backgroundColor: colorTokens.primary }]} onPress={() => setGenLevel(lv)}>
|
||||||
|
<Text style={[styles.segmentText, genLevel === lv && { color: colorTokens.onPrimary }]}>
|
||||||
|
{lv === 'beginner' ? '入门' : lv === 'intermediate' ? '进阶' : '高级'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<Text style={[styles.switchLabel, { color: colorTokens.text }]}>段间休息</Text>
|
||||||
|
<Switch value={genWithRests} onValueChange={setGenWithRests} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<Text style={[styles.switchLabel, { color: colorTokens.text }]}>插入操作提示</Text>
|
||||||
|
<Switch value={genWithNotes} onValueChange={setGenWithNotes} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.inputRow}>
|
||||||
|
<Text style={[styles.switchLabel, { color: colorTokens.textMuted }]}>休息秒数</Text>
|
||||||
|
<TextInput value={genRest} onChangeText={setGenRest} keyboardType="number-pad" style={[styles.input, { borderColor: colorTokens.border, color: colorTokens.text }]} />
|
||||||
|
</View>
|
||||||
|
<View style={{ height: 8 }} />
|
||||||
|
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colorTokens.primary }]} onPress={onGenerate}>
|
||||||
|
<Text style={[styles.primaryBtnText, { color: colorTokens.onPrimary }]}>生成今日计划</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@@ -145,14 +290,35 @@ const styles = StyleSheet.create({
|
|||||||
actionRow: { paddingHorizontal: 20, marginTop: 8 },
|
actionRow: { paddingHorizontal: 20, marginTop: 8 },
|
||||||
primaryBtn: { backgroundColor: '#111827', paddingVertical: 10, borderRadius: 10, alignItems: 'center' },
|
primaryBtn: { backgroundColor: '#111827', paddingVertical: 10, borderRadius: 10, alignItems: 'center' },
|
||||||
primaryBtnText: { color: '#FFFFFF', fontWeight: '800' },
|
primaryBtnText: { color: '#FFFFFF', fontWeight: '800' },
|
||||||
|
secondaryBtn: { borderWidth: 2, paddingVertical: 10, borderRadius: 10, alignItems: 'center' },
|
||||||
|
secondaryBtnText: { fontWeight: '800' },
|
||||||
emptyBox: { marginTop: 16, backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginHorizontal: 0 },
|
emptyBox: { marginTop: 16, backgroundColor: '#FFFFFF', borderRadius: 16, padding: 16, marginHorizontal: 0 },
|
||||||
emptyText: { color: '#6B7280' },
|
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 },
|
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' },
|
cardTitle: { fontSize: 16, fontWeight: '800', color: '#111827' },
|
||||||
cardMeta: { marginTop: 4, fontSize: 12, color: '#6B7280' },
|
cardMeta: { marginTop: 4, fontSize: 12, color: '#6B7280' },
|
||||||
|
cardMetaItalic: { marginTop: 4, fontSize: 12, color: '#6B7280', fontStyle: 'italic' },
|
||||||
removeBtn: { backgroundColor: '#F3F4F6', paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8 },
|
removeBtn: { backgroundColor: '#F3F4F6', paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8 },
|
||||||
removeBtnText: { color: '#111827', fontWeight: '700' },
|
removeBtnText: { color: '#111827', fontWeight: '700' },
|
||||||
doneIconBtn: { paddingHorizontal: 4, paddingVertical: 4, borderRadius: 16, marginRight: 8 },
|
doneIconBtn: { paddingHorizontal: 4, paddingVertical: 4, borderRadius: 16, marginRight: 8 },
|
||||||
|
inlineRow: { marginTop: 10, marginHorizontal: 20, flexDirection: 'row', alignItems: 'center' },
|
||||||
|
inlineBadge: { marginLeft: 6, borderWidth: 1, borderRadius: 999, paddingVertical: 6, paddingHorizontal: 10 },
|
||||||
|
inlineBadgeRest: { backgroundColor: '#F8FAFC' },
|
||||||
|
inlineBadgeNote: { backgroundColor: '#F9FAFB' },
|
||||||
|
inlineText: { fontSize: 12, fontWeight: '700' },
|
||||||
|
inlineTextItalic: { fontSize: 12, fontStyle: 'italic' },
|
||||||
|
inlineRemoveBtn: { marginLeft: 6, padding: 4, borderRadius: 999 },
|
||||||
|
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.35)', alignItems: 'center', justifyContent: 'flex-end' },
|
||||||
|
modalSheet: { width: '100%', borderTopLeftRadius: 16, borderTopRightRadius: 16, paddingHorizontal: 16, paddingTop: 14, paddingBottom: 24 },
|
||||||
|
modalTitle: { fontSize: 16, fontWeight: '800', marginBottom: 8 },
|
||||||
|
modalLabel: { fontSize: 12, marginBottom: 6 },
|
||||||
|
segmentedRow: { flexDirection: 'row', gap: 8, marginBottom: 8 },
|
||||||
|
segment: { flex: 1, borderRadius: 999, borderWidth: 1, borderColor: '#E5E7EB', paddingVertical: 8, alignItems: 'center' },
|
||||||
|
segmentText: { fontWeight: '700' },
|
||||||
|
switchRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 8 },
|
||||||
|
switchLabel: { fontWeight: '700' },
|
||||||
|
inputRow: { marginTop: 8 },
|
||||||
|
input: { height: 40, borderWidth: 1, borderRadius: 10, paddingHorizontal: 12 },
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { addExercise, syncCheckin } from '@/store/checkinSlice';
|
|||||||
import { EXERCISE_LIBRARY, getCategories, searchExercises } from '@/utils/exerciseLibrary';
|
import { EXERCISE_LIBRARY, getCategories, searchExercises } from '@/utils/exerciseLibrary';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Animated, FlatList, LayoutAnimation, Modal, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, UIManager, View } from 'react-native';
|
import { Animated, FlatList, LayoutAnimation, Modal, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, UIManager, View } from 'react-native';
|
||||||
|
|
||||||
@@ -20,7 +20,9 @@ function formatDate(d: Date) {
|
|||||||
export default function SelectExerciseScreen() {
|
export default function SelectExerciseScreen() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const params = useLocalSearchParams<{ date?: string }>();
|
||||||
const today = useMemo(() => formatDate(new Date()), []);
|
const today = useMemo(() => formatDate(new Date()), []);
|
||||||
|
const currentDate = (typeof params?.date === 'string' && params.date) ? params.date : today;
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ export default function SelectExerciseScreen() {
|
|||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
dispatch(addExercise({
|
dispatch(addExercise({
|
||||||
date: today,
|
date: currentDate,
|
||||||
item: {
|
item: {
|
||||||
key: selected.key,
|
key: selected.key,
|
||||||
name: selected.name,
|
name: selected.name,
|
||||||
@@ -79,11 +81,11 @@ export default function SelectExerciseScreen() {
|
|||||||
reps: reps && reps > 0 ? reps : undefined,
|
reps: reps && reps > 0 ? reps : undefined,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
console.log('addExercise', today, selected.key, sets, reps);
|
console.log('addExercise', currentDate, selected.key, sets, reps);
|
||||||
// 同步到后端(读取最新 store 需要在返回后由首页触发 load,或此处直接上报)
|
// 同步到后端(读取最新 store 需要在返回后由首页触发 load,或此处直接上报)
|
||||||
// 简单做法:直接上报新增项(其余项由后端合并/覆盖)
|
// 简单做法:直接上报新增项(其余项由后端合并/覆盖)
|
||||||
dispatch(syncCheckin({
|
dispatch(syncCheckin({
|
||||||
date: today,
|
date: currentDate,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: selected.key,
|
key: selected.key,
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export default function EditProfileScreen() {
|
|||||||
allowsEditing: true,
|
allowsEditing: true,
|
||||||
quality: 0.9,
|
quality: 0.9,
|
||||||
aspect: [1, 1],
|
aspect: [1, 1],
|
||||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
mediaTypes: ['images'],
|
||||||
base64: false,
|
base64: false,
|
||||||
});
|
});
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
|
|||||||
73
components/ArticleCard.tsx
Normal file
73
components/ArticleCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import React from 'react';
|
||||||
|
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
coverImage: string;
|
||||||
|
publishedAt: string; // ISO
|
||||||
|
readCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ArticleCard({ id, title, coverImage, publishedAt, readCount }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<Pressable onPress={() => router.push(`/article/${id}`)} style={styles.card}>
|
||||||
|
<Image source={{ uri: coverImage }} style={styles.cover} />
|
||||||
|
<View style={styles.meta}>
|
||||||
|
<Text style={styles.title} numberOfLines={2}>{title}</Text>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.metaText}>{dayjs(publishedAt).format('YYYY-MM-DD')}</Text>
|
||||||
|
<Text style={[styles.metaText, styles.dot]}>·</Text>
|
||||||
|
<Text style={styles.metaText}>{readCount} 阅读</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 14,
|
||||||
|
marginBottom: 14,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 10,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
cover: {
|
||||||
|
width: 92,
|
||||||
|
height: 92,
|
||||||
|
borderRadius: 16,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
flex: 1,
|
||||||
|
paddingLeft: 12,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#192126',
|
||||||
|
fontWeight: '800',
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
metaText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#8A8A8E',
|
||||||
|
},
|
||||||
|
dot: {
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export const COS_BUCKET: string = '';
|
export const COS_BUCKET: string = 'plates-1251306435';
|
||||||
export const COS_REGION: string = '';
|
export const COS_REGION: string = 'ap-guangzhou';
|
||||||
export const COS_PUBLIC_BASE: string = '';
|
export const COS_PUBLIC_BASE: string = '';
|
||||||
|
|
||||||
// 统一的对象键前缀(可按业务拆分)
|
// 统一的对象键前缀(可按业务拆分)
|
||||||
@@ -19,8 +19,16 @@ export function buildCosKey(params: { prefix?: string; ext?: string; userId?: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildPublicUrl(key: string): string {
|
export function buildPublicUrl(key: string): string {
|
||||||
if (!COS_PUBLIC_BASE) return '';
|
const cleanedKey = key.replace(/^\//, '');
|
||||||
return `${COS_PUBLIC_BASE.replace(/\/$/, '')}/${key.replace(/^\//, '')}`;
|
if (COS_PUBLIC_BASE && COS_PUBLIC_BASE.trim()) {
|
||||||
|
return `${COS_PUBLIC_BASE.replace(/\/$/, '')}/${cleanedKey}`;
|
||||||
|
}
|
||||||
|
// 回退:使用 COS 默认公网域名
|
||||||
|
// 例如: https://<bucket>.cos.<region>.myqcloud.com/<key>
|
||||||
|
if (COS_BUCKET && COS_REGION) {
|
||||||
|
return `https://${COS_BUCKET}.cos.${COS_REGION}.myqcloud.com/${cleanedKey}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { buildCosKey, buildPublicUrl } from '@/constants/Cos';
|
import { buildCosKey, buildPublicUrl } from '@/constants/Cos';
|
||||||
import { uploadWithRetry } from '@/services/cos';
|
import { uploadWithRetry } from '@/services/cos';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
export type UseCosUploadOptions = {
|
export type UseCosUploadOptions = {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
@@ -31,37 +32,44 @@ export function useCosUpload(defaultOptions?: UseCosUploadOptions) {
|
|||||||
setProgress(0);
|
setProgress(0);
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
try {
|
try {
|
||||||
|
let res: any;
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
// Web:使用 Blob 走 cos-js-sdk-v5 分支
|
||||||
let body: any = null;
|
let body: any = null;
|
||||||
// 1) 直接可用类型:Blob 或 string
|
|
||||||
if (typeof file === 'string') {
|
if (typeof file === 'string') {
|
||||||
|
// 允许直接传 Base64/DataURL 字符串
|
||||||
body = file;
|
body = file;
|
||||||
} else if (typeof Blob !== 'undefined' && file instanceof Blob) {
|
} else if ((file as any)?.blob) {
|
||||||
body = file;
|
|
||||||
} else if ((file as any)?.blob && (typeof Blob === 'undefined' || (file as any).blob instanceof Blob || (file as any).blob?._data)) {
|
|
||||||
// 2) 已提供 blob 字段
|
|
||||||
body = (file as any).blob;
|
body = (file as any).blob;
|
||||||
} else if ((file as any)?.buffer) {
|
|
||||||
// 3) ArrayBuffer/TypedArray -> Blob
|
|
||||||
const buffer = (file as any).buffer;
|
|
||||||
body = new Blob([buffer], { type: (file as any)?.type || finalOptions.contentType || 'application/octet-stream' });
|
|
||||||
} else if ((file as any)?.uri) {
|
} else if ((file as any)?.uri) {
|
||||||
// 4) Expo ImagePicker/文件:必须先转 Blob
|
|
||||||
const resp = await fetch((file as any).uri);
|
const resp = await fetch((file as any).uri);
|
||||||
body = await resp.blob();
|
body = await resp.blob();
|
||||||
} else {
|
} else if (typeof Blob !== 'undefined' && file instanceof Blob) {
|
||||||
// 兜底:尝试直接作为字符串,否则抛错
|
|
||||||
if (file && (typeof file === 'object')) {
|
|
||||||
throw new Error('无效的上传体:请提供 Blob/String,或包含 uri 的对象');
|
|
||||||
}
|
|
||||||
body = file;
|
body = file;
|
||||||
|
} else {
|
||||||
|
throw new Error('无效的文件:请提供 uri 或 Blob');
|
||||||
}
|
}
|
||||||
const res = await uploadWithRetry({
|
res = await uploadWithRetry({
|
||||||
key,
|
key,
|
||||||
body,
|
body,
|
||||||
contentType: finalOptions.contentType || (file as any)?.type,
|
contentType: finalOptions.contentType || (file as any)?.type,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
onProgress: ({ percent }) => setProgress(percent),
|
onProgress: ({ percent }: { percent: number }) => setProgress(percent),
|
||||||
});
|
} as any);
|
||||||
|
} else {
|
||||||
|
// 原生:直接传本地路径
|
||||||
|
const srcUri = (file as any)?.uri || (typeof file === 'string' ? file : undefined);
|
||||||
|
if (!srcUri || typeof srcUri !== 'string') {
|
||||||
|
throw new Error('请提供包含 uri 的对象,或传入本地文件路径字符串');
|
||||||
|
}
|
||||||
|
res = await uploadWithRetry({
|
||||||
|
key,
|
||||||
|
srcUri,
|
||||||
|
contentType: finalOptions.contentType || (file as any)?.type,
|
||||||
|
signal: controller.signal,
|
||||||
|
onProgress: ({ percent }: { percent: number }) => setProgress(percent),
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
const url = (res as any).publicUrl || buildPublicUrl(res.key);
|
const url = (res as any).publicUrl || buildPublicUrl(res.key);
|
||||||
return { key: res.key, url };
|
return { key: res.key, url };
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ target 'digitalpilates' do
|
|||||||
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Force all Pods to build simulator slices as arm64 (avoid mixed x86_64/arm64 issues)
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
# Force pods to build arm64 simulator by excluding only x86_64
|
||||||
|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'x86_64'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# This is necessary for Xcode 14, because it signs resource bundles by default
|
# This is necessary for Xcode 14, because it signs resource bundles by default
|
||||||
# when building for devices.
|
# when building for devices.
|
||||||
installer.target_installation_results.pod_target_installation_results
|
installer.target_installation_results.pod_target_installation_results
|
||||||
|
|||||||
243
ios/Podfile.lock
243
ios/Podfile.lock
@@ -113,6 +113,15 @@ PODS:
|
|||||||
- libwebp/sharpyuv (1.5.0)
|
- libwebp/sharpyuv (1.5.0)
|
||||||
- libwebp/webp (1.5.0):
|
- libwebp/webp (1.5.0):
|
||||||
- libwebp/sharpyuv
|
- libwebp/sharpyuv
|
||||||
|
- QCloudCore (6.5.1):
|
||||||
|
- QCloudCore/Default (= 6.5.1)
|
||||||
|
- QCloudCore/Default (6.5.1):
|
||||||
|
- QCloudTrack/Beacon (= 6.5.1)
|
||||||
|
- QCloudCOSXML (6.5.1):
|
||||||
|
- QCloudCOSXML/Default (= 6.5.1)
|
||||||
|
- QCloudCOSXML/Default (6.5.1):
|
||||||
|
- QCloudCore (= 6.5.1)
|
||||||
|
- QCloudTrack/Beacon (6.5.1)
|
||||||
- RCT-Folly (2024.11.18.00):
|
- RCT-Folly (2024.11.18.00):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -1367,78 +1376,13 @@ PODS:
|
|||||||
- React-jsiexecutor
|
- React-jsiexecutor
|
||||||
- React-RCTFBReactNativeSpec
|
- React-RCTFBReactNativeSpec
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
|
- react-native-cos-sdk (1.2.1):
|
||||||
|
- QCloudCOSXML (= 6.5.1)
|
||||||
|
- React-Core
|
||||||
|
- react-native-render-html (6.3.4):
|
||||||
|
- React-Core
|
||||||
- react-native-safe-area-context (5.4.0):
|
- react-native-safe-area-context (5.4.0):
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- react-native-safe-area-context/common (= 5.4.0)
|
|
||||||
- react-native-safe-area-context/fabric (= 5.4.0)
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- react-native-safe-area-context/common (5.4.0):
|
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- react-native-safe-area-context/fabric (5.4.0):
|
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- react-native-safe-area-context/common
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- react-native-webview (13.13.5):
|
- react-native-webview (13.13.5):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
@@ -1760,51 +1704,9 @@ PODS:
|
|||||||
- RNAppleHealthKit (1.7.0):
|
- RNAppleHealthKit (1.7.0):
|
||||||
- React
|
- React
|
||||||
- RNCAsyncStorage (2.2.0):
|
- RNCAsyncStorage (2.2.0):
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- RNDateTimePicker (8.4.4):
|
- RNDateTimePicker (8.4.4):
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- RNGestureHandler (2.24.0):
|
- RNGestureHandler (2.24.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
@@ -1948,31 +1850,6 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNScreens (4.11.1):
|
- RNScreens (4.11.1):
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-RCTImage
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- RNScreens/common (= 4.11.1)
|
|
||||||
- Yoga
|
|
||||||
- RNScreens/common (4.11.1):
|
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
- RCT-Folly (= 2024.11.18.00)
|
||||||
@@ -1997,52 +1874,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNSVG (15.12.1):
|
- RNSVG (15.12.1):
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- RNSVG/common (= 15.12.1)
|
|
||||||
- Yoga
|
|
||||||
- RNSVG/common (15.12.1):
|
|
||||||
- DoubleConversion
|
|
||||||
- glog
|
|
||||||
- RCT-Folly (= 2024.11.18.00)
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-Core
|
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsc
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- Yoga
|
|
||||||
- SDWebImage (5.21.1):
|
- SDWebImage (5.21.1):
|
||||||
- SDWebImage/Core (= 5.21.1)
|
- SDWebImage/Core (= 5.21.1)
|
||||||
- SDWebImage/Core (5.21.1)
|
- SDWebImage/Core (5.21.1)
|
||||||
@@ -2107,7 +1939,6 @@ DEPENDENCIES:
|
|||||||
- React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
|
- React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
|
||||||
- React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
|
- React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
|
||||||
- React-jsc (from `../node_modules/react-native/ReactCommon/jsc`)
|
- React-jsc (from `../node_modules/react-native/ReactCommon/jsc`)
|
||||||
- React-jsc/Fabric (from `../node_modules/react-native/ReactCommon/jsc`)
|
|
||||||
- React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
|
- React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
|
||||||
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
||||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||||
@@ -2118,6 +1949,8 @@ DEPENDENCIES:
|
|||||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||||
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
|
||||||
|
- react-native-cos-sdk (from `../node_modules/react-native-cos-sdk`)
|
||||||
|
- react-native-render-html (from `../node_modules/react-native-render-html`)
|
||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||||
@@ -2164,6 +1997,9 @@ SPEC REPOS:
|
|||||||
- libavif
|
- libavif
|
||||||
- libdav1d
|
- libdav1d
|
||||||
- libwebp
|
- libwebp
|
||||||
|
- QCloudCore
|
||||||
|
- QCloudCOSXML
|
||||||
|
- QCloudTrack
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SDWebImageAVIFCoder
|
- SDWebImageAVIFCoder
|
||||||
- SDWebImageSVGCoder
|
- SDWebImageSVGCoder
|
||||||
@@ -2285,6 +2121,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
React-microtasksnativemodule:
|
React-microtasksnativemodule:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||||
|
react-native-cos-sdk:
|
||||||
|
:path: "../node_modules/react-native-cos-sdk"
|
||||||
|
react-native-render-html:
|
||||||
|
:path: "../node_modules/react-native-render-html"
|
||||||
react-native-safe-area-context:
|
react-native-safe-area-context:
|
||||||
:path: "../node_modules/react-native-safe-area-context"
|
:path: "../node_modules/react-native-safe-area-context"
|
||||||
react-native-webview:
|
react-native-webview:
|
||||||
@@ -2371,7 +2211,7 @@ SPEC CHECKSUMS:
|
|||||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||||
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
|
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
|
||||||
Expo: ad449665420b9fe5e907bd97e79aec2e47f98785
|
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
|
||||||
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
||||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||||
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
||||||
@@ -2384,8 +2224,8 @@ SPEC CHECKSUMS:
|
|||||||
ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7
|
ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7
|
||||||
ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d
|
ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d
|
||||||
ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902
|
ExpoLinking: d5c183998ca6ada66ff45e407e0f965b398a8902
|
||||||
ExpoModulesCore: 2c1a84ec154d32afb4f6569bc558f059ebbcdb8e
|
ExpoModulesCore: 272bc6c06ddd9c4bee2048acc57891cab3700627
|
||||||
ExpoSplashScreen: 0ad5acac1b5d2953c6e00d4319f16d616f70d4dd
|
ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385
|
||||||
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
|
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
|
||||||
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2
|
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2
|
||||||
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
||||||
@@ -2396,6 +2236,9 @@ SPEC CHECKSUMS:
|
|||||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
|
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
|
||||||
|
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
|
||||||
|
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
|
||||||
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
|
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
|
||||||
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
|
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
|
||||||
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
|
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
|
||||||
@@ -2427,22 +2270,24 @@ SPEC CHECKSUMS:
|
|||||||
React-logger: 85fa3509931497c72ccd2547fcc91e7299d8591e
|
React-logger: 85fa3509931497c72ccd2547fcc91e7299d8591e
|
||||||
React-Mapbuffer: 96a2f2a176268581733be182fa6eebab1c0193be
|
React-Mapbuffer: 96a2f2a176268581733be182fa6eebab1c0193be
|
||||||
React-microtasksnativemodule: 11b292232f1626567a79d58136689f1b911c605f
|
React-microtasksnativemodule: 11b292232f1626567a79d58136689f1b911c605f
|
||||||
react-native-safe-area-context: b0ee54c424896b916aab46212b884cb8794308d7
|
react-native-cos-sdk: a29ad87f60e2edb2adc46da634aa5b6e7cd14e35
|
||||||
react-native-webview: faccaeb84216940628d4422822d367ad03d15a81
|
react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd
|
||||||
|
react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1
|
||||||
|
react-native-webview: 3df1192782174d1bd23f6a0f5a4fec3cdcca9954
|
||||||
React-NativeModulesApple: 494c38599b82392ed14b2c0118fca162425bb618
|
React-NativeModulesApple: 494c38599b82392ed14b2c0118fca162425bb618
|
||||||
React-oscompat: 0592889a9fcf0eacb205532028e4a364e22907dd
|
React-oscompat: 0592889a9fcf0eacb205532028e4a364e22907dd
|
||||||
React-perflogger: c584fa50e422a46f37404d083fad12eb289d5de4
|
React-perflogger: c584fa50e422a46f37404d083fad12eb289d5de4
|
||||||
React-performancetimeline: 8deae06fc819e6f7d1f834818e72ab5581540e45
|
React-performancetimeline: 8deae06fc819e6f7d1f834818e72ab5581540e45
|
||||||
React-RCTActionSheet: ce67bdc050cc1d9ef673c7a93e9799288a183f24
|
React-RCTActionSheet: ce67bdc050cc1d9ef673c7a93e9799288a183f24
|
||||||
React-RCTAnimation: 8bb813eb29c6de85be99c62640f3a999df76ba02
|
React-RCTAnimation: 8bb813eb29c6de85be99c62640f3a999df76ba02
|
||||||
React-RCTAppDelegate: 4de5b1b68d9bc435bb7949fdde274895f12428c6
|
React-RCTAppDelegate: 738515a4ab15cc17996887269e17444bf08dee85
|
||||||
React-RCTBlob: 4c6fa35aa8b2b4d46ff2e5fb80c2b26df9457e57
|
React-RCTBlob: 4c6fa35aa8b2b4d46ff2e5fb80c2b26df9457e57
|
||||||
React-RCTFabric: 05582e7dc62b2c393b054b39d1b4202e9dcbce68
|
React-RCTFabric: f53fbf29459c959ce9ccbea28edfe6dc9ca35e36
|
||||||
React-RCTFBReactNativeSpec: f5970e7ba0b15cf23c0552c82251aff9630a6acd
|
React-RCTFBReactNativeSpec: 1a2f1bd84f03ea0d7e3228055c3b894fb56680dd
|
||||||
React-RCTImage: 8a4f6ce18e73a7e894b886dfb7625e9e9fbc90ef
|
React-RCTImage: 8a4f6ce18e73a7e894b886dfb7625e9e9fbc90ef
|
||||||
React-RCTLinking: fa49c624cd63979e7a6295ae9b1351d23ac4395a
|
React-RCTLinking: fa49c624cd63979e7a6295ae9b1351d23ac4395a
|
||||||
React-RCTNetwork: f236fd2897d18522bba24453e2995a4c83e01024
|
React-RCTNetwork: f236fd2897d18522bba24453e2995a4c83e01024
|
||||||
React-RCTRuntime: f46f5c9890b77bbb38a536157d317a7a04a8825e
|
React-RCTRuntime: 596bd113c46f61d82ac5d6199023bafd9a390cf4
|
||||||
React-RCTSettings: 69e2f25a5a1bf6cb37eef2e5c3bd4bb7e848296b
|
React-RCTSettings: 69e2f25a5a1bf6cb37eef2e5c3bd4bb7e848296b
|
||||||
React-RCTText: 515ce74ed79c31dbf509e6f12770420ebbf23755
|
React-RCTText: 515ce74ed79c31dbf509e6f12770420ebbf23755
|
||||||
React-RCTVibration: ef30ada606dfed859b2c71577f6f041d47f2cfbb
|
React-RCTVibration: ef30ada606dfed859b2c71577f6f041d47f2cfbb
|
||||||
@@ -2460,12 +2305,12 @@ SPEC CHECKSUMS:
|
|||||||
ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe
|
ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe
|
||||||
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
|
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
|
||||||
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
||||||
RNCAsyncStorage: f4b48b7eb2ae9296be4df608ff60c1b12a469b7a
|
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
||||||
RNDateTimePicker: 41af3f0749ea5555f15805b468bc8453e6fa9850
|
RNDateTimePicker: 7d93eacf4bdf56350e4b7efd5cfc47639185e10c
|
||||||
RNGestureHandler: 6bf8b210cbad95ced45f3f9b8df05924b3a97300
|
RNGestureHandler: 6e640921d207f070e4bbcf79f4e6d0eabf323389
|
||||||
RNReanimated: 79c239f5562adcf2406b681830f716f1e7d76081
|
RNReanimated: 34e90d19560aebd52a2ad583fdc2de2cf7651bbb
|
||||||
RNScreens: dd9a329b21412c5322a5447fc2c3ae6471cf6e5a
|
RNScreens: 241cfe8fc82737f3e132dd45779f9512928075b8
|
||||||
RNSVG: 820687c168d70d90a47d96a0cd5e263905fc67d9
|
RNSVG: 3544def7b3ddc43c7ba69dade91bacf99f10ec46
|
||||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||||
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
||||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||||
@@ -2473,6 +2318,6 @@ SPEC CHECKSUMS:
|
|||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||||
|
|
||||||
PODFILE CHECKSUM: b384f735cddc85333f7f9842fb492a2893323ea2
|
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expo.jsEngine": "jsc",
|
"expo.jsEngine": "jsc",
|
||||||
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
"newArchEnabled": "true"
|
"newArchEnabled": "false"
|
||||||
}
|
}
|
||||||
@@ -268,6 +268,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/QCloudCOSXML/QCloudCOSXML.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||||
@@ -283,6 +284,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QCloudCOSXML.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||||
@@ -322,6 +324,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"FB_SONARKIT_ENABLED=1",
|
"FB_SONARKIT_ENABLED=1",
|
||||||
@@ -358,6 +361,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
|
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
DEVELOPMENT_TEAM = 756WVXJ6MT;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
|
||||||
INFOPLIST_FILE = digitalpilates/Info.plist;
|
INFOPLIST_FILE = digitalpilates/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
432
package-lock.json
generated
432
package-lock.json
generated
@@ -36,11 +36,14 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
|
"react-native-cos-sdk": "^1.2.1",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-health": "^1.19.0",
|
"react-native-health": "^1.19.0",
|
||||||
"react-native-image-viewing": "^0.2.2",
|
"react-native-image-viewing": "^0.2.2",
|
||||||
|
"react-native-markdown-display": "^7.0.2",
|
||||||
"react-native-modal-datetime-picker": "^18.0.0",
|
"react-native-modal-datetime-picker": "^18.0.0",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
|
"react-native-render-html": "^6.3.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
"react-native-svg": "^15.12.1",
|
"react-native-svg": "^15.12.1",
|
||||||
@@ -2652,6 +2655,23 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsamr/counter-style": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@jsamr/counter-style/-/counter-style-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-2mXudGVtSzVxWEA7B9jZLKjoXUeUFYDDtFrQoC0IFX9/Dszz4t1vZOmafi3JSw/FxD+udMQ+4TAFR8Qs0J3URQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@jsamr/react-native-li": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@jsamr/react-native-li/-/react-native-li-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-Qbo4NEj48SQ4k8FZJHFE2fgZDKTWaUGmVxcIQh3msg5JezLdTMMHuRRDYctfdHI6L0FZGObmEv3haWbIvmol8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@jsamr/counter-style": "^1.0.0 || ^2.0.0",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||||
@@ -2665,6 +2685,88 @@
|
|||||||
"@tybys/wasm-util": "^0.10.0"
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@native-html/css-processor": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@native-html/css-processor/-/css-processor-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-NnhBEbJX5M2gBGltPKOetiLlKhNf3OHdRafc8//e2ZQxXN8JaSW/Hy8cm94pnIckQxwaMKxrtaNT3x4ZcffoNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-to-react-native": "^3.0.0",
|
||||||
|
"csstype": "^3.0.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@native-html/transient-render-engine": {
|
||||||
|
"version": "11.2.3",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@native-html/transient-render-engine/-/transient-render-engine-11.2.3.tgz",
|
||||||
|
"integrity": "sha512-zXwgA3gPUEmFs3I3syfnvDvS6WiUHXEE6jY09OBzK+trq7wkweOSFWIoyXiGkbXrozGYG0KY90YgPyr8Tg8Uyg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@native-html/css-processor": "1.11.0",
|
||||||
|
"@types/ramda": "^0.27.44",
|
||||||
|
"csstype": "^3.0.9",
|
||||||
|
"domelementtype": "^2.2.0",
|
||||||
|
"domhandler": "^4.2.2",
|
||||||
|
"domutils": "^2.8.0",
|
||||||
|
"htmlparser2": "^7.1.2",
|
||||||
|
"ramda": "^0.27.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react-native": "*",
|
||||||
|
"react-native": "^*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@native-html/transient-render-engine/node_modules/dom-serializer": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^4.2.0",
|
||||||
|
"entities": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@native-html/transient-render-engine/node_modules/domhandler": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domhandler/-/domhandler-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@native-html/transient-render-engine/node_modules/domutils": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domutils/-/domutils-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^1.0.1",
|
||||||
|
"domelementtype": "^2.2.0",
|
||||||
|
"domhandler": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@native-html/transient-render-engine/node_modules/entities": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/entities/-/entities-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -3051,6 +3153,20 @@
|
|||||||
"integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==",
|
"integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native/virtualized-lists": {
|
||||||
|
"version": "0.72.8",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz",
|
||||||
|
"integrity": "sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"nullthrows": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-navigation/bottom-tabs": {
|
"node_modules/@react-navigation/bottom-tabs": {
|
||||||
"version": "7.4.5",
|
"version": "7.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.5.tgz",
|
||||||
@@ -3341,22 +3457,47 @@
|
|||||||
"undici-types": "~7.10.0"
|
"undici-types": "~7.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ramda": {
|
||||||
|
"version": "0.27.66",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@types/ramda/-/ramda-0.27.66.tgz",
|
||||||
|
"integrity": "sha512-i2YW+E2U6NfMt3dp0RxNcejox+bxJUNDjB7BpYuRuoHIzv5juPHkJkNgcUOu+YSQEmaWu8cnAo/8r63C0NnuVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ts-toolbelt": "^6.15.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.0.14",
|
"version": "19.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz",
|
||||||
"integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==",
|
"integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-native": {
|
||||||
|
"version": "0.72.8",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@types/react-native/-/react-native-0.72.8.tgz",
|
||||||
|
"integrity": "sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@react-native/virtualized-lists": "^0.72.4",
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/urijs": {
|
||||||
|
"version": "1.19.25",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/@types/urijs/-/urijs-1.19.25.tgz",
|
||||||
|
"integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
@@ -4848,6 +4989,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelize": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/camelize/-/camelize-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001733",
|
"version": "1.0.30001733",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz",
|
||||||
@@ -4884,6 +5033,26 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/character-entities-html4": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/character-entities-html4/-/character-entities-html4-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/character-entities-legacy": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
@@ -5284,6 +5453,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-color-keywords": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-in-js-utils": {
|
"node_modules/css-in-js-utils": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
||||||
@@ -5308,6 +5486,17 @@
|
|||||||
"url": "https://github.com/sponsors/fb55"
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-to-react-native": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camelize": "^1.0.0",
|
||||||
|
"css-color-keywords": "^1.0.0",
|
||||||
|
"postcss-value-parser": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-tree": {
|
"node_modules/css-tree": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/css-tree/-/css-tree-1.1.3.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
@@ -5343,7 +5532,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
@@ -7458,6 +7646,83 @@
|
|||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/htmlparser2/-/htmlparser2-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^4.2.2",
|
||||||
|
"domutils": "^2.8.0",
|
||||||
|
"entities": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/dom-serializer": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^4.2.0",
|
||||||
|
"entities": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/entities/-/entities-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domhandler": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domhandler/-/domhandler-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domutils": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/domutils/-/domutils-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^1.0.1",
|
||||||
|
"domelementtype": "^2.2.0",
|
||||||
|
"domhandler": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/entities": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/entities/-/entities-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@@ -8780,6 +9045,15 @@
|
|||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -8927,6 +9201,36 @@
|
|||||||
"tmpl": "1.0.5"
|
"tmpl": "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"entities": "~2.0.0",
|
||||||
|
"linkify-it": "^2.0.0",
|
||||||
|
"mdurl": "^1.0.1",
|
||||||
|
"uc.micro": "^1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it/node_modules/argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it/node_modules/entities": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/entities/-/entities-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/marky": {
|
"node_modules/marky": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
||||||
@@ -8949,6 +9253,11 @@
|
|||||||
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/mdurl/-/mdurl-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@@ -10361,6 +10670,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ramda": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/ramda/-/ramda-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -10529,6 +10844,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-cos-sdk": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-cos-sdk/-/react-native-cos-sdk-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-mT75MIweoM2X3sWxe8/03RGtVsVsRWfV8ZkMAOyMsvPkhJ4k6a7D2G9rln24NKlOGONT2q/9f5rwjjYUTgFOBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-native-uuid": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-edge-to-edge": {
|
"node_modules/react-native-edge-to-edge": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz",
|
||||||
@@ -10539,6 +10870,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-fit-image": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==",
|
||||||
|
"license": "Beerware",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-gesture-handler": {
|
"node_modules/react-native-gesture-handler": {
|
||||||
"version": "2.24.0",
|
"version": "2.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.24.0.tgz",
|
||||||
@@ -10744,6 +11084,22 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-markdown-display": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-to-react-native": "^3.0.0",
|
||||||
|
"markdown-it": "^10.0.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-native-fit-image": "^1.5.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.2.0",
|
||||||
|
"react-native": ">=0.50.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-modal-datetime-picker": {
|
"node_modules/react-native-modal-datetime-picker": {
|
||||||
"version": "18.0.0",
|
"version": "18.0.0",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-18.0.0.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-18.0.0.tgz",
|
||||||
@@ -10792,6 +11148,27 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-render-html": {
|
||||||
|
"version": "6.3.4",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-render-html/-/react-native-render-html-6.3.4.tgz",
|
||||||
|
"integrity": "sha512-H2jSMzZjidE+Wo3qCWPUMU1nm98Vs2SGCvQCz/i6xf0P3Y9uVtG/b0sDbG/cYFir2mSYBYCIlS1Dv0WC1LjYig==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsamr/counter-style": "^2.0.1",
|
||||||
|
"@jsamr/react-native-li": "^2.3.0",
|
||||||
|
"@native-html/transient-render-engine": "11.2.3",
|
||||||
|
"@types/ramda": "^0.27.40",
|
||||||
|
"@types/urijs": "^1.19.15",
|
||||||
|
"prop-types": "^15.5.7",
|
||||||
|
"ramda": "^0.27.2",
|
||||||
|
"stringify-entities": "^3.1.0",
|
||||||
|
"urijs": "^1.19.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-safe-area-context": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz",
|
||||||
@@ -10832,6 +11209,16 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-uuid": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-uuid/-/react-native-uuid-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-f/YfIS2f5UB+gut7t/9BKGSCYbRA9/74A5R1MDp+FLYsuS+OSWoiM/D8Jko6OJB6Jcu3v6ONuddvZKHdIGpeiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0",
|
||||||
|
"npm": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-web": {
|
"node_modules/react-native-web": {
|
||||||
"version": "0.20.0",
|
"version": "0.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
|
||||||
@@ -12177,6 +12564,20 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stringify-entities": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/stringify-entities/-/stringify-entities-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg==",
|
||||||
|
"dependencies": {
|
||||||
|
"character-entities-html4": "^1.0.0",
|
||||||
|
"character-entities-legacy": "^1.0.0",
|
||||||
|
"xtend": "^4.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
@@ -12565,6 +12966,12 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-toolbelt": {
|
||||||
|
"version": "6.15.5",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
|
||||||
|
"integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.15.0",
|
"version": "3.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||||
@@ -12748,6 +13155,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
@@ -12918,6 +13331,12 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/urijs": {
|
||||||
|
"version": "1.19.11",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/urijs/-/urijs-1.19.11.tgz",
|
||||||
|
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/use-latest-callback": {
|
"node_modules/use-latest-callback": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz",
|
||||||
@@ -13356,6 +13775,15 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cos-js-sdk-v5": "^1.6.0",
|
|
||||||
"@expo/vector-icons": "^14.1.0",
|
"@expo/vector-icons": "^14.1.0",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-native-community/datetimepicker": "^8.4.4",
|
"@react-native-community/datetimepicker": "^8.4.4",
|
||||||
@@ -19,6 +18,7 @@
|
|||||||
"@react-navigation/elements": "^2.3.8",
|
"@react-navigation/elements": "^2.3.8",
|
||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
|
"cos-js-sdk-v5": "^1.6.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
"expo-apple-authentication": "6.4.2",
|
"expo-apple-authentication": "6.4.2",
|
||||||
@@ -39,11 +39,14 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
|
"react-native-cos-sdk": "^1.2.1",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-health": "^1.19.0",
|
"react-native-health": "^1.19.0",
|
||||||
"react-native-image-viewing": "^0.2.2",
|
"react-native-image-viewing": "^0.2.2",
|
||||||
|
"react-native-markdown-display": "^7.0.2",
|
||||||
"react-native-modal-datetime-picker": "^18.0.0",
|
"react-native-modal-datetime-picker": "^18.0.0",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
|
"react-native-render-html": "^6.3.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
"react-native-svg": "^15.12.1",
|
"react-native-svg": "^15.12.1",
|
||||||
|
|||||||
44
services/articles.ts
Normal file
44
services/articles.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { api } from './api';
|
||||||
|
|
||||||
|
export type Article = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
coverImage: string;
|
||||||
|
htmlContent: string;
|
||||||
|
publishedAt: string; // ISO string
|
||||||
|
readCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const demoArticles: Article[] = [
|
||||||
|
{
|
||||||
|
id: 'intro-pilates-posture',
|
||||||
|
title: '新手入门:普拉提核心与体态的关系',
|
||||||
|
coverImage: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
|
||||||
|
publishedAt: dayjs().subtract(2, 'day').toISOString(),
|
||||||
|
readCount: 1268,
|
||||||
|
htmlContent: `
|
||||||
|
<h2>为什么核心很重要?</h2>
|
||||||
|
<p>核心是维持良好体态与动作稳定的关键。普拉提通过强调呼吸与深层肌群激活,帮助你在<em>日常站立、坐姿与训练</em>中保持更好的身体对齐。</p>
|
||||||
|
<h3>入门建议</h3>
|
||||||
|
<ol>
|
||||||
|
<li>从呼吸开始:尝试<strong>胸廓外扩</strong>而非耸肩。</li>
|
||||||
|
<li>慢而可控:注意动作过程中的连贯与专注。</li>
|
||||||
|
<li>记录变化:每周拍照或在应用中记录体态变化。</li>
|
||||||
|
</ol>
|
||||||
|
<p>更多实操可在本应用的「AI体态评估」中获取个性化建议。</p>
|
||||||
|
<img src="https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/ImageCheck.jpeg" alt="pilates-illustration" />
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function listRecommendedArticles(): Article[] {
|
||||||
|
// 实际项目中可替换为 API 请求
|
||||||
|
return demoArticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getArticleById(id: string): Promise<Article | undefined> {
|
||||||
|
return api.get<Article>(`/articles/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
139
services/cos.ts
139
services/cos.ts
@@ -1,5 +1,6 @@
|
|||||||
import { COS_BUCKET, COS_REGION, buildPublicUrl } from '@/constants/Cos';
|
import { COS_BUCKET, COS_REGION, buildPublicUrl } from '@/constants/Cos';
|
||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
|
import Cos from 'react-native-cos-sdk';
|
||||||
|
|
||||||
type ServerCosToken = {
|
type ServerCosToken = {
|
||||||
tmpSecretId: string;
|
tmpSecretId: string;
|
||||||
@@ -29,41 +30,24 @@ type CosCredential = {
|
|||||||
|
|
||||||
type UploadOptions = {
|
type UploadOptions = {
|
||||||
key: string;
|
key: string;
|
||||||
body: any;
|
// React Native COS SDK 推荐使用本地文件路径(file:// 或 content://)
|
||||||
|
srcUri?: string;
|
||||||
|
// 为兼容旧实现(Web/Blob)。在 RN SDK 下会忽略 body
|
||||||
|
body?: any;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
onProgress?: (progress: { percent: number }) => void;
|
onProgress?: (progress: { percent: number }) => void;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
};
|
};
|
||||||
|
|
||||||
let CosSdk: any | null = null;
|
let rnTransferManager: any | null = null;
|
||||||
|
let rnInitialized = false;
|
||||||
|
|
||||||
async function ensureCosSdk(): Promise<any> {
|
|
||||||
if (CosSdk) return CosSdk;
|
|
||||||
// RN 兼容:SDK 在初始化时会访问 navigator.userAgent
|
|
||||||
const g: any = globalThis as any;
|
|
||||||
if (!g.navigator) g.navigator = {};
|
|
||||||
if (!g.navigator.userAgent) g.navigator.userAgent = 'react-native';
|
|
||||||
// 动态导入避免影响首屏,并加入 require 回退,兼容打包差异
|
|
||||||
let mod: any = null;
|
|
||||||
try {
|
|
||||||
mod = await import('cos-js-sdk-v5');
|
|
||||||
} catch (_) {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
mod = require('cos-js-sdk-v5');
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
const Candidate = mod?.COS || mod?.default || mod;
|
|
||||||
if (!Candidate) {
|
|
||||||
throw new Error('cos-js-sdk-v5 加载失败');
|
|
||||||
}
|
|
||||||
CosSdk = Candidate;
|
|
||||||
return CosSdk;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchCredential(): Promise<CosCredential> {
|
async function fetchCredential(): Promise<CosCredential> {
|
||||||
// 后端返回 { code, message, data },api.get 会提取 data
|
// 后端返回 { code, message, data },api.get 会提取 data
|
||||||
const data = await api.get<ServerCosToken>('/api/users/cos/upload-token');
|
const data = await api.get<ServerCosToken>('/api/users/cos/upload-token');
|
||||||
|
|
||||||
|
console.log('fetchCredential', data);
|
||||||
return {
|
return {
|
||||||
credentials: {
|
credentials: {
|
||||||
tmpSecretId: data.tmpSecretId,
|
tmpSecretId: data.tmpSecretId,
|
||||||
@@ -80,8 +64,8 @@ async function fetchCredential(): Promise<CosCredential> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadToCos(options: UploadOptions): Promise<{ key: string; etag?: string; headers?: Record<string, string>; publicUrl?: string }> {
|
export async function uploadToCos(options: UploadOptions): Promise<{ key: string; etag?: string; headers?: Record<string, string>; publicUrl?: string }> {
|
||||||
const { key, body, contentType, onProgress, signal } = options;
|
const { key, srcUri, contentType, onProgress, signal } = options;
|
||||||
const COS = await ensureCosSdk();
|
|
||||||
const cred = await fetchCredential();
|
const cred = await fetchCredential();
|
||||||
const bucket = COS_BUCKET || cred.bucket;
|
const bucket = COS_BUCKET || cred.bucket;
|
||||||
const region = COS_REGION || cred.region;
|
const region = COS_REGION || cred.region;
|
||||||
@@ -89,14 +73,48 @@ export async function uploadToCos(options: UploadOptions): Promise<{ key: string
|
|||||||
throw new Error('未配置 COS_BUCKET / COS_REGION,且服务端未返回 bucket/region');
|
throw new Error('未配置 COS_BUCKET / COS_REGION,且服务端未返回 bucket/region');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保对象键以服务端授权的前缀开头
|
// 确保对象键以服务端授权的前缀开头(去除通配符,折叠斜杠,避免把 * 拼进 Key)
|
||||||
const finalKey = ((): string => {
|
const finalKey = ((): string => {
|
||||||
const prefix = (cred.prefix || '').replace(/^\/+|\/+$/g, '');
|
const rawPrefix = String(cred.prefix || '');
|
||||||
if (!prefix) return key.replace(/^\//, '');
|
// 1) 去掉 * 与多余斜杠,再去掉首尾斜杠
|
||||||
|
const safePrefix = rawPrefix
|
||||||
|
.replace(/\*/g, '')
|
||||||
|
.replace(/\/{2,}/g, '/')
|
||||||
|
.replace(/^\/+|\/+$/g, '');
|
||||||
const normalizedKey = key.replace(/^\//, '');
|
const normalizedKey = key.replace(/^\//, '');
|
||||||
return normalizedKey.startsWith(prefix + '/') ? normalizedKey : `${prefix}/${normalizedKey}`;
|
if (!safePrefix) return normalizedKey;
|
||||||
|
if (normalizedKey.startsWith(safePrefix + '/')) return normalizedKey;
|
||||||
|
return `${safePrefix}/${normalizedKey}`.replace(/\/{2,}/g, '/');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// 初始化 react-native-cos-sdk(一次)
|
||||||
|
if (!rnInitialized) {
|
||||||
|
await Cos.initWithSessionCredentialCallback(async () => {
|
||||||
|
// SDK 会在需要时调用该回调,我们返回当前的临时密钥
|
||||||
|
return {
|
||||||
|
tmpSecretId: cred.credentials.tmpSecretId,
|
||||||
|
tmpSecretKey: cred.credentials.tmpSecretKey,
|
||||||
|
sessionToken: cred.credentials.sessionToken,
|
||||||
|
startTime: cred.startTime,
|
||||||
|
expiredTime: cred.expiredTime,
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
const serviceConfig = { region, isDebuggable: true, isHttps: true } as any;
|
||||||
|
await Cos.registerDefaultService(serviceConfig);
|
||||||
|
const transferConfig = {
|
||||||
|
forceSimpleUpload: false,
|
||||||
|
enableVerification: true,
|
||||||
|
divisionForUpload: 2 * 1024 * 1024,
|
||||||
|
sliceSizeForUpload: 1 * 1024 * 1024,
|
||||||
|
} as any;
|
||||||
|
rnTransferManager = await Cos.registerDefaultTransferManger(serviceConfig, transferConfig);
|
||||||
|
rnInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!srcUri || typeof srcUri !== 'string') {
|
||||||
|
throw new Error('请提供本地文件路径 srcUri(形如 file:/// 或 content://)');
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
if (signal) {
|
if (signal) {
|
||||||
if (signal.aborted) controller.abort();
|
if (signal.aborted) controller.abort();
|
||||||
@@ -104,43 +122,46 @@ export async function uploadToCos(options: UploadOptions): Promise<{ key: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const cos = new COS({
|
let cancelled = false;
|
||||||
getAuthorization: (_opts: any, cb: any) => {
|
let taskRef: any = null;
|
||||||
cb({
|
(async () => {
|
||||||
TmpSecretId: cred.credentials.tmpSecretId,
|
try {
|
||||||
TmpSecretKey: cred.credentials.tmpSecretKey,
|
taskRef = await rnTransferManager.upload(
|
||||||
SecurityToken: cred.credentials.sessionToken,
|
bucket,
|
||||||
StartTime: cred.startTime,
|
finalKey,
|
||||||
ExpiredTime: cred.expiredTime,
|
srcUri,
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const task = cos.putObject(
|
|
||||||
{
|
{
|
||||||
Bucket: bucket,
|
resultListener: {
|
||||||
Region: region,
|
successCallBack: (header: any) => {
|
||||||
Key: finalKey,
|
if (cancelled) return;
|
||||||
Body: body,
|
const publicUrl = buildPublicUrl(finalKey);
|
||||||
ContentType: contentType,
|
const etag = header?.ETag || header?.headers?.ETag || header?.headers?.etag;
|
||||||
onProgress: (progressData: any) => {
|
resolve({ key: finalKey, etag, headers: header, publicUrl });
|
||||||
|
},
|
||||||
|
failCallBack: (clientError: any, serviceError: any) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
console.log('uploadToCos', { clientError, serviceError });
|
||||||
|
const err = clientError || serviceError || new Error('COS 上传失败');
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
progressCallback: (complete: number, target: number) => {
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
const percent = progressData && progressData.percent ? progressData.percent : 0;
|
const percent = target > 0 ? complete / target : 0;
|
||||||
onProgress({ percent });
|
onProgress({ percent });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
contentType,
|
||||||
(err: any, data: any) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
const publicUrl = cred.cdnDomain
|
|
||||||
? `${String(cred.cdnDomain).replace(/\/$/, '')}/${finalKey.replace(/^\//, '')}`
|
|
||||||
: buildPublicUrl(finalKey);
|
|
||||||
resolve({ key: finalKey, etag: data && data.ETag, headers: data && data.headers, publicUrl });
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!cancelled) reject(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
controller.signal.addEventListener('abort', () => {
|
controller.signal.addEventListener('abort', () => {
|
||||||
try { task && task.cancel && task.cancel(); } catch { }
|
cancelled = true;
|
||||||
|
try { taskRef?.cancel?.(); } catch { }
|
||||||
reject(new DOMException('Aborted', 'AbortError'));
|
reject(new DOMException('Aborted', 'AbortError'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
23
services/recommendations.ts
Normal file
23
services/recommendations.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { api } from '@/services/api';
|
||||||
|
|
||||||
|
export enum RecommendationType {
|
||||||
|
Article = 'article',
|
||||||
|
Checkin = 'checkin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecommendationCard = {
|
||||||
|
id: string;
|
||||||
|
type: RecommendationType;
|
||||||
|
title?: string;
|
||||||
|
coverUrl: string;
|
||||||
|
articleId?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
extra?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchRecommendations(): Promise<RecommendationCard[]> {
|
||||||
|
// 后端返回 BaseResponseDto<data>,services/api 会自动解出 data 字段
|
||||||
|
return api.get<RecommendationCard[]>(`/recommendations/list`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -5,9 +5,18 @@ export type CheckinExercise = {
|
|||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
sets: number; // 组数
|
/**
|
||||||
|
* itemType
|
||||||
|
* - exercise: 正常训练动作(默认)
|
||||||
|
* - rest: 组间/动作间休息(仅展示,不可勾选完成)
|
||||||
|
* - note: 备注/口令提示(仅展示)
|
||||||
|
*/
|
||||||
|
itemType?: 'exercise' | 'rest' | 'note';
|
||||||
|
sets: number; // 组数(rest/note 可为 0)
|
||||||
reps?: number; // 每组重复(计次型)
|
reps?: number; // 每组重复(计次型)
|
||||||
durationSec?: number; // 每组时长(计时型)
|
durationSec?: number; // 每组时长(计时型)
|
||||||
|
restSec?: number; // 休息时长(当 itemType=rest 时使用)
|
||||||
|
note?: string; // 备注内容(当 itemType=note 时使用)
|
||||||
completed?: boolean; // 是否已完成该动作
|
completed?: boolean; // 是否已完成该动作
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,6 +25,8 @@ export type CheckinRecord = {
|
|||||||
date: string; // YYYY-MM-DD
|
date: string; // YYYY-MM-DD
|
||||||
items: CheckinExercise[];
|
items: CheckinExercise[];
|
||||||
note?: string;
|
note?: string;
|
||||||
|
// 保留后端原始返回,便于当 metrics.items 为空时做回退展示
|
||||||
|
raw?: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CheckinState = {
|
export type CheckinState = {
|
||||||
@@ -60,10 +71,17 @@ const checkinSlice = createSlice({
|
|||||||
const rec = ensureRecord(state, action.payload.date);
|
const rec = ensureRecord(state, action.payload.date);
|
||||||
rec.items = rec.items.filter((it) => it.key !== action.payload.key);
|
rec.items = rec.items.filter((it) => it.key !== action.payload.key);
|
||||||
},
|
},
|
||||||
|
replaceExercises(state, action: PayloadAction<{ date: string; items: CheckinExercise[]; note?: string }>) {
|
||||||
|
const rec = ensureRecord(state, action.payload.date);
|
||||||
|
rec.items = (action.payload.items || []).map((it) => ({ ...it, completed: false }));
|
||||||
|
if (typeof action.payload.note === 'string') rec.note = action.payload.note;
|
||||||
|
},
|
||||||
toggleExerciseCompleted(state, action: PayloadAction<{ date: string; key: string }>) {
|
toggleExerciseCompleted(state, action: PayloadAction<{ date: string; key: string }>) {
|
||||||
const rec = ensureRecord(state, action.payload.date);
|
const rec = ensureRecord(state, action.payload.date);
|
||||||
const idx = rec.items.findIndex((it) => it.key === action.payload.key);
|
const idx = rec.items.findIndex((it) => it.key === action.payload.key);
|
||||||
if (idx >= 0) rec.items[idx].completed = !rec.items[idx].completed;
|
if (idx >= 0 && (rec.items[idx].itemType ?? 'exercise') === 'exercise') {
|
||||||
|
rec.items[idx].completed = !rec.items[idx].completed;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setNote(state, action: PayloadAction<{ date: string; note: string }>) {
|
setNote(state, action: PayloadAction<{ date: string; note: string }>) {
|
||||||
const rec = ensureRecord(state, action.payload.date);
|
const rec = ensureRecord(state, action.payload.date);
|
||||||
@@ -106,20 +124,27 @@ const checkinSlice = createSlice({
|
|||||||
date,
|
date,
|
||||||
items: mergedItems,
|
items: mergedItems,
|
||||||
note,
|
note,
|
||||||
|
raw: list,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.addCase(loadMonthCheckins.fulfilled, (state, action) => {
|
.addCase(loadMonthCheckins.fulfilled, (state, action) => {
|
||||||
const monthKey = action.payload.monthKey;
|
const monthKey = action.payload.monthKey;
|
||||||
const merged = action.payload.byDate;
|
const merged = action.payload.byDate;
|
||||||
for (const d of Object.keys(merged)) {
|
for (const d of Object.keys(merged)) {
|
||||||
state.byDate[d] = merged[d];
|
const prev = state.byDate[d];
|
||||||
|
const next = merged[d];
|
||||||
|
const items = (next.items && next.items.length > 0) ? next.items : (prev?.items ?? []);
|
||||||
|
const note = (typeof next.note === 'string') ? next.note : prev?.note;
|
||||||
|
const id = next.id || prev?.id || `rec_${d}`;
|
||||||
|
const raw = prev?.raw ?? (next as any)?.raw;
|
||||||
|
state.byDate[d] = { id, date: d, items, note, raw } as CheckinRecord;
|
||||||
}
|
}
|
||||||
state.monthLoaded[monthKey] = true;
|
state.monthLoaded[monthKey] = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setCurrentDate, addExercise, removeExercise, toggleExerciseCompleted, setNote, resetDate } = checkinSlice.actions;
|
export const { setCurrentDate, addExercise, removeExercise, replaceExercises, toggleExerciseCompleted, setNote, resetDate } = checkinSlice.actions;
|
||||||
export default checkinSlice.reducer;
|
export default checkinSlice.reducer;
|
||||||
|
|
||||||
// Thunks
|
// Thunks
|
||||||
@@ -145,9 +170,10 @@ export const syncCheckin = createAsyncThunk('checkin/sync', async (record: { dat
|
|||||||
|
|
||||||
// 获取当天打卡列表(用于进入页面时拉取最新云端数据)
|
// 获取当天打卡列表(用于进入页面时拉取最新云端数据)
|
||||||
export const getDailyCheckins = createAsyncThunk('checkin/getDaily', async (date?: string) => {
|
export const getDailyCheckins = createAsyncThunk('checkin/getDaily', async (date?: string) => {
|
||||||
const list = await fetchDailyCheckins(date);
|
const dateParam = date ?? new Date().toISOString().slice(0, 10);
|
||||||
|
const list = await fetchDailyCheckins(dateParam);
|
||||||
return { date, list } as { date?: string; list: any[] };
|
try { console.log('getDailyCheckins', { date: dateParam, count: Array.isArray(list) ? list.length : -1 }); } catch { }
|
||||||
|
return { date: dateParam, list } as { date?: string; list: any[] };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按月加载:优先使用区间接口,失败则逐日回退
|
// 按月加载:优先使用区间接口,失败则逐日回退
|
||||||
|
|||||||
34
types/react-native-cos-sdk.d.ts
vendored
Normal file
34
types/react-native-cos-sdk.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
declare module 'react-native-cos-sdk' {
|
||||||
|
export type SessionCredential = {
|
||||||
|
tmpSecretId: string;
|
||||||
|
tmpSecretKey: string;
|
||||||
|
sessionToken: string;
|
||||||
|
startTime?: number;
|
||||||
|
expiredTime?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initWithSessionCredentialCallback(cb: () => Promise<SessionCredential> | SessionCredential): Promise<void> | void;
|
||||||
|
|
||||||
|
export function registerDefaultService(config: { region: string; isHttps?: boolean; isDebuggable?: boolean }): Promise<any>;
|
||||||
|
|
||||||
|
export function registerDefaultTransferManger(
|
||||||
|
serviceConfig: { region: string; isHttps?: boolean; isDebuggable?: boolean },
|
||||||
|
transferConfig: {
|
||||||
|
forceSimpleUpload?: boolean;
|
||||||
|
enableVerification?: boolean;
|
||||||
|
divisionForUpload?: number;
|
||||||
|
sliceSizeForUpload?: number;
|
||||||
|
}
|
||||||
|
): Promise<any>;
|
||||||
|
|
||||||
|
export function getDefaultTransferManger(): any;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initWithSessionCredentialCallback,
|
||||||
|
registerDefaultService,
|
||||||
|
registerDefaultTransferManger,
|
||||||
|
getDefaultTransferManger,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
112
utils/classicalSession.ts
Normal file
112
utils/classicalSession.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import type { CheckinExercise } from '@/store/checkinSlice';
|
||||||
|
|
||||||
|
export type ClassicalLevel = 'beginner' | 'intermediate' | 'advanced';
|
||||||
|
|
||||||
|
export type BuildOptions = {
|
||||||
|
level?: ClassicalLevel;
|
||||||
|
withSectionRests?: boolean; // 大段之间插入休息项
|
||||||
|
restSeconds?: number; // 休息秒数
|
||||||
|
withNotes?: boolean; // 插入提示/备注项
|
||||||
|
};
|
||||||
|
|
||||||
|
function restItem(idx: number, sec: number): CheckinExercise {
|
||||||
|
return {
|
||||||
|
key: `rest_${idx}`,
|
||||||
|
name: `间隔休息 ${sec}s`,
|
||||||
|
category: '休息',
|
||||||
|
itemType: 'rest',
|
||||||
|
sets: 0,
|
||||||
|
restSec: sec,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteItem(idx: number, text: string): CheckinExercise {
|
||||||
|
return {
|
||||||
|
key: `note_${idx}`,
|
||||||
|
name: '提示',
|
||||||
|
category: '备注',
|
||||||
|
itemType: 'note',
|
||||||
|
sets: 0,
|
||||||
|
note: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将图片中的“经典排课思路”转为结构化的动作清单(偏改革床序列,但以通用名称表示)
|
||||||
|
export function buildClassicalSession(options: BuildOptions = {}): { items: CheckinExercise[]; note: string } {
|
||||||
|
const level = options.level ?? 'beginner';
|
||||||
|
const withRests = options.withSectionRests ?? true;
|
||||||
|
const restSec = Math.max(10, Math.min(120, options.restSeconds ?? 30));
|
||||||
|
const withNotes = options.withNotes ?? true;
|
||||||
|
|
||||||
|
const items: CheckinExercise[] = [];
|
||||||
|
let noteText = '经典普拉提排课(根据学员情况可删减与调序)';
|
||||||
|
|
||||||
|
const pushSectionRest = () => { if (withRests) items.push(restItem(items.length, restSec)); };
|
||||||
|
const pushNote = (text: string) => { if (withNotes) items.push(noteItem(items.length, text)); };
|
||||||
|
|
||||||
|
// 1) 垫上热身/呼吸
|
||||||
|
pushNote('垫上热身:在仰卧位找到中立位,呼吸练习配合上举落下手臂');
|
||||||
|
items.push({ key: 'breathing', name: '呼吸练习', category: '热身', itemType: 'exercise', sets: 1, durationSec: 60 });
|
||||||
|
pushSectionRest();
|
||||||
|
|
||||||
|
// 2) 正式核心床练习(节选/映射)
|
||||||
|
// Footwork 系列(每项10次)
|
||||||
|
pushNote('Footwork:脚趾、脚跟、V型脚、宽距,各10次');
|
||||||
|
const footworkReps = 10;
|
||||||
|
items.push({ key: 'footwork_toes', name: 'Footwork - 脚趾', category: '下肢与核心', sets: 1, reps: footworkReps });
|
||||||
|
items.push({ key: 'footwork_heels', name: 'Footwork - 脚跟', category: '下肢与核心', sets: 1, reps: footworkReps });
|
||||||
|
items.push({ key: 'footwork_v', name: 'Footwork - V型脚', category: '下肢与核心', sets: 1, reps: footworkReps });
|
||||||
|
items.push({ key: 'footwork_wide', name: 'Footwork - 宽距', category: '下肢与核心', sets: 1, reps: footworkReps });
|
||||||
|
items.push({ key: 'hundred', name: '百次拍击 (Hundred)', category: '核心', sets: 1, reps: 100 });
|
||||||
|
items.push({ key: 'coordination', name: '协调 (Coordination)', category: '核心', sets: 1, reps: 6 });
|
||||||
|
items.push({ key: 'short_spine', name: '短脊柱按摩 (Short Spine Massage)', category: '脊柱与柔韧', sets: 1, reps: 6 });
|
||||||
|
items.push({ key: 'hug_a_tree', name: '抱树 (Hug a Tree)', category: '肩带与核心', sets: 1, reps: 10 });
|
||||||
|
// Stomach Massage
|
||||||
|
items.push({ key: 'stomach_round', name: '腹部按摩 - 背部弯曲', category: '核心与髋', sets: 1, reps: 6 });
|
||||||
|
items.push({ key: 'stomach_chest_up', name: '腹部按摩 - 挺胸', category: '核心与髋', sets: 1, reps: 6 });
|
||||||
|
// Short Box Abdominals 变式(每个3-4次)
|
||||||
|
const sbaReps = level === 'advanced' ? 4 : 3;
|
||||||
|
items.push({ key: 'short_box_round', name: '短箱腹肌 - 背部弯曲', category: '核心', sets: 1, reps: sbaReps });
|
||||||
|
items.push({ key: 'short_box_flat', name: '短箱腹肌 - 背部平直', category: '核心', sets: 1, reps: sbaReps });
|
||||||
|
items.push({ key: 'short_box_oblique', name: '短箱腹肌 - 斜向', category: '核心', sets: 1, reps: sbaReps });
|
||||||
|
items.push({ key: 'short_box_tree', name: '短箱腹肌 - 爬树', category: '核心', sets: 1, reps: sbaReps });
|
||||||
|
pushSectionRest();
|
||||||
|
|
||||||
|
// 3) Long Stretch Series(每组4次;Elephant 6次)
|
||||||
|
const lssReps = 4;
|
||||||
|
items.push({ key: 'long_stretch', name: 'Long Stretch', category: '平衡与支撑', sets: 1, reps: lssReps });
|
||||||
|
items.push({ key: 'up_stretch', name: 'Up Stretch', category: '平衡与支撑', sets: 1, reps: lssReps });
|
||||||
|
items.push({ key: 'down_stretch', name: 'Down Stretch', category: '平衡与支撑', sets: 1, reps: lssReps });
|
||||||
|
items.push({ key: 'elephant', name: 'Elephant', category: '后链与拉伸', sets: 1, reps: 6 });
|
||||||
|
pushSectionRest();
|
||||||
|
|
||||||
|
// 4) 半圆、长脊柱按摩
|
||||||
|
items.push({ key: 'semi_circle', name: '半圆 (Semi-Circle) 每个方向', category: '脊柱与胸椎', sets: 1, reps: 6 });
|
||||||
|
items.push({ key: 'long_spine', name: '长脊柱按摩 (Long Spine Massage) 每个方向', category: '脊柱与柔韧', sets: 1, reps: 3 });
|
||||||
|
pushSectionRest();
|
||||||
|
|
||||||
|
// 5) 膝跪伸展(Round/Flat/Knees Off)
|
||||||
|
items.push({ key: 'knee_stretch_round', name: '膝跪伸展 - 弯背', category: '核心与髋', sets: 1, reps: 10 });
|
||||||
|
items.push({ key: 'knee_stretch_flat', name: '膝跪伸展 - 平背', category: '核心与髋', sets: 1, reps: 15 });
|
||||||
|
items.push({ key: 'knee_stretch_knees_off', name: '膝跪伸展 - 双膝离地', category: '核心与髋', sets: 1, reps: level === 'advanced' ? 8 : 6 });
|
||||||
|
pushSectionRest();
|
||||||
|
|
||||||
|
// 6) 跑步、骨盆抬起
|
||||||
|
items.push({ key: 'running', name: '原地跑步 (Running in Place)', category: '下肢与心肺', sets: 1, reps: 25 });
|
||||||
|
items.push({ key: 'pelvic_lift', name: '骨盆抬起 (Pelvic Lift)', category: '后链', sets: 1, reps: 10 });
|
||||||
|
pushSectionRest();
|
||||||
|
|
||||||
|
// 7) 站姿与划船、Mermaid
|
||||||
|
items.push({ key: 'standing_knees_straight', name: '站姿 - 双膝伸直', category: '平衡与体态', sets: 1, reps: 6 });
|
||||||
|
items.push({ key: 'standing_knees_bent', name: '站姿 - 双膝弯曲', category: '平衡与体态', sets: 1, reps: 6 });
|
||||||
|
items.push({ key: 'rowing_front_1', name: '划船 正面1', category: '肩带与核心', sets: 1, reps: 4 });
|
||||||
|
items.push({ key: 'rowing_front_2', name: '划船 正面2', category: '肩带与核心', sets: 1, reps: 4 });
|
||||||
|
items.push({ key: 'mermaid', name: '美人鱼 (Mermaid)', category: '侧链与拉伸', sets: 1, reps: 3 });
|
||||||
|
|
||||||
|
// 结尾提示
|
||||||
|
pushNote('完成后侧坐于床侧边上,向前倾伸展背部,向后细伸脊椎并站起');
|
||||||
|
|
||||||
|
return { items, note: noteText };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user