From b4ec17fcacdf8a6a692688495a5e6eb6f493f236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E6=B1=80=E5=B2=9A?= Date: Mon, 9 Feb 2026 17:33:53 +0800 Subject: [PATCH] =?UTF-8?q?onboarding:=20=E8=BD=AC=E5=9C=BA=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E3=80=81=E5=90=8D=E5=AD=97=E6=AD=A5=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=B3=E9=A1=B5=E3=80=81=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E4=B8=AA=E6=80=A7=E5=8C=96=E6=8B=9B=E5=91=BC=E8=AF=AD=E3=80=81?= =?UTF-8?q?Skip/=E6=8F=90=E9=86=92=E6=AD=A5=E7=AD=89=E6=96=87=E6=A1=88?= =?UTF-8?q?=E4=B8=8E=E4=BA=A4=E4=BA=92=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- client/app/(onboarding)/onboarding.tsx | 3 +- .../components/onboarding/NameInputStep.tsx | 3 +- .../onboarding/OnboardingLayout.tsx | 86 ++++++++++++++++--- client/components/onboarding/ReminderStep.tsx | 8 +- .../components/onboarding/SelectionStep.tsx | 19 ++-- client/src/i18n/locales/all.json | 6 +- client/src/i18n/locales/en.json | 3 +- client/src/i18n/locales/es.json | 3 +- client/src/i18n/locales/pt.json | 3 +- client/src/i18n/locales/zh-CN.json | 3 +- client/src/i18n/locales/zh-TW.json | 3 +- 11 files changed, 105 insertions(+), 35 deletions(-) diff --git a/client/app/(onboarding)/onboarding.tsx b/client/app/(onboarding)/onboarding.tsx index 3f3ee98..7f7c3df 100644 --- a/client/app/(onboarding)/onboarding.tsx +++ b/client/app/(onboarding)/onboarding.tsx @@ -213,6 +213,7 @@ export default function OnboardingScreen() { onSkip={onSkip} onBack={onBack} showBackButton={stepIndex > 0} + userName={name} > {currentStep.type === 'name' && ( { diff --git a/client/components/onboarding/NameInputStep.tsx b/client/components/onboarding/NameInputStep.tsx index c367aef..46724a2 100644 --- a/client/components/onboarding/NameInputStep.tsx +++ b/client/components/onboarding/NameInputStep.tsx @@ -95,8 +95,7 @@ export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProp blurOnSubmit={true} onSubmitEditing={() => { Keyboard.dismiss(); - // 有输入时,“完成”直接进入下一步,避免真机卡在键盘上 - if (value.trim().length > 0) onNext(); + // 不再自動跳頁,僅收起鍵盤;前進需點擊底部 ➡️ }} /> diff --git a/client/components/onboarding/OnboardingLayout.tsx b/client/components/onboarding/OnboardingLayout.tsx index 3a7913b..a20d685 100644 --- a/client/components/onboarding/OnboardingLayout.tsx +++ b/client/components/onboarding/OnboardingLayout.tsx @@ -1,8 +1,11 @@ -import React from 'react'; -import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native'; +import React, { useRef, useEffect } from 'react'; +import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native'; import { useTranslation } from 'react-i18next'; import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme'; +const TRANSITION_OFFSET = 24; +const TRANSITION_DURATION = 280; + interface OnboardingLayoutProps { children: React.ReactNode; title?: string; @@ -11,6 +14,8 @@ interface OnboardingLayoutProps { onSkip: () => void; onBack?: () => void; showBackButton?: boolean; + /** 用户名字,仅在名字步骤之后的第一个问题(currentStep === 1)且非空时显示招呼语 */ + userName?: string; } export function OnboardingLayout({ @@ -20,9 +25,48 @@ export function OnboardingLayout({ totalSteps, onSkip, onBack, - showBackButton = false + showBackButton = false, + userName = '', }: OnboardingLayoutProps) { const { t } = useTranslation(); + const showGreeting = currentStep === 1 && userName.trim().length > 0; + const displayName = userName.trim(); + const prevStepRef = useRef(currentStep); + const isFirstRenderRef = useRef(true); + const translateX = useRef(new Animated.Value(0)).current; + const opacity = useRef(new Animated.Value(1)).current; + + useEffect(() => { + if (isFirstRenderRef.current) { + isFirstRenderRef.current = false; + prevStepRef.current = currentStep; + return; + } + if (prevStepRef.current === currentStep) return; + + const direction = currentStep > prevStepRef.current ? 'forward' : 'back'; + prevStepRef.current = currentStep; + + const startX = direction === 'forward' ? TRANSITION_OFFSET : -TRANSITION_OFFSET; + translateX.setValue(startX); + opacity.setValue(0.72); + + Animated.parallel([ + Animated.timing(translateX, { + toValue: 0, + duration: TRANSITION_DURATION, + useNativeDriver: true, + easing: Easing.out(Easing.cubic), + }), + Animated.timing(opacity, { + toValue: 1, + duration: TRANSITION_DURATION, + useNativeDriver: true, + easing: Easing.out(Easing.cubic), + }), + ]).start(); + }, [currentStep, translateX, opacity]); + return ( @@ -49,16 +93,29 @@ export function OnboardingLayout({ - {/* Title & Progress Row */} + {/* Title & Progress Row(名字步骤后第一步且名字非空时显示招呼语 + 问题) */} - {title} + + {showGreeting && ( + {t('onboardingSurvey.greeting', { name: displayName })} + )} + {title} + ({currentStep}/{totalSteps}) - {/* Content */} - + {/* Content:step 切换时滑动 + 淡入 */} + {children} - + ); @@ -114,13 +171,22 @@ const styles = StyleSheet.create({ alignItems: 'flex-end', paddingHorizontal: 20, marginTop: 20, - marginBottom: 20, + marginBottom: 8, + }, + titleBlock: { + flex: 1, + justifyContent: 'flex-end', + }, + greetingText: { + fontSize: 22, + color: OnboardingColors.questionTitle, + fontFamily: OnboardingFont.question, + marginBottom: 4, }, questionTitle: { fontSize: 22, color: OnboardingColors.questionTitle, fontFamily: OnboardingFont.question, - flex: 1, }, progressText: { fontSize: 18, diff --git a/client/components/onboarding/ReminderStep.tsx b/client/components/onboarding/ReminderStep.tsx index 7c2cea6..a48b15a 100644 --- a/client/components/onboarding/ReminderStep.tsx +++ b/client/components/onboarding/ReminderStep.tsx @@ -19,8 +19,8 @@ export function ReminderStep({ value, onChange, onFinish }: ReminderStepProps) { const insets = useSafeAreaInsets(); const handleReduce = () => { - // 允许 0~5;0 表示关闭每日提醒 - if (value > 0) onChange(value - 1); + // 本页最小为 1;不接收提醒请使用右上角 Skip + if (value > 1) onChange(value - 1); }; const handleAdd = () => { @@ -36,7 +36,9 @@ export function ReminderStep({ value, onChange, onFinish }: ReminderStepProps) { {value} - {t('dailyReminder.timesUnit')} + + {value === 1 ? t('dailyReminder.timesUnitSingular') : t('dailyReminder.timesUnit')} + diff --git a/client/components/onboarding/SelectionStep.tsx b/client/components/onboarding/SelectionStep.tsx index 2773aa9..2b9e989 100644 --- a/client/components/onboarding/SelectionStep.tsx +++ b/client/components/onboarding/SelectionStep.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { View, StyleSheet, TouchableOpacity, ScrollView, Text } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme'; -import SelectedIcon from '@/assets/images/icon/selected_icon.svg'; import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg'; import BtnClicked from '@/assets/images/icon/btn_clicked.svg'; @@ -39,16 +38,11 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip } return ( onToggle(option.id)} activeOpacity={0.7} > {option.label} - {isSelected && ( - - - - )} ); })} @@ -67,7 +61,7 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip } const styles = StyleSheet.create({ container: { flex: 1, - paddingTop: 20, + paddingTop: 8, }, scroll: { flex: 1, @@ -82,7 +76,7 @@ const styles = StyleSheet.create({ borderRadius: 20, flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', + justifyContent: 'center', paddingHorizontal: 24, marginBottom: 12, shadowColor: '#000', @@ -91,15 +85,14 @@ const styles = StyleSheet.create({ shadowRadius: 10, elevation: 2, }, + optionCardSelected: { + backgroundColor: OnboardingColors.cardSelected, + }, optionText: { fontSize: 18, color: OnboardingColors.textPrimary, fontWeight: '500', fontFamily: OnboardingFont.question, - flex: 1, - }, - iconWrapper: { - marginLeft: 10, }, footer: { position: 'absolute', diff --git a/client/src/i18n/locales/all.json b/client/src/i18n/locales/all.json index 763335b..054f664 100644 --- a/client/src/i18n/locales/all.json +++ b/client/src/i18n/locales/all.json @@ -25,6 +25,7 @@ "q4Desc": "You can skip. We’ll stay with you along the way." }, "onboardingSurvey": { + "greeting": "Hi {{name}},", "steps": { "name": { "title": "What should we call you?", "placeholder": "Mama" }, "status": { @@ -115,6 +116,7 @@ "dailyReminder": { "title": "Daily Reminder", "timesUnit": "times", + "timesUnitSingular": "time", "pushLabel": "Push Reminder", "ok": "Ok", "minus": "Decrease", @@ -183,7 +185,7 @@ "progress": "{{current}}/{{total}}", "next": "下一步", "skip": "跳過", - "skipAll": "跳過整個引導", + "skipAll": "跳過", "q1Title": "你最近的感受更接近哪一種?", "q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。", "q2Title": "你更希望獲得哪種支持?", @@ -194,6 +196,7 @@ "q4Desc": "你可以直接跳過,我們會在之後繼續陪你。" }, "onboardingSurvey": { + "greeting": "{{name}},你好", "steps": { "name": { "title": "我可以怎麼稱呼你?", "placeholder": "媽媽" }, "status": { @@ -284,6 +287,7 @@ "dailyReminder": { "title": "每日提醒", "timesUnit": "次", + "timesUnitSingular": "次", "pushLabel": "推送提醒", "ok": "確定", "minus": "減少次數", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index ec20c6a..4dbbf18 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -11,7 +11,7 @@ "progress": "{{current}}/{{total}}", "next": "Next", "skip": "Skip", - "skipAll": "Skip onboarding", + "skipAll": "Skip", "q1Title": "How are you feeling lately?", "q1Desc": "No right or wrong. You can skip and adjust later.", "q2Title": "What kind of support do you want?", @@ -59,6 +59,7 @@ "dailyReminder": { "title": "Daily Reminder", "timesUnit": "times", + "timesUnitSingular": "time", "pushLabel": "Push Reminder", "ok": "Ok", "minus": "Decrease", diff --git a/client/src/i18n/locales/es.json b/client/src/i18n/locales/es.json index e6d1b81..2ca7f78 100644 --- a/client/src/i18n/locales/es.json +++ b/client/src/i18n/locales/es.json @@ -9,7 +9,7 @@ "progress": "{{current}}/{{total}}", "next": "Siguiente", "skip": "Saltar", - "skipAll": "Saltar introducción", + "skipAll": "Saltar", "q1Title": "¿Cómo te sientes últimamente?", "q1Desc": "No hay respuestas correctas. Puedes saltar y ajustar después.", "q2Title": "¿Qué tipo de apoyo quieres?", @@ -57,6 +57,7 @@ "dailyReminder": { "title": "Recordatorio diario", "timesUnit": "veces", + "timesUnitSingular": "vez", "pushLabel": "Recordatorio Push", "ok": "Ok", "minus": "Disminuir", diff --git a/client/src/i18n/locales/pt.json b/client/src/i18n/locales/pt.json index a09aa32..d284e44 100644 --- a/client/src/i18n/locales/pt.json +++ b/client/src/i18n/locales/pt.json @@ -9,7 +9,7 @@ "progress": "{{current}}/{{total}}", "next": "Próximo", "skip": "Pular", - "skipAll": "Pular introdução", + "skipAll": "Pular", "q1Title": "Como você tem se sentido ultimamente?", "q1Desc": "Não há certo ou errado. Você pode pular e ajustar depois.", "q2Title": "Que tipo de apoio você quer?", @@ -57,6 +57,7 @@ "dailyReminder": { "title": "Lembrete diário", "timesUnit": "vezes", + "timesUnitSingular": "vez", "pushLabel": "Lembrete Push", "ok": "Ok", "minus": "Diminuir", diff --git a/client/src/i18n/locales/zh-CN.json b/client/src/i18n/locales/zh-CN.json index f774875..6dbc785 100644 --- a/client/src/i18n/locales/zh-CN.json +++ b/client/src/i18n/locales/zh-CN.json @@ -12,7 +12,7 @@ "progress": "{{current}}/{{total}}", "next": "下一步", "skip": "跳过", - "skipAll": "跳过整个引导", + "skipAll": "跳过", "q1Title": "你最近的感受更接近哪一种?", "q1Desc": "没有对错,你可以跳过,之后也可以慢慢调整。", "q2Title": "你更希望获得哪种支持?", @@ -60,6 +60,7 @@ "dailyReminder": { "title": "每日提醒", "timesUnit": "次", + "timesUnitSingular": "次", "pushLabel": "推送提醒", "ok": "确定", "minus": "减少次数", diff --git a/client/src/i18n/locales/zh-TW.json b/client/src/i18n/locales/zh-TW.json index 4e91b8f..b773ef7 100644 --- a/client/src/i18n/locales/zh-TW.json +++ b/client/src/i18n/locales/zh-TW.json @@ -9,7 +9,7 @@ "progress": "{{current}}/{{total}}", "next": "下一步", "skip": "跳過", - "skipAll": "跳過整個引導", + "skipAll": "跳過", "q1Title": "你最近的感受更接近哪一種?", "q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。", "q2Title": "你更希望獲得哪種支持?", @@ -57,6 +57,7 @@ "dailyReminder": { "title": "每日提醒", "timesUnit": "次", + "timesUnitSingular": "次", "pushLabel": "推送提醒", "ok": "確定", "minus": "減少次數",