feat: 引入路由常量并更新相关页面导航
- 新增 ROUTES 常量文件,集中管理应用路由 - 更新多个页面的导航逻辑,使用 ROUTES 常量替代硬编码路径 - 修改教练页面和今日训练页面的路由,提升代码可维护性 - 优化标签页和登录页面的导航,确保一致性和易用性
This commit is contained in:
@@ -7,6 +7,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol';
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
@@ -17,9 +18,9 @@ export default function TabLayout() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={({ route }) => {
|
screenOptions={({ route }) => {
|
||||||
const routeName = route.name;
|
const routeName = route.name;
|
||||||
const isSelected = (routeName === 'index' && pathname === '/') ||
|
const isSelected = (routeName === 'index' && pathname === ROUTES.TAB_HOME) ||
|
||||||
(routeName === 'coach' && pathname === '/coach') ||
|
(routeName === 'coach' && pathname === ROUTES.TAB_COACH) ||
|
||||||
(routeName === 'statistics' && pathname === '/statistics') ||
|
(routeName === 'statistics' && pathname === ROUTES.TAB_STATISTICS) ||
|
||||||
pathname.includes(routeName);
|
pathname.includes(routeName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -39,7 +40,7 @@ export default function TabLayout() {
|
|||||||
const getIconAndTitle = () => {
|
const getIconAndTitle = () => {
|
||||||
switch (routeName) {
|
switch (routeName) {
|
||||||
case 'index':
|
case 'index':
|
||||||
return { icon: 'house.fill', title: '首页' } as const;
|
return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const;
|
||||||
case 'coach':
|
case 'coach':
|
||||||
return { icon: 'person.3.fill', title: 'Bot' } as const;
|
return { icon: 'person.3.fill', title: 'Bot' } as const;
|
||||||
case 'statistics':
|
case 'statistics':
|
||||||
@@ -157,12 +158,12 @@ export default function TabLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: '首页',
|
title: '发现',
|
||||||
tabBarIcon: ({ color }) => {
|
tabBarIcon: ({ color }) => {
|
||||||
const isHomeSelected = pathname === '/' || pathname === '/index';
|
const isHomeSelected = pathname === '/' || pathname === '/index';
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<IconSymbol size={22} name="house.fill" color={color} />
|
<IconSymbol size={22} name="magnifyingglass.circle.fill" color={color} />
|
||||||
{isHomeSelected && (
|
{isHomeSelected && (
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
@@ -174,7 +175,7 @@ export default function TabLayout() {
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
首页
|
发现
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -24,16 +24,14 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
|
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
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 { updateUser as updateUserApi } from '@/services/users';
|
|
||||||
import type { CheckinRecord } from '@/store/checkinSlice';
|
|
||||||
import { fetchMyProfile, fetchWeightHistory, updateProfile } from '@/store/userSlice';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { ActionSheet } from '../../components/ui/ActionSheet';
|
||||||
|
|
||||||
type Role = 'user' | 'assistant';
|
type Role = 'user' | 'assistant';
|
||||||
|
|
||||||
@@ -43,6 +41,15 @@ type ChatMessage = {
|
|||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 卡片类型常量定义
|
||||||
|
const CardType = {
|
||||||
|
WEIGHT_INPUT: '__WEIGHT_INPUT_CARD__',
|
||||||
|
DIET_INPUT: '__DIET_INPUT_CARD__',
|
||||||
|
DIET_TEXT_INPUT: '__DIET_TEXT_INPUT__',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type CardType = typeof CardType[keyof typeof CardType];
|
||||||
|
|
||||||
const COACH_AVATAR = require('@/assets/images/logo.png');
|
const COACH_AVATAR = require('@/assets/images/logo.png');
|
||||||
|
|
||||||
export default function CoachScreen() {
|
export default function CoachScreen() {
|
||||||
@@ -84,10 +91,13 @@ export default function CoachScreen() {
|
|||||||
}>>([]);
|
}>>([]);
|
||||||
const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
|
const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
|
||||||
const [dietTextInputs, setDietTextInputs] = useState<Record<string, string>>({});
|
const [dietTextInputs, setDietTextInputs] = useState<Record<string, string>>({});
|
||||||
|
const [weightInputs, setWeightInputs] = useState<Record<string, string>>({});
|
||||||
|
const [showDietPhotoActionSheet, setShowDietPhotoActionSheet] = useState(false);
|
||||||
|
const [currentCardId, setCurrentCardId] = useState<string | null>(null);
|
||||||
|
|
||||||
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
||||||
const checkin = useAppSelector((s) => s.checkin || {});
|
const checkin = useAppSelector((s) => s.checkin || {});
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const userProfile = useAppSelector((s) => s.user?.profile);
|
const userProfile = useAppSelector((s) => s.user?.profile);
|
||||||
const { upload } = useCosUpload();
|
const { upload } = useCosUpload();
|
||||||
|
|
||||||
@@ -199,8 +209,8 @@ export default function CoachScreen() {
|
|||||||
// { 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() },
|
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
|
||||||
{ key: 'diet', label: '记饮食', action: () => insertDietInputCard() },
|
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
|
||||||
], [router, planDraft, checkin]);
|
], [router, planDraft, checkin]);
|
||||||
|
|
||||||
const scrollToEnd = useCallback(() => {
|
const scrollToEnd = useCallback(() => {
|
||||||
@@ -424,6 +434,7 @@ export default function CoachScreen() {
|
|||||||
setConversationId(undefined);
|
setConversationId(undefined);
|
||||||
setSelectedImages([]);
|
setSelectedImages([]);
|
||||||
setDietTextInputs({});
|
setDietTextInputs({});
|
||||||
|
setWeightInputs({});
|
||||||
|
|
||||||
// 创建新的欢迎消息
|
// 创建新的欢迎消息
|
||||||
initializeWelcomeMessage();
|
initializeWelcomeMessage();
|
||||||
@@ -614,88 +625,6 @@ export default function CoachScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQuickPlan() {
|
|
||||||
const goalMap: Record<string, string> = {
|
|
||||||
postpartum_recovery: '产后恢复',
|
|
||||||
fat_loss: '减脂塑形',
|
|
||||||
posture_correction: '体态矫正',
|
|
||||||
core_strength: '核心力量',
|
|
||||||
flexibility: '柔韧灵活',
|
|
||||||
rehab: '康复保健',
|
|
||||||
stress_relief: '释压放松',
|
|
||||||
};
|
|
||||||
const goalText = planDraft?.goal ? goalMap[planDraft.goal] : '整体提升';
|
|
||||||
const freq = planDraft?.mode === 'sessionsPerWeek'
|
|
||||||
? `${planDraft?.sessionsPerWeek ?? 3}次/周`
|
|
||||||
: (planDraft?.daysOfWeek?.length ? `${planDraft.daysOfWeek.length}次/周` : '3次/周');
|
|
||||||
const prefer = planDraft?.preferredTimeOfDay ? `偏好${planDraft.preferredTimeOfDay}` : '时间灵活';
|
|
||||||
const prompt = `请根据我的目标"${goalText}"、频率"${freq}"、${prefer},制定1周的普拉提训练计划,包含每次训练主题、时长、主要动作与注意事项,并给出恢复建议。`;
|
|
||||||
send(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTrainingSummary(): string {
|
|
||||||
try {
|
|
||||||
const entries = Object.values(checkin?.byDate || {}) as CheckinRecord[];
|
|
||||||
if (!entries.length) return '';
|
|
||||||
|
|
||||||
const recent = entries
|
|
||||||
.filter(entry => entry && entry.date) // 过滤无效数据
|
|
||||||
.sort((a: any, b: any) => String(b.date).localeCompare(String(a.date)))
|
|
||||||
.slice(0, 14);
|
|
||||||
|
|
||||||
let totalSessions = 0;
|
|
||||||
let totalExercises = 0;
|
|
||||||
let totalCompleted = 0;
|
|
||||||
const categoryCount: Record<string, number> = {};
|
|
||||||
const exerciseCount: Record<string, number> = {};
|
|
||||||
|
|
||||||
for (const rec of recent) {
|
|
||||||
if (!rec?.items?.length) continue;
|
|
||||||
totalSessions += 1;
|
|
||||||
for (const it of rec.items) {
|
|
||||||
if (!it || typeof it !== 'object') continue;
|
|
||||||
totalExercises += 1;
|
|
||||||
if (it.completed) totalCompleted += 1;
|
|
||||||
if (it.category) {
|
|
||||||
categoryCount[it.category] = (categoryCount[it.category] || 0) + 1;
|
|
||||||
}
|
|
||||||
if (it.name) {
|
|
||||||
exerciseCount[it.name] = (exerciseCount[it.name] || 0) + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const topCategories = Object.entries(categoryCount)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(([k, v]) => `${k}×${v}`);
|
|
||||||
|
|
||||||
const topExercises = Object.entries(exerciseCount)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 5)
|
|
||||||
.map(([k, v]) => `${k}×${v}`);
|
|
||||||
|
|
||||||
return [
|
|
||||||
`统计周期:最近${recent.length}天(按有记录日计 ${totalSessions} 天)`,
|
|
||||||
`记录条目:${totalExercises},完成标记:${totalCompleted}`,
|
|
||||||
topCategories.length ? `高频类别:${topCategories.join(',')}` : '',
|
|
||||||
topExercises.length ? `高频动作:${topExercises.join(',')}` : '',
|
|
||||||
].filter(Boolean).join('\n');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[AI_CHAT] Error building training summary:', error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAnalyzeRecords() {
|
|
||||||
const summary = buildTrainingSummary();
|
|
||||||
if (!summary) {
|
|
||||||
send('我还没有可分析的打卡记录,请先在"每日打卡"添加并完成一些训练记录,然后帮我分析近期训练表现与改进建议。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const prompt = `请基于以下我的近期训练记录进行分析,输出:1)整体训练负荷与节奏;2)动作与肌群的均衡性(指出偏多/偏少);3)容易忽视的恢复与热身建议;4)后续一周的优化建议(频次/时长/动作方向)。\n\n${summary}`;
|
|
||||||
send(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadImage = useCallback(async (img: any) => {
|
const uploadImage = useCallback(async (img: any) => {
|
||||||
if (!img?.localUri || !img?.id) {
|
if (!img?.localUri || !img?.id) {
|
||||||
@@ -785,9 +714,6 @@ export default function CoachScreen() {
|
|||||||
layout={Layout.springify().damping(18)}
|
layout={Layout.springify().damping(18)}
|
||||||
style={[styles.row, { justifyContent: isUser ? 'flex-end' : 'flex-start' }]}
|
style={[styles.row, { justifyContent: isUser ? 'flex-end' : 'flex-start' }]}
|
||||||
>
|
>
|
||||||
{!isUser && (
|
|
||||||
<Image source={COACH_AVATAR} style={styles.avatar} />
|
|
||||||
)}
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.bubble,
|
styles.bubble,
|
||||||
@@ -811,7 +737,8 @@ export default function CoachScreen() {
|
|||||||
return <Text style={[styles.bubbleText, { color: '#687076' }]}>正在思考…</Text>;
|
return <Text style={[styles.bubbleText, { color: '#687076' }]}>正在思考…</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.content?.startsWith('__WEIGHT_INPUT_CARD__')) {
|
if (item.content?.startsWith(CardType.WEIGHT_INPUT)) {
|
||||||
|
const cardId = item.id;
|
||||||
const preset = (() => {
|
const preset = (() => {
|
||||||
try {
|
try {
|
||||||
const m = item.content.split('\n')?.[1];
|
const m = item.content.split('\n')?.[1];
|
||||||
@@ -822,6 +749,9 @@ export default function CoachScreen() {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// 初始化输入值(如果还没有的话)
|
||||||
|
const currentValue = weightInputs[cardId] ?? preset;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ gap: 8 }}>
|
<View style={{ gap: 8 }}>
|
||||||
<Text style={[styles.bubbleText, { color: '#192126', fontWeight: '700' }]}>记录今日体重</Text>
|
<Text style={[styles.bubbleText, { color: '#192126', fontWeight: '700' }]}>记录今日体重</Text>
|
||||||
@@ -829,10 +759,11 @@ export default function CoachScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder="例如 60.5"
|
placeholder="例如 60.5"
|
||||||
keyboardType="decimal-pad"
|
keyboardType="decimal-pad"
|
||||||
defaultValue={preset}
|
value={currentValue}
|
||||||
placeholderTextColor={'#687076'}
|
placeholderTextColor={'#687076'}
|
||||||
style={styles.weightInput}
|
style={styles.weightInput}
|
||||||
onSubmitEditing={(e) => handleSubmitWeight(e.nativeEvent.text)}
|
onChangeText={(text) => setWeightInputs(prev => ({ ...prev, [cardId]: text }))}
|
||||||
|
onSubmitEditing={(e) => handleSubmitWeight(e.nativeEvent.text, cardId)}
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
submitBehavior="blurAndSubmit"
|
submitBehavior="blurAndSubmit"
|
||||||
/>
|
/>
|
||||||
@@ -840,9 +771,9 @@ export default function CoachScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={styles.weightSaveBtn}
|
style={styles.weightSaveBtn}
|
||||||
onPress={() => handleSubmitWeight(preset || '')}
|
onPress={() => handleSubmitWeight(currentValue, cardId)}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#192126', fontWeight: '700' }}>保存</Text>
|
<Text style={{ color: '#192126', fontWeight: '700' }}>记录</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={{ color: '#687076', fontSize: 12 }}>按回车或点击保存,即可将该体重同步到账户并发送到对话。</Text>
|
<Text style={{ color: '#687076', fontSize: 12 }}>按回车或点击保存,即可将该体重同步到账户并发送到对话。</Text>
|
||||||
@@ -850,7 +781,7 @@ export default function CoachScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.content?.startsWith('__DIET_INPUT_CARD__')) {
|
if (item.content?.startsWith(CardType.DIET_INPUT)) {
|
||||||
return (
|
return (
|
||||||
<View style={{ gap: 12 }}>
|
<View style={{ gap: 12 }}>
|
||||||
<Text style={[styles.bubbleText, { color: '#192126', fontWeight: '700' }]}>记录今日饮食</Text>
|
<Text style={[styles.bubbleText, { color: '#192126', fontWeight: '700' }]}>记录今日饮食</Text>
|
||||||
@@ -891,7 +822,7 @@ export default function CoachScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.content?.startsWith('__DIET_TEXT_INPUT__')) {
|
if (item.content?.startsWith(CardType.DIET_TEXT_INPUT)) {
|
||||||
const cardId = item.content.split('\n')?.[1] || '';
|
const cardId = item.content.split('\n')?.[1] || '';
|
||||||
const currentText = dietTextInputs[cardId] || '';
|
const currentText = dietTextInputs[cardId] || '';
|
||||||
|
|
||||||
@@ -917,7 +848,6 @@ export default function CoachScreen() {
|
|||||||
value={currentText}
|
value={currentText}
|
||||||
onChangeText={(text) => setDietTextInputs(prev => ({ ...prev, [cardId]: text }))}
|
onChangeText={(text) => setDietTextInputs(prev => ({ ...prev, [cardId]: text }))}
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
blurOnSubmit={false}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -944,12 +874,12 @@ export default function CoachScreen() {
|
|||||||
function insertWeightInputCard() {
|
function insertWeightInputCard() {
|
||||||
const id = `wcard_${Date.now()}`;
|
const id = `wcard_${Date.now()}`;
|
||||||
const preset = userProfile?.weight ? Number(userProfile.weight) : undefined;
|
const preset = userProfile?.weight ? Number(userProfile.weight) : undefined;
|
||||||
const payload = `__WEIGHT_INPUT_CARD__\n${preset ?? ''}`;
|
const payload = `${CardType.WEIGHT_INPUT}\n${preset ?? ''}`;
|
||||||
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
||||||
setTimeout(scrollToEnd, 100);
|
setTimeout(scrollToEnd, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmitWeight(text?: string) {
|
async function handleSubmitWeight(text?: string, cardId?: string) {
|
||||||
const val = parseFloat(String(text ?? '').trim());
|
const val = parseFloat(String(text ?? '').trim());
|
||||||
if (isNaN(val) || val <= 0 || val > 500) {
|
if (isNaN(val) || val <= 0 || val > 500) {
|
||||||
Alert.alert('请输入有效体重', '请填写合理的公斤数,例如 60.5');
|
Alert.alert('请输入有效体重', '请填写合理的公斤数,例如 60.5');
|
||||||
@@ -957,32 +887,19 @@ export default function CoachScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 本地更新
|
// 清理该卡片的输入状态
|
||||||
dispatch(updateProfile({ weight: String(val) }));
|
if (cardId) {
|
||||||
|
setWeightInputs(prev => {
|
||||||
|
const { [cardId]: _, ...rest } = prev;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
// 后端同步(尝试从不同可能的字段获取用户ID)
|
// 移除体重输入卡片
|
||||||
try {
|
setMessages((prev) => prev.filter(msg => msg.id !== cardId));
|
||||||
// 从后端响应的原始数据中查找可能的用户ID字段
|
|
||||||
const rawUserData = await api.get<any>('/api/users/info');
|
|
||||||
const userId = rawUserData?.id || rawUserData?.userId || rawUserData?._id ||
|
|
||||||
rawUserData?.user?.id || rawUserData?.user?.userId || rawUserData?.user?._id ||
|
|
||||||
rawUserData?.profile?.id || rawUserData?.profile?.userId || rawUserData?.profile?._id;
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
await updateUserApi({ userId, weight: val });
|
|
||||||
await dispatch(fetchMyProfile() as any);
|
|
||||||
// 刷新体重历史记录
|
|
||||||
await dispatch(fetchWeightHistory() as any);
|
|
||||||
} else {
|
|
||||||
console.warn('[AI_CHAT] No user ID found for weight sync');
|
|
||||||
}
|
|
||||||
} catch (syncError) {
|
|
||||||
console.warn('[AI_CHAT] Failed to sync weight to server:', syncError);
|
|
||||||
// 不阻断对话体验,但可以给用户一个提示
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在对话中插入"确认消息"并发送给教练
|
// 在对话中插入"确认消息"并发送给教练
|
||||||
const textMsg = `记录了今日体重:${val} kg。`;
|
const textMsg = `#记体重:\n\n${val} kg`;
|
||||||
await send(textMsg);
|
await send(textMsg);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[AI_CHAT] Error handling weight submission:', e);
|
console.error('[AI_CHAT] Error handling weight submission:', e);
|
||||||
@@ -992,14 +909,14 @@ export default function CoachScreen() {
|
|||||||
|
|
||||||
function insertDietInputCard() {
|
function insertDietInputCard() {
|
||||||
const id = `dcard_${Date.now()}`;
|
const id = `dcard_${Date.now()}`;
|
||||||
const payload = `__DIET_INPUT_CARD__\n${id}`;
|
const payload = `${CardType.DIET_INPUT}\n${id}`;
|
||||||
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
||||||
setTimeout(scrollToEnd, 100);
|
setTimeout(scrollToEnd, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDietTextInput(cardId: string) {
|
function handleDietTextInput(cardId: string) {
|
||||||
// 替换当前的饮食选择卡片为文字输入卡片
|
// 替换当前的饮食选择卡片为文字输入卡片
|
||||||
const payload = `__DIET_TEXT_INPUT__\n${cardId}`;
|
const payload = `${CardType.DIET_TEXT_INPUT}\n${cardId}`;
|
||||||
setMessages((prev) => prev.map(msg =>
|
setMessages((prev) => prev.map(msg =>
|
||||||
msg.id === cardId
|
msg.id === cardId
|
||||||
? { ...msg, content: payload }
|
? { ...msg, content: payload }
|
||||||
@@ -1008,9 +925,14 @@ export default function CoachScreen() {
|
|||||||
setTimeout(scrollToEnd, 100);
|
setTimeout(scrollToEnd, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDietPhotoInput(cardId: string) {
|
function handleDietPhotoInput(cardId: string) {
|
||||||
|
console.log('[DIET] handleDietPhotoInput called with cardId:', cardId);
|
||||||
|
setCurrentCardId(cardId);
|
||||||
|
setShowDietPhotoActionSheet(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCameraPhoto() {
|
||||||
try {
|
try {
|
||||||
// 使用现有的拍照功能
|
|
||||||
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
|
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
|
||||||
if (permissionResult.status !== 'granted') {
|
if (permissionResult.status !== 'granted') {
|
||||||
Alert.alert('权限不足', '需要相机权限以拍摄食物照片');
|
Alert.alert('权限不足', '需要相机权限以拍摄食物照片');
|
||||||
@@ -1025,24 +947,7 @@ export default function CoachScreen() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.canceled && result.assets?.[0]) {
|
if (!result.canceled && result.assets?.[0]) {
|
||||||
const asset = result.assets[0];
|
await processSelectedImage(result.assets[0]);
|
||||||
try {
|
|
||||||
// 上传图片
|
|
||||||
const { url } = await upload(
|
|
||||||
{ uri: asset.uri, name: `diet-${Date.now()}.jpg`, type: 'image/jpeg' },
|
|
||||||
{ prefix: 'images/diet' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 移除饮食选择卡片
|
|
||||||
setMessages((prev) => prev.filter(msg => msg.id !== cardId));
|
|
||||||
|
|
||||||
// 发送包含图片的饮食记录消息
|
|
||||||
const dietMsg = `拍摄了食物照片,请Health Bot帮我分析这餐的营养成分、热量和健康建议:\n\n`;
|
|
||||||
await send(dietMsg);
|
|
||||||
} catch (uploadError) {
|
|
||||||
console.error('[DIET] 图片上传失败:', uploadError);
|
|
||||||
Alert.alert('上传失败', '图片上传失败,请重试');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[DIET] 拍照失败:', e);
|
console.error('[DIET] 拍照失败:', e);
|
||||||
@@ -1050,9 +955,55 @@ export default function CoachScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleLibraryPhoto() {
|
||||||
|
try {
|
||||||
|
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
|
if (permissionResult.status !== 'granted') {
|
||||||
|
Alert.alert('权限不足', '需要相册权限以选择食物照片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: ['images'],
|
||||||
|
allowsEditing: true,
|
||||||
|
quality: 0.9,
|
||||||
|
aspect: [4, 3],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.assets?.[0]) {
|
||||||
|
await processSelectedImage(result.assets[0]);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[DIET] 选择照片失败:', e);
|
||||||
|
Alert.alert('选择照片失败', e?.message || '选择照片失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processSelectedImage(asset: ImagePicker.ImagePickerAsset) {
|
||||||
|
if (!currentCardId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 上传图片
|
||||||
|
const { url } = await upload(
|
||||||
|
{ uri: asset.uri, name: `diet-${Date.now()}.jpg`, type: 'image/jpeg' },
|
||||||
|
{ prefix: 'images/diet' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 移除饮食选择卡片
|
||||||
|
setMessages((prev) => prev.filter(msg => msg.id !== currentCardId));
|
||||||
|
|
||||||
|
// 发送包含图片的饮食记录消息
|
||||||
|
const dietMsg = `#记饮食:\n\n`;
|
||||||
|
await send(dietMsg);
|
||||||
|
} catch (uploadError) {
|
||||||
|
console.error('[DIET] 图片上传失败:', uploadError);
|
||||||
|
Alert.alert('上传失败', '图片上传失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleBackToDietOptions(cardId: string) {
|
function handleBackToDietOptions(cardId: string) {
|
||||||
// 返回到饮食选择界面
|
// 返回到饮食选择界面
|
||||||
const payload = `__DIET_INPUT_CARD__\n${cardId}`;
|
const payload = `${CardType.DIET_INPUT}\n${cardId}`;
|
||||||
setMessages((prev) => prev.map(msg =>
|
setMessages((prev) => prev.map(msg =>
|
||||||
msg.id === cardId
|
msg.id === cardId
|
||||||
? { ...msg, content: payload }
|
? { ...msg, content: payload }
|
||||||
@@ -1232,7 +1183,7 @@ export default function CoachScreen() {
|
|||||||
onChangeText={setInput}
|
onChangeText={setInput}
|
||||||
multiline
|
multiline
|
||||||
onSubmitEditing={() => send(input)}
|
onSubmitEditing={() => send(input)}
|
||||||
blurOnSubmit={false}
|
submitBehavior="blurAndSubmit"
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
@@ -1257,7 +1208,7 @@ export default function CoachScreen() {
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={scrollToEnd}
|
onPress={scrollToEnd}
|
||||||
style={[styles.scrollToBottomFab, {
|
style={[styles.scrollToBottomFab, {
|
||||||
bottom: composerHeight + keyboardOffset + 10,
|
bottom: composerHeight + keyboardOffset + 10,
|
||||||
backgroundColor: theme.primary
|
backgroundColor: theme.primary
|
||||||
}]}
|
}]}
|
||||||
>
|
>
|
||||||
@@ -1320,6 +1271,16 @@ export default function CoachScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<ActionSheet
|
||||||
|
visible={showDietPhotoActionSheet}
|
||||||
|
onClose={() => setShowDietPhotoActionSheet(false)}
|
||||||
|
title="选择图片来源"
|
||||||
|
options={[
|
||||||
|
{ id: 'camera', title: '拍照', onPress: handleCameraPhoto },
|
||||||
|
{ id: 'library', title: '从相册选择', onPress: handleLibraryPhoto },
|
||||||
|
{ id: 'cancel', title: '取消', onPress: () => setShowDietPhotoActionSheet(false) }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useRouter } from 'expo-router';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Animated, Image, PanResponder, Pressable, SafeAreaView, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
|
import { Animated, Image, PanResponder, Pressable, SafeAreaView, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { ROUTES, QUERY_PARAMS, ROUTE_PARAMS } from '@/constants/Routes';
|
||||||
|
|
||||||
// 移除旧的“热门活动”滑动数据,改为固定的“热点功能”卡片
|
// 移除旧的“热门活动”滑动数据,改为固定的“热点功能”卡片
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ export default function HomeScreen() {
|
|||||||
Animated.spring(pan, { toValue: { x: snapX, y: clampedY }, useNativeDriver: false, bounciness: 6 }).start(() => {
|
Animated.spring(pan, { toValue: { x: snapX, y: clampedY }, useNativeDriver: false, bounciness: 6 }).start(() => {
|
||||||
if (!dragState.current.moved) {
|
if (!dragState.current.moved) {
|
||||||
// 切换到教练 tab,并传递name参数
|
// 切换到教练 tab,并传递name参数
|
||||||
router.push('/coach?name=Iris' as any);
|
router.push(`${ROUTES.TAB_COACH}?${QUERY_PARAMS.COACH_NAME}=Iris` as any);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -113,7 +114,7 @@ export default function HomeScreen() {
|
|||||||
title: '今日训练',
|
title: '今日训练',
|
||||||
subtitle: '完成一次普拉提训练,记录你的坚持',
|
subtitle: '完成一次普拉提训练,记录你的坚持',
|
||||||
level: '初学者',
|
level: '初学者',
|
||||||
onPress: () => pushIfAuthedElseLogin('/workout/today'),
|
onPress: () => pushIfAuthedElseLogin(ROUTES.WORKOUT_TODAY),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'plan',
|
type: 'plan',
|
||||||
@@ -123,7 +124,7 @@ export default function HomeScreen() {
|
|||||||
title: '体态评估',
|
title: '体态评估',
|
||||||
subtitle: '评估你的体态,制定训练计划',
|
subtitle: '评估你的体态,制定训练计划',
|
||||||
level: '初学者',
|
level: '初学者',
|
||||||
onPress: () => router.push('/ai-posture-assessment'),
|
onPress: () => router.push(ROUTES.AI_POSTURE_ASSESSMENT),
|
||||||
},
|
},
|
||||||
...listRecommendedArticles().map((a) => ({
|
...listRecommendedArticles().map((a) => ({
|
||||||
type: 'article' as const,
|
type: 'article' as const,
|
||||||
@@ -185,7 +186,7 @@ export default function HomeScreen() {
|
|||||||
image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
|
image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
|
||||||
title: c.title || '今日训练',
|
title: c.title || '今日训练',
|
||||||
subtitle: c.subtitle || '完成一次普拉提训练,记录你的坚持',
|
subtitle: c.subtitle || '完成一次普拉提训练,记录你的坚持',
|
||||||
onPress: () => pushIfAuthedElseLogin('/workout/today'),
|
onPress: () => pushIfAuthedElseLogin(ROUTES.WORKOUT_TODAY),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +205,7 @@ export default function HomeScreen() {
|
|||||||
const handlePlanCardPress = () => {
|
const handlePlanCardPress = () => {
|
||||||
if (activePlan) {
|
if (activePlan) {
|
||||||
// 跳转到训练计划页面的锻炼tab,并传递planId参数
|
// 跳转到训练计划页面的锻炼tab,并传递planId参数
|
||||||
router.push(`/training-plan?planId=${activePlan.id}&tab=schedule` as any);
|
router.push(`${ROUTES.TRAINING_PLAN}?${ROUTE_PARAMS.TRAINING_PLAN_ID}=${activePlan.id}&${ROUTE_PARAMS.TRAINING_PLAN_TAB}=${QUERY_PARAMS.TRAINING_PLAN_TAB_SCHEDULE}` as any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,7 +276,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.featureCard, styles.featureCardQuinary]}
|
style={[styles.featureCard, styles.featureCardQuinary]}
|
||||||
onPress={() => pushIfAuthedElseLogin('/workout/today')}
|
onPress={() => pushIfAuthedElseLogin(ROUTES.WORKOUT_TODAY)}
|
||||||
>
|
>
|
||||||
<View style={styles.featureIconWrapper}>
|
<View style={styles.featureIconWrapper}>
|
||||||
<View style={styles.featureIconPlaceholder}>
|
<View style={styles.featureIconPlaceholder}>
|
||||||
@@ -290,7 +291,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.featureCard, styles.featureCardPrimary]}
|
style={[styles.featureCard, styles.featureCardPrimary]}
|
||||||
onPress={() => pushIfAuthedElseLogin('/ai-posture-assessment')}
|
onPress={() => pushIfAuthedElseLogin(ROUTES.AI_POSTURE_ASSESSMENT)}
|
||||||
>
|
>
|
||||||
<View style={styles.featureIconWrapper}>
|
<View style={styles.featureIconWrapper}>
|
||||||
<Image
|
<Image
|
||||||
@@ -303,7 +304,7 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.featureCard, styles.featureCardQuaternary]}
|
style={[styles.featureCard, styles.featureCardQuaternary]}
|
||||||
onPress={() => pushIfAuthedElseLogin('/training-plan')}
|
onPress={() => pushIfAuthedElseLogin(ROUTES.TRAINING_PLAN)}
|
||||||
>
|
>
|
||||||
<View style={styles.featureIconWrapper}>
|
<View style={styles.featureIconWrapper}>
|
||||||
<View style={styles.featureIconPlaceholder}>
|
<View style={styles.featureIconPlaceholder}>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Toast from 'react-native-toast-message';
|
|||||||
|
|
||||||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useAppDispatch } from '@/hooks/redux';
|
|||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { login } from '@/store/userSlice';
|
import { login } from '@/store/userSlice';
|
||||||
import Toast from 'react-native-toast-message';
|
import Toast from 'react-native-toast-message';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -23,16 +24,16 @@ export default function WelcomeScreen() {
|
|||||||
const textColor = useThemeColor({}, 'text');
|
const textColor = useThemeColor({}, 'text');
|
||||||
|
|
||||||
const handleGetStarted = () => {
|
const handleGetStarted = () => {
|
||||||
router.push('/onboarding/personal-info');
|
router.push(ROUTES.ONBOARDING_PERSONAL_INFO);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSkip = async () => {
|
const handleSkip = async () => {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem('@onboarding_completed', 'true');
|
await AsyncStorage.setItem('@onboarding_completed', 'true');
|
||||||
router.replace('/(tabs)');
|
router.replace(ROUTES.TAB_HOME);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存引导状态失败:', error);
|
console.error('保存引导状态失败:', error);
|
||||||
router.replace('/(tabs)');
|
router.replace(ROUTES.TAB_HOME);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
startWorkoutSession
|
startWorkoutSession
|
||||||
} from '@/store/workoutSlice';
|
} from '@/store/workoutSlice';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
@@ -276,7 +277,7 @@ export default function TodayWorkoutScreen() {
|
|||||||
iconColor: '#10B981',
|
iconColor: '#10B981',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
// 跳转到创建页面选择训练计划
|
// 跳转到创建页面选择训练计划
|
||||||
router.push('/workout/create-session');
|
router.push(ROUTES.WORKOUT_CREATE_SESSION);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -478,7 +479,7 @@ export default function TodayWorkoutScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.sessionCardContent}
|
style={styles.sessionCardContent}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push(`/workout/session/${item.id}`);
|
router.push(`${ROUTES.WORKOUT_SESSION}/${item.id}`);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import dayjs from 'dayjs';
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
|
import { ROUTES } from '@/constants/Routes';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -14,7 +15,7 @@ type Props = {
|
|||||||
export function ArticleCard({ id, title, coverImage, publishedAt, readCount }: Props) {
|
export function ArticleCard({ id, title, coverImage, publishedAt, readCount }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={() => router.push(`/article/${id}`)} style={styles.card}>
|
<Pressable onPress={() => router.push(`${ROUTES.ARTICLE}/${id}`)} style={styles.card}>
|
||||||
<Image source={{ uri: coverImage }} style={styles.cover} />
|
<Image source={{ uri: coverImage }} style={styles.cover} />
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
<Text style={styles.title} numberOfLines={2}>{title}</Text>
|
<Text style={styles.title} numberOfLines={2}>{title}</Text>
|
||||||
|
|||||||
68
constants/Routes.ts
Normal file
68
constants/Routes.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// 应用路由常量定义
|
||||||
|
export const ROUTES = {
|
||||||
|
// Tab路由
|
||||||
|
TAB_HOME: '/',
|
||||||
|
TAB_COACH: '/coach',
|
||||||
|
TAB_STATISTICS: '/statistics',
|
||||||
|
TAB_PERSONAL: '/personal',
|
||||||
|
|
||||||
|
// 训练相关路由
|
||||||
|
WORKOUT_TODAY: '/workout/today',
|
||||||
|
WORKOUT_CREATE_SESSION: '/workout/create-session',
|
||||||
|
WORKOUT_SESSION: '/workout/session',
|
||||||
|
|
||||||
|
// 训练计划相关路由
|
||||||
|
TRAINING_PLAN: '/training-plan',
|
||||||
|
|
||||||
|
// 体态评估路由
|
||||||
|
AI_POSTURE_ASSESSMENT: '/ai-posture-assessment',
|
||||||
|
|
||||||
|
// 挑战路由
|
||||||
|
CHALLENGE: '/challenge',
|
||||||
|
CHALLENGE_DAY: '/challenge/day',
|
||||||
|
|
||||||
|
// 文章路由
|
||||||
|
ARTICLE: '/article',
|
||||||
|
|
||||||
|
// 用户相关路由
|
||||||
|
AUTH_LOGIN: '/auth/login',
|
||||||
|
PROFILE_EDIT: '/profile/edit',
|
||||||
|
PROFILE_GOALS: '/profile/goals',
|
||||||
|
|
||||||
|
// 法律相关路由
|
||||||
|
LEGAL_USER_AGREEMENT: '/legal/user-agreement',
|
||||||
|
LEGAL_PRIVACY_POLICY: '/legal/privacy-policy',
|
||||||
|
|
||||||
|
// 引导页路由
|
||||||
|
ONBOARDING: '/onboarding',
|
||||||
|
ONBOARDING_PERSONAL_INFO: '/onboarding/personal-info',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 路由参数常量
|
||||||
|
export const ROUTE_PARAMS = {
|
||||||
|
// 训练会话参数
|
||||||
|
WORKOUT_SESSION_ID: 'id',
|
||||||
|
|
||||||
|
// 训练计划参数
|
||||||
|
TRAINING_PLAN_ID: 'planId',
|
||||||
|
TRAINING_PLAN_TAB: 'tab',
|
||||||
|
|
||||||
|
// 挑战日参数
|
||||||
|
CHALLENGE_DAY: 'day',
|
||||||
|
|
||||||
|
// 文章参数
|
||||||
|
ARTICLE_ID: 'id',
|
||||||
|
|
||||||
|
// 重定向参数
|
||||||
|
REDIRECT_TO: 'redirectTo',
|
||||||
|
REDIRECT_PARAMS: 'redirectParams',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 查询参数常量
|
||||||
|
export const QUERY_PARAMS = {
|
||||||
|
// 训练计划查询参数
|
||||||
|
TRAINING_PLAN_TAB_SCHEDULE: 'schedule',
|
||||||
|
|
||||||
|
// 教练页面参数
|
||||||
|
COACH_NAME: 'name',
|
||||||
|
} as const;
|
||||||
Reference in New Issue
Block a user