Files
mindfulness/client/app/(onboarding)/onboarding.tsx
2026-02-02 16:47:37 +08:00

205 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { useRouter } from 'expo-router';
import * as Notifications from 'expo-notifications';
import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout';
import { NameInputStep } from '@/components/onboarding/NameInputStep';
import { SelectionStep } from '@/components/onboarding/SelectionStep';
import { ReminderStep } from '@/components/onboarding/ReminderStep';
import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnaireAnswers } from '@/src/features/userProfileScoring';
import { fetchRecoFeed } from '@/src/services/recoApi';
import {
recordRecoFeedServed,
setOnboardingCompleted,
setUserProfile,
setDailyReminderSettings,
setUserProfileScoring,
setRecoFeedCache,
} from '@/src/storage/appStorage';
const STEPS = [
{ id: 'name', type: 'name', title: '我可以怎么称呼你?' },
{
id: 'status',
type: 'selection',
title: '媽媽的狀態?',
options: [
{ id: 'pregnant', label: '懷孕中/準備成為媽媽' },
{ id: 'has_kids', label: '已經有孩子' },
{ id: 'no_fill', label: '不想填寫' },
]
},
{
id: 'emotion',
type: 'selection',
title: '當下情緒狀態?',
options: [
{ id: 'happy', label: '愉悅、滿足' },
{ id: 'calm', label: '平靜、安穩' },
{ id: 'stressed', label: '被壓得有點喘不過氣' },
{ id: 'low', label: '情緒低落' },
]
},
{
id: 'influence',
type: 'selection',
title: '是什麼影響了你最近的感受?',
options: [
{ id: 'family', label: '家庭與孩子' },
{ id: 'work', label: '工作或學習' },
{ id: 'relationship', label: '親密關係' },
{ id: 'friends', label: '朋友與人際' },
{ id: 'health', label: '身心健康' },
]
},
{
id: 'support',
type: 'selection',
title: '最需要什麼支持?',
options: [
{ id: 'emotional', label: '情緒支持' },
{ id: 'parenting', label: '育兒壓力' },
{ id: 'self_worth', label: '自我價值' },
{ id: 'anxiety', label: '焦慮舒緩' },
{ id: 'balance', label: '休息與平衡' },
]
},
{ id: 'reminder', type: 'reminder', title: '你需要每天几次提醒?' },
];
export default function OnboardingScreen() {
const router = useRouter();
const [stepIndex, setStepIndex] = useState(0);
const [name, setName] = useState('');
const [selections, setSelections] = useState<Record<string, string[]>>({});
const [reminderTimes, setReminderTimes] = useState(3);
const currentStep = STEPS[stepIndex];
async function onFinish() {
// 请求推送权限
const { status } = await Notifications.requestPermissionsAsync();
const pushEnabled = status === 'granted';
// 将 Onboarding 选择映射为标准问卷枚举(允许跳过)
const answers = mapOnboardingSelectionsToQuestionnaireAnswers(selections);
// 生成用户画像(供推荐/Push/Widget 复用)
const scoringProfile = buildUserProfileFromQuestionnaire(answers);
await setUserProfileScoring(scoringProfile);
// Onboarding 结束后预拉取一次 Feed 文案(失败不阻塞进入首页)
try {
const { items, meta } = await fetchRecoFeed({
k: 30,
user_profile: {
profile_version: scoringProfile.profile_version,
profile_source: scoringProfile.profile_source,
profile_generated_at: scoringProfile.profile_generated_at,
profile_confidence: scoringProfile.profile_confidence,
profile_answered: scoringProfile.profile_answered,
stage: scoringProfile.stage,
emotion_score: scoringProfile.emotion_score,
context: scoringProfile.context,
need: scoringProfile.need,
},
});
await setRecoFeedCache({
saved_at: new Date().toISOString(),
items: items.map((x) => ({ content_id: x.content_id, text: x.text })),
meta: meta as Record<string, unknown>,
});
await recordRecoFeedServed(items.map((x) => x.content_id));
} catch {
// 网络失败时使用首页本地 mock 兜底
}
await setUserProfile({
name,
intents: Object.values(selections).flat()
});
await setDailyReminderSettings({
timesPerDay: reminderTimes,
pushEnabled: pushEnabled
});
await setOnboardingCompleted(true);
router.replace('/(app)/home');
}
const onNext = () => {
if (stepIndex < STEPS.length - 1) {
setStepIndex(stepIndex + 1);
} else {
onFinish();
}
};
const onBack = () => {
if (stepIndex > 0) {
setStepIndex(stepIndex - 1);
}
};
const onSkip = () => {
// 跳过整个 Onboarding仍生成一个“全跳过”的最小画像保证下游可用
const scoringProfile = buildUserProfileFromQuestionnaire({});
void setUserProfileScoring(scoringProfile);
// 标记已完成,避免下次启动再次进入 Onboarding
void setOnboardingCompleted(true);
router.replace('/(app)/home');
};
// 题目为单选:再次点击可取消;选择其他选项会替换为唯一选项
const handleToggleSelection = (id: string) => {
setSelections(prev => {
const currentIds = prev[currentStep.id] || [];
const nextIds = currentIds.includes(id) ? [] : [id];
return { ...prev, [currentStep.id]: nextIds };
});
};
const handleSkipStep = () => {
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
onNext();
};
return (
<OnboardingLayout
title={currentStep.title}
currentStep={stepIndex}
totalSteps={STEPS.length - 1}
onSkip={onSkip}
onBack={onBack}
showBackButton={stepIndex > 0}
>
{currentStep.type === 'name' && (
<NameInputStep
value={name}
onChangeText={setName}
onNext={onNext}
/>
)}
{currentStep.type === 'selection' && (
<SelectionStep
options={currentStep.options!}
selectedIds={selections[currentStep.id] || []}
onToggle={handleToggleSelection}
onNext={onNext}
onSkip={handleSkipStep}
/>
)}
{currentStep.type === 'reminder' && (
<ReminderStep
value={reminderTimes}
onChange={setReminderTimes}
onFinish={onFinish}
/>
)}
</OnboardingLayout>
);
}