APP-PUSH和纯色小组件
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)', // 使用半透明的文字颜色
|
||||
|
||||
@@ -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%',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user