205 lines
6.3 KiB
TypeScript
205 lines
6.3 KiB
TypeScript
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>
|
||
);
|
||
}
|