From 3312250f2db7843b9aa2c078e43d7df6f5de9035 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 15 Aug 2025 21:38:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=95=99=E7=BB=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=92=8C=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增教练页面,用户可以与教练进行互动和咨询 - 更新首页,切换到教练 tab 并传递名称参数 - 优化个人信息页面,添加注销帐号和退出登录功能 - 更新隐私政策和用户协议的链接,确保用户在使用前同意相关条款 - 修改今日训练页面标题为“开始训练”,提升用户体验 - 删除不再使用的进度条组件,简化代码结构 --- app/(tabs)/_layout.tsx | 31 + app/(tabs)/coach.tsx | 1158 +++++++++++++++++++++++++++ app/(tabs)/index.tsx | 36 +- app/(tabs)/personal.tsx | 111 ++- app/auth/login.tsx | 27 +- app/index.tsx | 25 +- app/workout/today.tsx | 2 +- assets/images/icons/iconWorkout.png | Bin 0 -> 65741 bytes components/PlanCard.tsx | 12 +- components/PrivacyConsentModal.tsx | 19 +- components/ui/IconSymbol.tsx | 1 + constants/Agree.ts | 4 + hooks/useAuthGuard.ts | 89 +- 13 files changed, 1385 insertions(+), 130 deletions(-) create mode 100644 app/(tabs)/coach.tsx create mode 100644 assets/images/icons/iconWorkout.png create mode 100644 constants/Agree.ts diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 31aab2f..cca2d01 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -18,6 +18,7 @@ export default function TabLayout() { screenOptions={({ route }) => { const routeName = route.name; const isSelected = (routeName === 'index' && pathname === '/') || + (routeName === 'coach' && pathname === '/coach') || (routeName === 'explore' && pathname === '/explore') || pathname.includes(routeName); @@ -39,6 +40,8 @@ export default function TabLayout() { switch (routeName) { case 'index': return { icon: 'house.fill', title: '首页' } as const; + case 'coach': + return { icon: 'person.3.fill', title: '教练' } as const; case 'explore': return { icon: 'paperplane.fill', title: '探索' } as const; case 'personal': @@ -150,6 +153,34 @@ export default function TabLayout() { }, }} /> + { + const isCoachSelected = pathname === '/coach'; + return ( + + + {isCoachSelected && ( + + 教练 + + )} + + ); + }, + }} + /> (); + const insets = useSafeAreaInsets(); + + const { isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); + // 为了让页面更贴近品牌主题与更亮的观感,这里使用亮色系配色 + const theme = Colors.light; + const coachName = (params?.name || 'Sarah').toString(); + const [input, setInput] = useState(''); + const [isSending, setIsSending] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [conversationId, setConversationId] = useState(undefined); + const [messages, setMessages] = useState([{ + id: 'm_welcome', + role: 'assistant', + content: `你好,我是你的普拉提教练 ${coachName}。可以向我咨询训练、体态、康复、柔韧等问题~`, + }]); + const [historyVisible, setHistoryVisible] = useState(false); + const [historyLoading, setHistoryLoading] = useState(false); + const [historyPage, setHistoryPage] = useState(1); + const [historyTotal, setHistoryTotal] = useState(0); + const [historyItems, setHistoryItems] = useState([]); + const listRef = useRef>(null); + const [isAtBottom, setIsAtBottom] = useState(true); + const didInitialScrollRef = useRef(false); + const [composerHeight, setComposerHeight] = useState(80); + const shouldAutoScrollRef = useRef(false); + const [keyboardOffset, setKeyboardOffset] = useState(0); + const [headerHeight, setHeaderHeight] = useState(60); + const pendingAssistantIdRef = useRef(null); + const [selectedImages, setSelectedImages] = useState>([]); + const [previewImageUri, setPreviewImageUri] = useState(null); + + const planDraft = useAppSelector((s) => s.trainingPlan?.draft); + const checkin = useAppSelector((s) => (s as any).checkin); + const dispatch = useAppDispatch(); + const userProfile = useAppSelector((s) => (s as any)?.user?.profile); + + const chips = useMemo(() => [ + { key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') }, + { key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() }, + { key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() }, + { key: 'weight', label: '记体重', action: () => insertWeightInputCard() }, + ], [router, planDraft, checkin]); + + const scrollToEnd = useCallback(() => { + requestAnimationFrame(() => { + listRef.current?.scrollToEnd({ animated: true }); + }); + }, []); + + const handleScroll = useCallback((e: any) => { + try { + const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent || {}; + const paddingToBottom = 60; + const distanceFromBottom = (contentSize?.height || 0) - ((layoutMeasurement?.height || 0) + (contentOffset?.y || 0)); + setIsAtBottom(distanceFromBottom <= paddingToBottom); + } catch { } + }, []); + + useEffect(() => { + // 初次进入或恢复时,保持最新消息可见 + scrollToEnd(); + }, [scrollToEnd]); + // 启动页面时尝试恢复当次应用会话缓存 + useEffect(() => { + (async () => { + try { + const cached = await loadAiCoachSessionCache(); + if (cached && Array.isArray(cached.messages) && cached.messages.length > 0) { + setConversationId(cached.conversationId); + setMessages(cached.messages as any); + setTimeout(scrollToEnd, 0); + } + } catch { } + })(); + }, [scrollToEnd]); + + // 会话变动时,轻量防抖写入缓存(在本次应用生命周期内可跨页面恢复;下次冷启动会被根布局清空) + const saveCacheTimerRef = useRef | null>(null); + useEffect(() => { + if (saveCacheTimerRef.current) clearTimeout(saveCacheTimerRef.current); + saveCacheTimerRef.current = setTimeout(() => { + saveAiCoachSessionCache({ conversationId, messages: messages as any, updatedAt: Date.now() }).catch(() => { }); + }, 150); + return () => { if (saveCacheTimerRef.current) clearTimeout(saveCacheTimerRef.current); }; + // 仅在 messages 或 conversationId 变化时触发 + }, [messages, conversationId]); + + + // 取消对 messages.length 的全局监听滚动,改为在"消息实际追加完成"后再判断与滚动,避免突兀与多次触发 + + useEffect(() => { + // 输入区高度变化时,若用户在底部则轻柔跟随一次 + if (isAtBottom) { + const id = setTimeout(scrollToEnd, 0); + return () => clearTimeout(id); + } + }, [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); } + }); + hideSub = Keyboard.addListener('keyboardWillHide', () => 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); + + useEffect(() => { + return () => { + try { streamAbortRef.current?.abort(); } catch { } + }; + }, []); + + function ensureConversationId(): string { + if (conversationId && conversationId.trim()) return conversationId; + const cid = `mobile-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + setConversationId(cid); + try { console.log('[AI_CHAT][ui] create temp conversationId', cid); } catch { } + return cid; + } + + function convertToServerMessages(history: ChatMessage[]): Array<{ role: 'user' | 'assistant' | 'system'; content: string }> { + // 仅映射 user/assistant 消息;系统提示由后端自动注入 + return history + .filter((m) => m.role === 'user' || m.role === 'assistant') + .map((m) => ({ role: m.role, content: m.content })); + } + + async function openHistory() { + if (isStreaming) { + try { streamAbortRef.current?.abort(); } catch { } + } + setHistoryVisible(true); + await refreshHistory(1); + } + + async function refreshHistory(page = 1) { + try { + setHistoryLoading(true); + const resp = await listConversations(page, 20); + setHistoryPage(resp.page); + setHistoryTotal(resp.total); + setHistoryItems(resp.items || []); + } catch (e) { + Alert.alert('错误', (e as any)?.message || '获取会话列表失败'); + } finally { + setHistoryLoading(false); + } + } + + async function handleSelectConversation(id: string) { + try { + if (isStreaming) { + try { streamAbortRef.current?.abort(); } catch { } + } + const detail = await getConversationDetail(id); + if (!detail || !(detail as any).messages) { + Alert.alert('提示', '会话不存在或已删除'); + return; + } + const mapped: ChatMessage[] = (detail.messages || []) + .filter((m) => m.role === 'user' || m.role === 'assistant') + .map((m, idx) => ({ id: `${m.role}_${idx}_${Date.now()}`, role: m.role as Role, content: m.content || '' })); + setConversationId(detail.conversationId); + setMessages(mapped.length ? mapped : [{ id: 'm_welcome', role: 'assistant', content: `你好,我是你的普拉提教练 ${coachName}。可以向我咨询训练、体态、康复、柔韧等问题~` }]); + setHistoryVisible(false); + setTimeout(scrollToEnd, 0); + } catch (e) { + Alert.alert('错误', (e as any)?.message || '加载会话失败'); + } + } + + function confirmDeleteConversation(id: string) { + Alert.alert('删除会话', '删除后将无法恢复,确定要删除该会话吗?', [ + { text: '取消', style: 'cancel' }, + { + text: '删除', style: 'destructive', onPress: async () => { + try { + await deleteConversation(id); + if (conversationId === id) { + setConversationId(undefined); + setMessages([{ id: 'm_welcome', role: 'assistant', content: `你好,我是你的普拉提教练 ${coachName}。可以向我咨询训练、体态、康复、柔韧等问题~` }]); + } + await refreshHistory(historyPage); + } catch (e) { + Alert.alert('错误', (e as any)?.message || '删除失败'); + } + } + } + ]); + } + + async function sendStream(text: string) { + const tokenExists = !!getAuthToken(); + try { console.log('[AI_CHAT][ui] send start', { tokenExists, conversationId, textPreview: text.slice(0, 50) }); } catch { } + + // 终止上一次未完成的流 + if (streamAbortRef.current) { + try { console.log('[AI_CHAT][ui] abort previous stream'); } catch { } + try { streamAbortRef.current.abort(); } catch { } + streamAbortRef.current = null; + } + + // 发送 body:尽量提供历史消息,后端会优先使用 conversationId 关联上下文 + const historyForServer = convertToServerMessages(messages); + const cid = ensureConversationId(); + const body = { + conversationId: cid, + messages: [...historyForServer, { role: 'user' as const, content: text }], + stream: true, + }; + + // 在 UI 中先放置占位回答,随后持续增量更新 + const assistantId = `a_${Date.now()}`; + const userMsgId = `u_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + + const userMsg: ChatMessage = { id: userMsgId, role: 'user', content: text }; + shouldAutoScrollRef.current = isAtBottom; + setMessages((m) => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]); + pendingAssistantIdRef.current = assistantId; + + setIsSending(true); + setIsStreaming(true); + + let receivedAnyChunk = false; + + const updateAssistantContent = (delta: string) => { + setMessages((prev) => { + const next = prev.map((msg) => { + if (msg.id === assistantId) { + return { ...msg, content: msg.content + delta }; + } + return msg; + }); + return next; + }); + }; + + const onChunk = (chunk: string) => { + receivedAnyChunk = true; + const atBottomNow = isAtBottom; + updateAssistantContent(chunk); + if (atBottomNow) { + // 在底部时,持续开启自动滚动,并主动触发一次滚动以避免极小增量未触发 onContentSizeChange 的情况 + shouldAutoScrollRef.current = true; + setTimeout(scrollToEnd, 0); + } + try { console.log('[AI_CHAT][api] chunk', { length: chunk.length, preview: chunk.slice(0, 40) }); } catch { } + }; + + const onEnd = (cidFromHeader?: string) => { + setIsSending(false); + setIsStreaming(false); + streamAbortRef.current = null; + if (cidFromHeader && !conversationId) setConversationId(cidFromHeader); + pendingAssistantIdRef.current = null; + try { console.log('[AI_CHAT][api] end', { cidFromHeader, hadChunks: receivedAnyChunk }); } catch { } + }; + + const onError = async (err: any) => { + try { console.warn('[AI_CHAT][api] error', err); } catch { } + setIsSending(false); + setIsStreaming(false); + streamAbortRef.current = null; + pendingAssistantIdRef.current = null; + // 流式失败时的降级:尝试一次性非流式 + try { + const bodyNoStream = { ...body, stream: false }; + try { console.log('[AI_CHAT][fallback] try non-stream'); } catch { } + const resp = await api.post<{ conversationId?: string; text: string }>('/api/ai-coach/chat', bodyNoStream); + const textCombined = (resp as any)?.text ?? ''; + if ((resp as any)?.conversationId && !conversationId) { + setConversationId((resp as any).conversationId); + } + setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: textCombined || '(空响应)' } : msg)); + } catch (e2: any) { + setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: '抱歉,请求失败,请稍后再试。' } : msg)); + try { console.warn('[AI_CHAT][fallback] non-stream error', e2); } catch { } + } + }; + + try { + const controller = postTextStream('/api/ai-coach/chat', body, { onChunk, onEnd, onError }, { timeoutMs: 120000 }); + streamAbortRef.current = controller; + } catch (e) { + onError(e); + } + } + + async function send(text: string) { + if (!isLoggedIn) { + pushIfAuthedElseLogin('/auth/login'); + return; + } + if (isSending) return; + const trimmed = text.trim(); + if (!trimmed && selectedImages.length === 0) return; + + async function ensureImagesUploaded(): Promise { + 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) => `![image](${u})`).join('\n\n'); + const composed = [trimmed, mdImages].filter(Boolean).join('\n\n'); + setInput(''); + setSelectedImages([]); + await sendStream(composed); + } catch (e: any) { + Alert.alert('上传失败', e?.message || '图片上传失败,请稍后重试'); + } + } + + function handleQuickPlan() { + const goalMap: Record = { + 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 { + const entries = Object.values(checkin?.byDate || {}) as CheckinRecord[]; + if (!entries.length) return ''; + const recent = entries.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 = {}; + const exerciseCount: Record = {}; + for (const rec of recent) { + if (!rec?.items?.length) continue; + totalSessions += 1; + for (const it of rec.items) { + totalExercises += 1; + if (it.completed) totalCompleted += 1; + categoryCount[it.category] = (categoryCount[it.category] || 0) + 1; + 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'); + } + + function handleAnalyzeRecords() { + const summary = buildTrainingSummary(); + if (!summary) { + send('我还没有可分析的打卡记录,请先在"每日打卡"添加并完成一些训练记录,然后帮我分析近期训练表现与改进建议。'); + return; + } + const prompt = `请基于以下我的近期训练记录进行分析,输出:1)整体训练负荷与节奏;2)动作与肌群的均衡性(指出偏多/偏少);3)容易忽视的恢复与热身建议;4)后续一周的优化建议(频次/时长/动作方向)。\n\n${summary}`; + 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 }) { + const isUser = item.role === 'user'; + return ( + + {!isUser && ( + + )} + + {renderBubbleContent(item)} + + {false} + + ); + } + + function renderBubbleContent(item: ChatMessage) { + if (!item.content?.trim() && isStreaming && pendingAssistantIdRef.current === item.id) { + return 正在思考…; + } + 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 ( + + 记录今日体重 + + handleSubmitWeight(e.nativeEvent.text)} + returnKeyType="done" + submitBehavior="blurAndSubmit" + /> + kg + handleSubmitWeight((preset || '').toString())}> + 保存 + + + 按回车或点击保存,即可将该体重同步到账户并发送到对话。 + + ); + } + return ( + + {item.content} + + ); + } + + 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: String(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 ( + + {/* 顶部标题区域,只显示教练名称和历史按钮 */} + { + const h = e.nativeEvent.layout.height; + if (h && Math.abs(h - headerHeight) > 0.5) setHeaderHeight(h); + }} + > + 教练 {coachName} + + + + + + {/* 消息列表容器 - 设置固定高度避免输入框重叠 */} + + m.id} + renderItem={renderItem} + onLayout={() => { + // 确保首屏布局后也尝试滚动 + if (!didInitialScrollRef.current) { + didInitialScrollRef.current = true; + setTimeout(scrollToEnd, 0); + requestAnimationFrame(scrollToEnd); + } + }} + contentContainerStyle={{ + paddingHorizontal: 14, + paddingTop: 8, + paddingBottom: 16 + }} + onContentSizeChange={() => { + // 首次内容变化强制滚底,其余仅在接近底部时滚动 + if (!didInitialScrollRef.current) { + didInitialScrollRef.current = true; + setTimeout(scrollToEnd, 0); + requestAnimationFrame(scrollToEnd); + return; + } + if (shouldAutoScrollRef.current) { + shouldAutoScrollRef.current = false; + setTimeout(scrollToEnd, 0); + } + }} + onScroll={handleScroll} + scrollEventThrottle={16} + showsVerticalScrollIndicator={false} + /> + + + { + const h = e.nativeEvent.layout.height; + if (h && Math.abs(h - composerHeight) > 0.5) setComposerHeight(h); + }} + > + + {chips.map((c) => ( + + {c.label} + + ))} + + + {!!selectedImages.length && ( + + {selectedImages.map((img) => ( + + setPreviewImageUri(img.uploadedUrl || img.localUri)}> + + + {!!(img.progress > 0 && img.progress < 1) && ( + + {Math.round((img.progress || 0) * 100)}% + + )} + removeSelectedImage(img.id)} style={styles.imageRemoveBtn}> + + + + ))} + + )} + + + + + + send(input)} + blurOnSubmit={false} + /> + send(input)} + style={[ + styles.sendBtn, + { backgroundColor: theme.primary, opacity: (input.trim() || selectedImages.length > 0) && !isSending ? 1 : 0.5 } + ]} + > + {isSending ? ( + + ) : ( + + )} + + + + + {!isAtBottom && ( + + + + )} + + setHistoryVisible(false)}> + setHistoryVisible(false)}> + + + 历史会话 + refreshHistory(historyPage)} style={styles.modalRefreshBtn}> + + + + {historyLoading ? ( + + + 加载中... + + ) : ( + + {historyItems.length === 0 ? ( + 暂无会话 + ) : ( + historyItems.map((it) => ( + + handleSelectConversation(it.conversationId)} + > + {it.title || '未命名会话'} + + {dayjs(it.lastMessageAt || it.createdAt).format('YYYY/MM/DD HH:mm')} + + + confirmDeleteConversation(it.conversationId)} style={styles.historyDeleteBtn}> + + + + )) + )} + + )} + + setHistoryVisible(false)} style={styles.modalCloseBtn}> + 关闭 + + + + + + setPreviewImageUri(null)}> + setPreviewImageUri(null)}> + + {previewImageUri ? ( + + ) : null} + + + + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingBottom: 10, + }, + headerTitle: { + fontSize: 20, + fontWeight: '800', + }, + historyButton: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + }, + row: { + flexDirection: 'row', + alignItems: 'flex-end', + gap: 8, + marginVertical: 6, + }, + avatar: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + }, + avatarText: { + color: '#192126', + fontSize: 12, + fontWeight: '800', + }, + bubble: { + maxWidth: '82%', + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 16, + }, + bubbleText: { + fontSize: 15, + 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: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + paddingTop: 8, + paddingHorizontal: 10, + borderTopWidth: 0, + }, + chipsRow: { + flexDirection: 'row', + gap: 8, + paddingHorizontal: 6, + marginBottom: 8, + }, + chipsRowScroll: { + marginBottom: 8, + }, + chip: { + paddingHorizontal: 10, + height: 34, + borderRadius: 18, + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'transparent', + }, + chipText: { + fontSize: 13, + 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: { + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderWidth: 1, + borderRadius: 16, + backgroundColor: 'rgba(0,0,0,0.04)' + }, + mediaBtn: { + width: 40, + height: 40, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + marginRight: 6, + }, + input: { + flex: 1, + fontSize: 15, + maxHeight: 120, + minHeight: 40, + paddingHorizontal: 8, + paddingVertical: 6, + textAlignVertical: 'center', + }, + sendBtn: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + }, + scrollToBottomFab: { + position: 'absolute', + right: 16, + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 4, + elevation: 2, + }, + modalBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.35)', + padding: 16, + justifyContent: 'flex-end', + }, + modalSheet: { + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + paddingHorizontal: 12, + paddingTop: 10, + paddingBottom: 12, + }, + modalHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 4, + paddingBottom: 8, + }, + modalTitle: { + fontSize: 16, + fontWeight: '800', + color: '#192126', + }, + modalRefreshBtn: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0,0,0,0.06)' + }, + historyRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 8, + borderRadius: 10, + }, + historyTitle: { + fontSize: 15, + color: '#192126', + fontWeight: '600', + }, + historyMeta: { + marginTop: 2, + fontSize: 12, + color: '#687076', + }, + historyDeleteBtn: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(255,68,68,0.08)' + }, + modalFooter: { + paddingTop: 8, + alignItems: 'flex-end', + }, + modalCloseBtn: { + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 10, + 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; diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index b316076..5637f32 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -75,9 +75,8 @@ export default function HomeScreen() { const snapX = distLeft <= distRight ? minX : maxX; Animated.spring(pan, { toValue: { x: snapX, y: clampedY }, useNativeDriver: false, bounciness: 6 }).start(() => { if (!dragState.current.moved) { - // Treat as tap - // @ts-ignore - expo-router string ok - router.push('/ai-coach-chat?name=Iris' as any); + // 切换到教练 tab,并传递name参数 + router.push('/coach?name=Iris' as any); } }); }, @@ -91,7 +90,6 @@ export default function HomeScreen() { title: string; subtitle: string; level?: '初学者' | '中级' | '高级'; - progress: number; onPress?: () => void; } | { @@ -115,7 +113,6 @@ export default function HomeScreen() { title: '今日训练', subtitle: '完成一次普拉提训练,记录你的坚持', level: '初学者', - progress: 0, onPress: () => pushIfAuthedElseLogin('/workout/today'), }, { @@ -126,7 +123,6 @@ export default function HomeScreen() { title: '体态评估', subtitle: '评估你的体态,制定训练计划', level: '初学者', - progress: 0, onPress: () => router.push('/ai-posture-assessment'), }, ...listRecommendedArticles().map((a) => ({ @@ -194,7 +190,6 @@ export default function HomeScreen() { image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg', title: c.title || '今日训练', subtitle: c.subtitle || '完成一次普拉提训练,记录你的坚持', - progress: 0, onPress: () => pushIfAuthedElseLogin('/workout/today'), }); } @@ -282,6 +277,22 @@ export default function HomeScreen() { 热点功能 + + pushIfAuthedElseLogin('/workout/today')} + > + + + + + + 训练 + + router.push('/ai-posture-assessment')} @@ -292,7 +303,7 @@ export default function HomeScreen() { style={styles.featureIconImage} /> - AI体态评估 + 体态 - 计划管理 + 计划 + + + @@ -344,7 +358,6 @@ export default function HomeScreen() { title={item.title} subtitle={item.subtitle} level={item.level} - progress={item.progress} /> ); return item.onPress ? ( @@ -490,6 +503,9 @@ const styles = StyleSheet.create({ featureCardQuaternary: { backgroundColor: '#fa709a', }, + featureCardQuinary: { + backgroundColor: '#f59e0b', + }, featureIconWrapper: { width: 32, height: 32, diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index 3b3789b..4594b75 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -1,23 +1,23 @@ +import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { api } from '@/services/api'; -import { DEFAULT_MEMBER_NAME, fetchMyProfile, logout } from '@/store/userSlice'; +import { DEFAULT_MEMBER_NAME, fetchMyProfile } from '@/store/userSlice'; import { Ionicons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useFocusEffect } from '@react-navigation/native'; -import type { Href } from 'expo-router'; -import { router } from 'expo-router'; import React, { useMemo, useState } from 'react'; -import { Alert, Image, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'; +import { Alert, Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg'; export default function PersonalScreen() { const dispatch = useAppDispatch(); + const { confirmLogout, confirmDeleteAccount, isLoggedIn, pushIfAuthedElseLogin } = useAuthGuard(); const insets = useSafeAreaInsets(); const tabBarHeight = useBottomTabBarHeight(); const colorScheme = useColorScheme(); @@ -95,32 +95,7 @@ export default function PersonalScreen() { - const handleDeleteAccount = () => { - Alert.alert( - '确认注销帐号', - '此操作不可恢复,将删除您的帐号及相关数据。确定继续吗?', - [ - { text: '取消', style: 'cancel' }, - { - text: '确认注销', - style: 'destructive', - onPress: async () => { - try { - await api.delete('/api/users/delete-account'); - await AsyncStorage.multiRemove(['@user_personal_info']); - await dispatch(logout()).unwrap(); - Alert.alert('帐号已注销', '您的帐号已成功注销'); - router.replace('/auth/login'); - } catch (err: any) { - const message = err?.message || '注销失败,请稍后重试'; - Alert.alert('注销失败', message); - } - }, - }, - ], - { cancelable: true } - ); - }; + const UserInfoSection = () => ( @@ -138,7 +113,7 @@ export default function PersonalScreen() { {/* 编辑按钮 */} - router.push('/profile/edit')}> + pushIfAuthedElseLogin('/profile/edit')}> 编辑 @@ -170,7 +145,8 @@ export default function PersonalScreen() { {item.type === 'switch' ? ( { + if (!isLoggedIn) { + pushIfAuthedElseLogin('/profile/notification-settings'); + return; + } + setNotificationEnabled(value); + }} trackColor={{ false: '#E5E5E5', true: colors.primary }} thumbColor="#FFFFFF" style={styles.switch} @@ -243,12 +225,20 @@ export default function PersonalScreen() { { icon: 'flag-outline' as const, title: '目标管理', - onPress: () => router.push('/profile/goals' as Href), - }, - { - icon: 'stats-chart-outline' as const, - title: '训练进度', + onPress: () => pushIfAuthedElseLogin('/profile/goals'), }, + // { + // icon: 'stats-chart-outline' as const, + // title: '训练进度', + // onPress: () => { + // // 训练进度页面暂未实现,先显示提示 + // if (isLoggedIn) { + // Alert.alert('提示', '训练进度功能正在开发中'); + // } else { + // pushIfAuthedElseLogin('/profile/training-progress'); + // } + // }, + // }, ], }, { @@ -264,42 +254,36 @@ export default function PersonalScreen() { { title: '其他', items: [ - { - icon: 'mail-outline' as const, - title: '联系我们', - }, { icon: 'shield-checkmark-outline' as const, title: '隐私政策', + onPress: () => Linking.openURL(PRIVACY_POLICY_URL), }, { - icon: 'settings-outline' as const, - title: '设置', + icon: 'document-text-outline' as const, + title: '用户协议', + onPress: () => Linking.openURL(USER_AGREEMENT_URL), }, ], }, - { + // 只有登录用户才显示账号与安全菜单 + ...(isLoggedIn ? [{ title: '账号与安全', items: [ + { + icon: 'log-out-outline' as const, + title: '退出登录', + onPress: confirmLogout, + isDanger: false, + }, { icon: 'trash-outline' as const, title: '注销帐号', - onPress: handleDeleteAccount, + onPress: confirmDeleteAccount, isDanger: true, }, ], - }, - { - title: '开发者', - items: [ - { - icon: 'refresh-outline' as const, - title: '重置引导流程', - onPress: handleResetOnboarding, - isDanger: true, - }, - ], - }, + }] : []), ]; return ( @@ -317,12 +301,6 @@ export default function PersonalScreen() { ))} - {/* 底部浮动按钮 */} - - - - - @@ -430,6 +408,7 @@ const styles = StyleSheet.create({ menuItemText: { fontSize: 16, flex: 1, + fontWeight: '600', }, switch: { transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }], diff --git a/app/auth/login.tsx b/app/auth/login.tsx index 2c9bfb2..ee1f1b9 100644 --- a/app/auth/login.tsx +++ b/app/auth/login.tsx @@ -3,11 +3,12 @@ import * as AppleAuthentication from 'expo-apple-authentication'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Alert, Animated, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Alert, Animated, Linking, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; +import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import { Colors } from '@/constants/Colors'; import { useAppDispatch } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; @@ -230,8 +231,8 @@ export default function LoginScreen() { - 数字普拉提 - 欢迎登录 + 普拉提助手 + 欢迎登录普拉提星球 {/* Apple 登录 */} @@ -252,22 +253,6 @@ export default function LoginScreen() { )} - {/* 游客登录(弱化样式) */} - guardAgreement(onGuestLogin)} - disabled={loading} - style={({ pressed }) => [ - styles.guestButton, - { borderColor: color.border, backgroundColor: color.surface }, - loading && { opacity: 0.7 }, - pressed && { transform: [{ scale: 0.98 }] }, - ]} - > - - 以游客身份继续 - - {/* 协议勾选 */} setHasAgreed((v) => !v)} style={styles.checkboxWrap} accessibilityRole="checkbox" accessibilityState={{ checked: hasAgreed }}> @@ -281,11 +266,11 @@ export default function LoginScreen() { 我已阅读并同意 - router.push('/legal/privacy-policy')}> + Linking.openURL(PRIVACY_POLICY_URL)}> 《隐私政策》 - router.push('/legal/user-agreement')}> + Linking.openURL(USER_AGREEMENT_URL)}> 《用户协议》 diff --git a/app/index.tsx b/app/index.tsx index 5f7cb80..0756595 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,6 +1,5 @@ import { ThemedView } from '@/components/ThemedView'; import { useThemeColor } from '@/hooks/useThemeColor'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, View } from 'react-native'; @@ -18,22 +17,24 @@ export default function SplashScreen() { const checkOnboardingStatus = async () => { try { - const onboardingCompleted = await AsyncStorage.getItem(ONBOARDING_COMPLETED_KEY); + // const onboardingCompleted = await AsyncStorage.getItem(ONBOARDING_COMPLETED_KEY); - if (onboardingCompleted === 'true') { - router.replace('/(tabs)'); - } else { - router.replace('/onboarding'); - } - setIsLoading(false); + // if (onboardingCompleted === 'true') { + // router.replace('/(tabs)'); + // } else { + // router.replace('/onboarding'); + // } + // setIsLoading(false); + router.replace('/(tabs)'); } catch (error) { console.error('检查引导状态失败:', error); // 如果出现错误,默认显示引导页面 - setTimeout(() => { - router.replace('/onboarding'); - setIsLoading(false); - }, 1000); + // setTimeout(() => { + // router.replace('/onboarding'); + // setIsLoading(false); + // }, 1000); } + setIsLoading(false); }; if (!isLoading) { diff --git a/app/workout/today.tsx b/app/workout/today.tsx index 247a510..0769907 100644 --- a/app/workout/today.tsx +++ b/app/workout/today.tsx @@ -427,7 +427,7 @@ export default function TodayWorkoutScreen() { router.back()} withSafeTop={false} transparent={true} diff --git a/assets/images/icons/iconWorkout.png b/assets/images/icons/iconWorkout.png new file mode 100644 index 0000000000000000000000000000000000000000..8635eb17e2837be65457eb0c822e626b1ef630e6 GIT binary patch literal 65741 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 TQ_!{wfPR$Z)#WPRnT7lx*`6Bu literal 0 HcmV?d00001 diff --git a/components/PlanCard.tsx b/components/PlanCard.tsx index fc0b9db..d67e8b1 100644 --- a/components/PlanCard.tsx +++ b/components/PlanCard.tsx @@ -1,4 +1,4 @@ -import { ProgressBar } from '@/components/ProgressBar'; + import React from 'react'; import { Image, StyleSheet, Text, View } from 'react-native'; @@ -9,10 +9,9 @@ type PlanCardProps = { title: string; subtitle: string; level?: Level; - progress: number; // 0 - 1 }; -export function PlanCard({ image, title, subtitle, level, progress }: PlanCardProps) { +export function PlanCard({ image, title, subtitle, level }: PlanCardProps) { return ( @@ -27,10 +26,6 @@ export function PlanCard({ image, title, subtitle, level, progress }: PlanCardPr {title} {subtitle} - - - - ); @@ -86,9 +81,6 @@ const styles = StyleSheet.create({ fontSize: 12, color: '#B1B6BD', }, - progressWrapper: { - marginTop: 18, - }, }); diff --git a/components/PrivacyConsentModal.tsx b/components/PrivacyConsentModal.tsx index 18b580f..75202a2 100644 --- a/components/PrivacyConsentModal.tsx +++ b/components/PrivacyConsentModal.tsx @@ -1,12 +1,13 @@ -import { router } from 'expo-router'; +import { PRIVACY_POLICY_URL, USER_AGREEMENT_URL } from '@/constants/Agree'; import React from 'react'; import { - Dimensions, - Modal, - StyleSheet, - Text, - TouchableOpacity, - View + Dimensions, + Linking, + Modal, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; const { width } = Dimensions.get('window'); @@ -23,11 +24,11 @@ export default function PrivacyConsentModal({ onDisagree, }: PrivacyConsentModalProps) { const handleUserAgreementPress = () => { - router.push('/legal/user-agreement'); + Linking.openURL(USER_AGREEMENT_URL); }; const handlePrivacyPolicyPress = () => { - router.push('/legal/privacy-policy'); + Linking.openURL(PRIVACY_POLICY_URL); }; return ( diff --git a/components/ui/IconSymbol.tsx b/components/ui/IconSymbol.tsx index 87c1c52..c2de0cf 100644 --- a/components/ui/IconSymbol.tsx +++ b/components/ui/IconSymbol.tsx @@ -19,6 +19,7 @@ const MAPPING = { 'chevron.left.forwardslash.chevron.right': 'code', 'chevron.right': 'chevron-right', 'person.fill': 'person', + 'person.3.fill': 'people', } as IconMapping; /** diff --git a/constants/Agree.ts b/constants/Agree.ts new file mode 100644 index 0000000..ba8f498 --- /dev/null +++ b/constants/Agree.ts @@ -0,0 +1,4 @@ +// 用户协议 +export const USER_AGREEMENT_URL = 'https://docs.qq.com/doc/DWkVYZ3RhRmZCaUdn?nlc=1'; +// 隐私政策 +export const PRIVACY_POLICY_URL = 'https://docs.qq.com/doc/DWkh2Y1Zxb21BVGNY?nlc=1'; \ No newline at end of file diff --git a/hooks/useAuthGuard.ts b/hooks/useAuthGuard.ts index d3d41a2..bf68749 100644 --- a/hooks/useAuthGuard.ts +++ b/hooks/useAuthGuard.ts @@ -1,7 +1,11 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; import { usePathname, useRouter } from 'expo-router'; import { useCallback } from 'react'; +import { Alert } from 'react-native'; -import { useAppSelector } from '@/hooks/redux'; +import { useAppDispatch, useAppSelector } from '@/hooks/redux'; +import { api } from '@/services/api'; +import { logout as logoutAction } from '@/store/userSlice'; type RedirectParams = Record; @@ -12,6 +16,7 @@ type EnsureOptions = { export function useAuthGuard() { const router = useRouter(); + const dispatch = useAppDispatch(); const currentPath = usePathname(); const token = useAppSelector((s) => (s as any)?.user?.token as string | null); const isLoggedIn = !!token; @@ -52,11 +57,93 @@ export function useAuthGuard() { [ensureLoggedIn] ); + // 退出登录功能 + const handleLogout = useCallback(async () => { + try { + // 调用 Redux action 清除本地状态和缓存 + await dispatch(logoutAction()).unwrap(); + + // 跳转到登录页面 + router.replace('/auth/login'); + } catch (error) { + console.error('退出登录失败:', error); + Alert.alert('错误', '退出登录失败,请稍后重试'); + } + }, [dispatch, router]); + + // 带确认对话框的退出登录 + const confirmLogout = useCallback(() => { + Alert.alert( + '确认退出', + '确定要退出当前账号吗?', + [ + { + text: '取消', + style: 'cancel', + }, + { + text: '确定', + style: 'default', + onPress: handleLogout, + }, + ] + ); + }, [handleLogout]); + + // 注销账号功能 + const handleDeleteAccount = useCallback(async () => { + try { + // 调用注销账号API + await api.delete('/api/users/delete-account'); + + // 清除额外的本地数据 + await AsyncStorage.multiRemove(['@user_personal_info', '@onboarding_completed']); + + // 执行退出登录逻辑 + await dispatch(logoutAction()).unwrap(); + + Alert.alert('账号已注销', '您的账号已成功注销', [ + { + text: '确定', + onPress: () => router.replace('/auth/login'), + }, + ]); + } catch (error: any) { + console.error('注销账号失败:', error); + const message = error?.message || '注销失败,请稍后重试'; + Alert.alert('注销失败', message); + } + }, [dispatch, router]); + + // 带确认对话框的注销账号 + const confirmDeleteAccount = useCallback(() => { + Alert.alert( + '确认注销账号', + '此操作不可恢复,将删除您的账号及相关数据。确定继续吗?', + [ + { + text: '取消', + style: 'cancel', + }, + { + text: '确认注销', + style: 'destructive', + onPress: handleDeleteAccount, + }, + ], + { cancelable: true } + ); + }, [handleDeleteAccount]); + return { isLoggedIn, ensureLoggedIn, pushIfAuthedElseLogin, guardHandler, + handleLogout, + confirmLogout, + handleDeleteAccount, + confirmDeleteAccount, } as const; }