APP-PUSH和纯色小组件

This commit is contained in:
吕新雨
2026-02-05 01:14:13 +08:00
parent c1c2c6197d
commit 4c03fce720
28 changed files with 258 additions and 156 deletions

View File

@@ -28,6 +28,7 @@ import {
} from '@/src/storage/appStorage';
import { fetchRecoFeed } from '@/src/services/recoApi';
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
import ProfileModal from '@/components/home/ProfileModal';
import ThemeModal from '@/components/home/ThemeModal';
@@ -78,7 +79,7 @@ type FeedItem = { content_id: string; text: string };
export default function HomeScreen() {
const { t, i18n } = useTranslation();
const isEnglish = i18n.language?.startsWith('en');
const recoLang: 'en' | 'tc' = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
const recoLang: 'en' | 'tc' = toBackendLocaleFromLanguageTag(i18n.language);
const insets = useSafeAreaInsets();
const [index, setIndex] = useState(0);
const [themeMode, setThemeModeState] = useState<ThemeMode>('scenery');

View File

@@ -10,6 +10,7 @@ import { SelectionStep } from '@/components/onboarding/SelectionStep';
import { ReminderStep } from '@/components/onboarding/ReminderStep';
import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnaireAnswers } from '@/src/features/userProfileScoring';
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
import { fetchRecoFeed } from '@/src/services/recoApi';
import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi';
import {
@@ -30,7 +31,7 @@ type Step =
const STEPS: Step[] = [
{ id: 'name', type: 'name' },
{ id: 'status', type: 'selection', optionIds: ['pregnant', 'has_kids', 'no_fill'] },
{ id: 'emotion', type: 'selection', optionIds: ['happy', 'calm', 'stressed', 'low'] },
{ id: 'emotion', type: 'selection', optionIds: ['happy', 'calm', 'okay', 'tired', 'stressed', 'low'] },
{ id: 'influence', type: 'selection', optionIds: ['family', 'work', 'relationship', 'friends', 'health'] },
{ id: 'support', type: 'selection', optionIds: ['emotional', 'parenting', 'self_worth', 'anxiety', 'balance'] },
{ id: 'reminder', type: 'reminder' },
@@ -74,7 +75,7 @@ export default function OnboardingScreen() {
// Onboarding 结束后预拉取一次 Feed 文案(失败不阻塞进入首页)
try {
const lang = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
const lang = toBackendLocaleFromLanguageTag(i18n.language);
const { items, meta } = await fetchRecoFeed({
k: 30,
user_profile: {

View File

@@ -88,16 +88,22 @@ export default function SplashScreen() {
{/* 文案内容 */}
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
<Text style={styles.titleText}>
You Are Perfect.{"\n"}
Everything{"\n"}
Will Be Better.
{t('consent.title')}
{'\n'}
{t('consent.subtitle')}
</Text>
</View>
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
{showConsent && (
<>
<TouchableOpacity onPress={handleAgree} activeOpacity={0.8} style={styles.buttonWrapper}>
<TouchableOpacity
onPress={handleAgree}
activeOpacity={0.8}
style={styles.buttonWrapper}
accessibilityRole="button"
accessibilityLabel={t('consent.agree')}
>
<WelcomeBtn width={87} height={57} />
</TouchableOpacity>
@@ -116,6 +122,8 @@ export default function SplashScreen() {
<Text style={styles.linkText}>{t('consent.terms')}</Text>
</TouchableOpacity>
</View>
<Text style={styles.noticeText}>{t('consent.notice')}</Text>
</>
)}
</SafeAreaView>
@@ -168,6 +176,14 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
},
noticeText: {
marginTop: 10,
paddingHorizontal: 28,
fontSize: 12,
lineHeight: 16,
textAlign: 'center',
color: 'rgba(119, 47, 0, 0.45)',
},
linkText: {
fontSize: 12,
color: 'rgba(119, 47, 0, 0.5)', // 使用半透明的文字颜色

View File

@@ -4,9 +4,9 @@ import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import * as Notifications from 'expo-notifications';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import 'react-native-reanimated';
import { AppState } from 'react-native';
import { Animated, AppState, Image, StyleSheet, View } from 'react-native';
import { useColorScheme } from '@/components/useColorScheme';
import { initI18n } from '@/src/i18n';
@@ -41,6 +41,10 @@ export default function RootLayout() {
...FontAwesome.font,
});
const [i18nReady, setI18nReady] = useState(false);
const [appReady, setAppReady] = useState(false);
const [splashOverlayVisible, setSplashOverlayVisible] = useState(true);
const splashOpacity = useRef(new Animated.Value(1)).current;
const hasHiddenNativeSplashRef = useRef(false);
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
@@ -68,17 +72,52 @@ export default function RootLayout() {
}, []);
useEffect(() => {
// 字体与 i18n 都准备好后再隐藏启动页,避免文案闪烁
if (loaded && i18nReady) {
SplashScreen.hideAsync();
}
// 字体与 i18n 都准备好后,允许渲染 App原生 splash 的隐藏交给 onLayout避免“硬切/闪白”)
if (loaded && i18nReady) setAppReady(true);
}, [loaded, i18nReady]);
const onLayoutRootView = useCallback(() => {
if (!appReady) return;
if (hasHiddenNativeSplashRef.current) return;
hasHiddenNativeSplashRef.current = true;
// 先隐藏原生 splash再把同款覆盖层淡出视觉上实现平滑过渡
void SplashScreen.hideAsync().finally(() => {
Animated.timing(splashOpacity, {
toValue: 0,
duration: 380,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) setSplashOverlayVisible(false);
});
});
}, [appReady, splashOpacity]);
const content = useMemo(() => {
if (!appReady) return null;
return <RootLayoutNav />;
}, [appReady]);
if (!loaded || !i18nReady) {
return null;
}
return <RootLayoutNav />;
return (
<View style={styles.root} onLayout={onLayoutRootView}>
{content}
{splashOverlayVisible && (
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { opacity: splashOpacity }]}>
<View style={styles.splashOverlay}>
<Image
source={require('../assets/images/splashScreen.png')}
style={styles.splashImage}
resizeMode="contain"
/>
</View>
</Animated.View>
)}
</View>
);
}
function RootLayoutNav() {
@@ -118,3 +157,20 @@ function RootLayoutNav() {
</ThemeProvider>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
},
splashOverlay: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
// 与 app.json 的 expo.splash.backgroundColor 保持一致
backgroundColor: '#EAD2BA',
},
splashImage: {
width: '80%',
height: '80%',
},
});