feature:基本功能

This commit is contained in:
吕新雨
2026-01-29 19:09:36 +08:00
parent c1a433a469
commit 0dd84b1873
47 changed files with 2978 additions and 219 deletions

View File

@@ -1,104 +1,68 @@
import { useMemo, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { useState } from 'react';
import { useRouter } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { setOnboardingCompleted } from '@/src/storage/appStorage';
type OnboardingPage = {
title: string;
desc: string;
};
import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout';
import { NameInputStep } from '@/components/onboarding/NameInputStep';
import { IntentSelectionStep } from '@/components/onboarding/IntentSelectionStep';
import { setOnboardingCompleted, setUserProfile } from '@/src/storage/appStorage';
export default function OnboardingScreen() {
const router = useRouter();
const { t } = useTranslation();
// 35 页:这里默认 4 页,后续可按产品调整为 3 或 5
const pages = useMemo<OnboardingPage[]>(
() => [
{ title: t('onboarding.q1Title'), desc: t('onboarding.q1Desc') },
{ title: t('onboarding.q2Title'), desc: t('onboarding.q2Desc') },
{ title: t('onboarding.q3Title'), desc: t('onboarding.q3Desc') },
{ title: t('onboarding.q4Title'), desc: t('onboarding.q4Desc') }
],
[t]
);
const total = pages.length;
const [step, setStep] = useState(0);
const page = pages[Math.min(step, total - 1)];
const [name, setName] = useState('');
const [intents, setIntents] = useState<string[]>([]);
async function finishAndNext() {
async function onFinish() {
// Save whatever input we have
await setUserProfile({ name, intents });
await setOnboardingCompleted(true);
router.replace('/(onboarding)/push-prompt');
}
async function onNext() {
if (step >= total - 1) {
await finishAndNext();
return;
if (step === 0) {
setStep(1);
} else {
await onFinish();
}
setStep((s) => s + 1);
}
async function onSkipPage() {
// 每页可跳过:直接进入下一页(最后一页则结束)
await onNext();
async function onSkip() {
// Skip logic: move to next step regardless of input
if (step === 0) {
setStep(1);
} else {
await onFinish();
}
}
async function onSkipAll() {
// 一键跳过整个 Onboarding
await finishAndNext();
}
// Step 0: Next button requires input
// Step 1: Next button always enabled (can proceed with empty selection)
const nextEnabled = step === 0 ? name.trim().length > 0 : true;
return (
<View style={styles.container}>
<Text style={styles.progress}>
{t('onboarding.progress', { current: step + 1, total })}
</Text>
<View style={styles.card}>
<Text style={styles.title}>{page.title}</Text>
<Text style={styles.desc}>{page.desc}</Text>
</View>
<View style={styles.actions}>
<Pressable style={[styles.btn, styles.secondary]} onPress={onSkipPage}>
<Text style={[styles.btnText, styles.secondaryText]}>{t('onboarding.skip')}</Text>
</Pressable>
<Pressable style={[styles.btn, styles.primary]} onPress={onNext}>
<Text style={styles.btnText}>{t('onboarding.next')}</Text>
</Pressable>
</View>
<Pressable style={styles.skipAll} onPress={onSkipAll}>
<Text style={styles.skipAllText}>{t('onboarding.skipAll')}</Text>
</Pressable>
</View>
<OnboardingLayout
onSkip={onSkip}
onNext={onNext}
nextEnabled={nextEnabled}
>
{step === 0 ? (
<NameInputStep
value={name}
onChangeText={setName}
onSubmitEditing={name.trim().length > 0 ? onNext : undefined}
/>
) : (
<IntentSelectionStep
selectedIds={intents}
onToggle={(id) => {
setIntents(prev =>
prev.includes(id)
? prev.filter(i => i !== id)
: [...prev, id]
);
}}
/>
)}
</OnboardingLayout>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, justifyContent: 'center', gap: 16 },
progress: { textAlign: 'center', color: '#6B7280' },
card: {
borderRadius: 18,
padding: 20,
backgroundColor: '#FFFFFF',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#E5E7EB',
gap: 10
},
title: { fontSize: 22, fontWeight: '700', color: '#111827' },
desc: { fontSize: 16, lineHeight: 22, color: '#374151' },
actions: { flexDirection: 'row', gap: 12 },
btn: { flex: 1, paddingVertical: 14, borderRadius: 14, alignItems: 'center' },
primary: { backgroundColor: '#111827' },
secondary: { backgroundColor: '#F3F4F6' },
btnText: { fontSize: 16, fontWeight: '600', color: '#FFFFFF' },
secondaryText: { color: '#111827' },
skipAll: { alignItems: 'center', paddingTop: 10 },
skipAllText: { color: '#6B7280', textDecorationLine: 'underline' }
});