feat: 完善个人中心与小组件功能,优化 Onboarding 交互与手势体验

This commit is contained in:
1
2026-01-31 19:02:54 +08:00
parent c4c7d7d251
commit e675cbbbfb
49 changed files with 1536 additions and 522 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import { Alert, FlatList, Image, Pressable, StyleSheet, Text, View, Platform, Dimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import { LinearGradient } from 'expo-linear-gradient';
import { Switch } from 'react-native';
@@ -11,6 +11,7 @@ import Animated, {
SlideInRight,
SlideOutLeft,
SlideOutRight,
Layout,
} from 'react-native-reanimated';
import SheetModal from '@/components/ui/SheetModal';
@@ -20,16 +21,25 @@ import {
getDailyReminderSettings,
getFavorites,
setDailyReminderSettings,
removeFavorite,
getUserProfile,
type DailyReminderSettings,
type FavoriteItem,
} from '@/src/storage/appStorage';
import AvatarIcon from '@/assets/images/home/Profile/Default_avatar.png';
import MyLikeIcon from '@/assets/images/home/Profile/mylike.svg';
import SmallComponentIcon from '@/assets/images/home/Profile/small_component.svg';
import RemindIcon from '@/assets/images/home/Profile/remind.svg';
import PrivacyIcon from '@/assets/images/home/Profile/privacy Policy.svg';
import TermsIcon from '@/assets/images/home/Profile/terms_of_use.svg';
import LanguageIcon from '@/assets/images/home/Profile/language.svg';
import AvatarIcon from '@/assets/images/home/Profile/Default_avatar.svg';
import MyLikeIcon from '@/assets/images/icon/mylike_icon.svg';
import SmallComponentIcon from '@/assets/images/icon/weight_icon.svg';
import RemindIcon from '@/assets/images/icon/Push_icon.svg';
import PrivacyIcon from '@/assets/images/icon/privacy_icon.svg';
import TermsIcon from '@/assets/images/icon/Terms_icon.svg';
import LanguageIcon from '@/assets/images/icon/language_icon.svg';
import SelectedIcon from '@/assets/images/icon/selected_icon.svg';
import QuestionIcon from '@/assets/images/home/Profile/widget/question_icon.svg';
import * as Notifications from 'expo-notifications';
import { changeLanguage } from '@/src/i18n';
const { width } = Dimensions.get('window');
type Props = {
visible: boolean;
@@ -37,24 +47,38 @@ type Props = {
onClose: () => void;
};
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget';
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo';
type NavDirection = 'forward' | 'back';
export default function ProfileModal({ visible, name, onClose }: Props) {
export default function ProfileModal({ visible, name: propName, onClose }: Props) {
const { t } = useTranslation();
const [page, setPage] = useState<Page>('root');
const [navDirection, setNavDirection] = useState<NavDirection>('forward');
const [currentName, setCurrentName] = useState(propName);
const isRoot = page === 'root';
// 关闭弹窗时重置为首页,避免下次打开停留在二级页
// 当弹窗打开时,尝试从存储中获取最新的昵称,确保与 onboarding 同步
useEffect(() => {
if (!visible) {
if (visible) {
getUserProfile().then(profile => {
if (profile.name) {
setCurrentName(profile.name);
}
});
} else {
setNavDirection('back');
setPage('root');
}
}, [visible]);
// 同步外部 propName 的变化
useEffect(() => {
if (propName) {
setCurrentName(propName);
}
}, [propName]);
function go(next: Page, direction: NavDirection) {
setNavDirection(direction);
setPage(next);
@@ -79,6 +103,8 @@ export default function ProfileModal({ visible, name, onClose }: Props) {
if (page === 'favorites') return t('profile.favorites');
if (page === 'dailyReminder') return t('dailyReminder.title');
if (page === 'widget') return t('profile.widget');
if (page === 'language') return t('profile.language');
if (page === 'widgetHowTo') return t('widget.howToTitle');
return t('profile.title');
}, [page, t]);
@@ -107,42 +133,46 @@ export default function ProfileModal({ visible, name, onClose }: Props) {
}, [navDirection]);
return (
<SheetModal visible={visible} title={title} onClose={handleSheetClose}>
{!isRoot && (
<Pressable
onPress={() => go('root', 'back')}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('common.back')}
style={styles.backRow}
<SheetModal
visible={visible}
title={title}
onClose={handleSheetClose}
leftIcon={!isRoot ? require('@/assets/images/icon/back_icon.png') : undefined}
>
<View style={[styles.pageWrap, !isRoot && { flex: 1 }]}>
<Animated.View
key={page}
entering={transition.entering}
exiting={transition.exiting}
style={!isRoot ? { flex: 1 } : undefined}
>
<Text style={styles.backText}> {t('common.back')}</Text>
</Pressable>
)}
<Animated.View
key={page}
entering={transition.entering}
exiting={transition.exiting}
style={styles.pageWrap}
>
<Animated.View entering={transition.fadeIn} exiting={transition.fadeOut}>
<Animated.View
entering={transition.fadeIn}
exiting={transition.fadeOut}
style={!isRoot ? { flex: 1 } : undefined}
>
{page === 'root' ? (
<RootPage
name={name}
name={currentName}
onOpenFavorites={() => go('favorites', 'forward')}
onOpenWidget={() => go('widget', 'forward')}
onOpenDailyReminder={() => go('dailyReminder', 'forward')}
onOpenLanguage={() => go('language', 'forward')}
/>
) : page === 'favorites' ? (
<FavoritesPage visible={visible} />
<FavoritesPage visible={visible} page={page} />
) : page === 'dailyReminder' ? (
<DailyReminderPage visible={visible} onDone={() => go('root', 'back')} />
) : page === 'language' ? (
<LanguagePage />
) : page === 'widgetHowTo' ? (
<WidgetHowToPage />
) : (
<WidgetPage />
<WidgetPage onOpenHowTo={() => go('widgetHowTo', 'forward')} />
)}
</Animated.View>
</Animated.View>
</Animated.View>
</View>
</SheetModal>
);
}
@@ -156,17 +186,19 @@ function RootPage({
onOpenFavorites,
onOpenWidget,
onOpenDailyReminder,
onOpenLanguage,
}: {
name?: string;
onOpenFavorites: () => void;
onOpenWidget: () => void;
onOpenDailyReminder: () => void;
onOpenLanguage: () => void;
}) {
const { t } = useTranslation();
return (
<>
<View style={styles.header}>
<Image source={AvatarIcon} style={styles.avatarImage} resizeMode="contain" />
<AvatarIcon width={234} height={183} />
<Text style={styles.name}>{name || 'Hali'}</Text>
</View>
@@ -198,48 +230,79 @@ function RootPage({
<ListItem
icon={<LanguageIcon width={22} height={22} />}
title={t('profile.language')}
onPress={() => toastTodo(t)}
onPress={onOpenLanguage}
/>
</View>
</>
);
}
function FavoritesPage({ visible }: { visible: boolean }) {
function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
const { t } = useTranslation();
const [ids, setIds] = useState<string[]>([]);
const [favorites, setFavorites] = useState<(FavoriteItem & { text: string })[]>([]);
// 每次进入该页刷新一次,确保展示最新“喜欢”
useEffect(() => {
if (!visible) return;
let cancelled = false;
(async () => {
const list = await getFavorites();
if (!cancelled) setIds(list);
})();
return () => {
cancelled = true;
};
}, [visible]);
refreshFavorites();
}, [visible, page]); // 当页面切换回 favorites 时也刷新一次
const items = useMemo(() => {
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c]));
return ids.map((id) => map.get(id)).filter(Boolean) as { id: string; text: string }[];
}, [ids]);
async function refreshFavorites() {
const storedFavs = await getFavorites();
const map = new Map(MOCK_CONTENT.map((c) => [c.id, c.text]));
const list = storedFavs
.map((fav) => ({
...fav,
text: map.get(fav.id) || ''
}))
.filter(item => item.text !== '');
setFavorites(list);
}
async function handleRemove(id: string) {
// 1. 调用存储层移除收藏
await removeFavorite(id);
// 2. 更新本地状态
setFavorites(prev => prev.filter(item => item.id !== id));
}
return (
<View style={styles.favContainer}>
{items.length === 0 ? (
{favorites.length === 0 ? (
<Text style={styles.favEmpty}>{t('favorites.empty')}</Text>
) : (
<FlatList
data={items}
data={favorites}
keyExtractor={(it) => it.id}
contentContainerStyle={styles.favList}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={styles.favRow}>
<Text style={styles.favText}>{item.text}</Text>
</View>
<Animated.View
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(200)}
layout={Layout.duration(300).easing(Easing.out(Easing.quad))}
style={styles.favCard}
>
<View style={styles.favLeft}>
<Text style={styles.favDate}>{item.date}</Text>
</View>
<View style={styles.favRight}>
<View style={[
styles.favThumb,
{ backgroundColor: item.background } // 动态同步 Home 页的背景
]}>
<Text style={styles.favThumbText} numberOfLines={4}>{item.text}</Text>
<Pressable
onPress={() => handleRemove(item.id)}
style={styles.favRemoveBtn}
hitSlop={10}
>
<MyLikeIcon width={37 * 0.7} height={33 * 0.7} />
</Pressable>
</View>
</View>
</Animated.View>
)}
/>
)}
@@ -252,16 +315,23 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
const [loading, setLoading] = useState(false);
const [timesPerDay, setTimesPerDay] = useState(3);
const [pushEnabled, setPushEnabled] = useState(false);
const [hasSystemPermission, setHasSystemPermission] = useState<boolean | null>(null);
useEffect(() => {
let cancelled = false;
if (!visible) return;
setLoading(true);
(async () => {
// 1. 获取本地存储设置
const s = await getDailyReminderSettings();
// 【测试模式】:强制模拟无权限状态
const granted = false;
if (cancelled) return;
setTimesPerDay(s.timesPerDay);
setPushEnabled(s.pushEnabled);
setPushEnabled(granted);
setHasSystemPermission(granted);
setLoading(false);
})();
return () => {
@@ -269,6 +339,40 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
};
}, [visible]);
const handleTogglePush = async (value: boolean) => {
if (value) {
// 获取当前权限状态
const settings = await Notifications.getPermissionsAsync();
// 如果已经拒绝,弹窗提示
if (settings.status === 'denied') {
Alert.alert(
t('common.notice'),
"系统权限已被拒绝,请前往手机设置开启通知。"
);
setPushEnabled(false);
return;
}
// 尝试申请权限
const { status } = await Notifications.requestPermissionsAsync();
// 调试:打印状态
console.log('Push Permission Status:', status);
if (status === 'granted') {
setPushEnabled(true);
setHasSystemPermission(true);
} else {
setPushEnabled(false);
setHasSystemPermission(false);
Alert.alert(t('common.notice'), t('dailyReminder.permissionDenied'));
}
} else {
setPushEnabled(false);
}
};
function clamp(next: number) {
return Math.min(10, Math.max(1, next));
}
@@ -311,15 +415,24 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
</Pressable>
</View>
<View style={styles.remindRow}>
<View style={styles.rowLeft}>
<View style={styles.rowIcon}>
<RemindIcon width={18} height={18} />
{!hasSystemPermission && (
<View style={styles.remindRow}>
<View style={styles.rowLeft}>
<View style={styles.rowIcon}>
<RemindIcon width={18} height={18} />
</View>
<Text style={styles.rowText}>{t('dailyReminder.pushLabel')}</Text>
</View>
<View style={styles.rowRight}>
<Switch
value={pushEnabled}
onValueChange={handleTogglePush}
trackColor={{ false: '#D1D1D6', true: '#4CD964' }}
ios_backgroundColor="#D1D1D6"
/>
</View>
<Text style={styles.rowText}>{t('dailyReminder.pushLabel')}</Text>
</View>
<Switch value={pushEnabled} onValueChange={setPushEnabled} />
</View>
)}
<Pressable onPress={onOk} disabled={loading} style={styles.okPressable}>
<LinearGradient
@@ -335,34 +448,150 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
);
}
function WidgetPage() {
const { t } = useTranslation();
function WidgetPage({ onOpenHowTo }: { onOpenHowTo: () => void }) {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
// 根据语言选择图片
const widget1 = currentLang === 'en'
? require('@/assets/images/home/Profile/widget/Widget1_en.png')
: require('@/assets/images/home/Profile/widget/Widget1_tw.png');
const widget2 = currentLang === 'en'
? require('@/assets/images/home/Profile/widget/Widget2_en.png')
: require('@/assets/images/home/Profile/widget/Widget2_tw.png');
return (
<View style={styles.widgetContent}>
<View style={styles.widgetRow}>
<View style={styles.widgetCard}>
<View style={[styles.widgetPreviewInner, styles.widgetPreviewLock]}>
<Text style={styles.widgetPreviewDate} numberOfLines={1}>
{t('widget.previewDate')}
</Text>
<Text style={styles.widgetPreviewTime} numberOfLines={1}>
13:45
</Text>
</View>
<Text style={styles.widgetCardLabel}>{t('widget.lockScreen')}</Text>
</View>
<Pressable style={styles.questionBtn} onPress={onOpenHowTo}>
<QuestionIcon width={19} height={19} />
</Pressable>
<View style={styles.widgetCard}>
<View style={[styles.widgetPreviewInner, styles.widgetPreviewHome]}>
<Text style={styles.widgetPreviewQuote} numberOfLines={3}>
{t('widget.previewQuote')}
</Text>
</View>
<Text style={styles.widgetCardLabel}>{t('widget.homeScreen')}</Text>
</View>
<View style={styles.widgetScroll}>
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
<Image source={widget1} style={styles.widgetImg1} resizeMode="contain" />
<Text style={styles.widgetLabel}>{t('widget.lockScreen')}</Text>
</Pressable>
<Pressable style={styles.widgetItem} onPress={onOpenHowTo}>
<Image source={widget2} style={styles.widgetImg2} resizeMode="contain" />
<Text style={styles.widgetLabel}>{t('widget.homeScreen')}</Text>
</Pressable>
</View>
</View>
);
}
<Text style={styles.widgetDesc}>{t('settings.widgetDesc')}</Text>
function WidgetHowToPage() {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const flatListRef = useRef<FlatList>(null);
const [activeIndex, setActiveIndex] = useState(0);
const [isManual, setIsManual] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const images = currentLang === 'en' ? [
{ id: '1', src: require('@/assets/images/home/Profile/widget/Widget_description1_en.png'), desc: t('widget.howToDesc1') },
{ id: '2', src: require('@/assets/images/home/Profile/widget/Widget_description2_en.png'), desc: t('widget.howToDesc2') },
] : [
{ id: '1', src: require('@/assets/images/home/Profile/widget/Widget_description1_tw.png'), desc: t('widget.howToDesc1') },
{ id: '2', src: require('@/assets/images/home/Profile/widget/Widget_description2_tw.png'), desc: t('widget.howToDesc2') },
];
const startAutoPlay = useCallback(() => {
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
if (!isManual) {
const nextIndex = (activeIndex + 1) % images.length;
flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true });
setActiveIndex(nextIndex);
}
}, 3000);
}, [activeIndex, isManual, images.length]);
useEffect(() => {
startAutoPlay();
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [startAutoPlay]);
const onScroll = (event: any) => {
const x = event.nativeEvent.contentOffset.x;
const index = Math.round(x / (width - 32));
if (index !== activeIndex) {
setActiveIndex(index);
}
};
const onScrollBeginDrag = () => {
setIsManual(true);
if (timerRef.current) clearInterval(timerRef.current);
};
return (
<View style={styles.howToPage}>
<FlatList
ref={flatListRef}
data={images}
keyExtractor={(item) => item.id}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={onScroll}
onScrollBeginDrag={onScrollBeginDrag}
scrollEventThrottle={16}
renderItem={({ item }) => (
<View style={styles.howToSlide}>
<Image source={item.src} style={styles.howToImg} resizeMode="contain" />
<Text style={styles.howToDesc}>{item.desc}</Text>
</View>
)}
/>
<View style={styles.pagination}>
{images.map((_, i) => (
<View
key={i}
style={[
styles.dot,
i === activeIndex ? styles.dotActive : styles.dotInactive
]}
/>
))}
</View>
</View>
);
}
function LanguagePage() {
const { i18n } = useTranslation();
const currentLang = i18n.language;
const languages = [
{ id: 'zh-TW', label: '繁体' },
{ id: 'en', label: 'English' },
];
return (
<View style={styles.langPage}>
<View style={styles.langList}>
{languages.map((lang, index) => (
<Pressable
key={lang.id}
style={[
styles.langItem,
index < languages.length - 1 && styles.langItemBorder
]}
onPress={() => changeLanguage(lang.id as any)}
>
<Text style={styles.langText}>{lang.label}</Text>
{currentLang === lang.id && (
<SelectedIcon width={20} height={20} />
)}
</Pressable>
))}
</View>
</View>
);
}
@@ -442,26 +671,31 @@ const styles = StyleSheet.create({
},
quickCard: {
flex: 1,
borderRadius: 16,
borderRadius: 20,
padding: 14,
backgroundColor: 'rgba(244,214,194,0.65)',
height: 142,
justifyContent: 'space-between',
},
quickTitle: {
color: '#5E2A28',
fontSize: 14,
fontWeight: '700',
marginBottom: 10,
color: '#482A0D',
fontSize: 15,
fontWeight: '500',
},
quickIconWrap: {
alignItems: 'flex-end',
justifyContent: 'flex-end',
flex: 1
},
quickIconWrap: { alignItems: 'center', justifyContent: 'center', flex: 1 },
list: {
borderRadius: 16,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.75)',
overflow: 'hidden',
marginBottom: 8,
},
item: {
height: 52,
paddingHorizontal: 14,
paddingHorizontal: 18,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
@@ -471,55 +705,82 @@ const styles = StyleSheet.create({
itemLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
gap: 12,
},
itemIcon: {
width: 28,
height: 28,
borderRadius: 10,
backgroundColor: 'rgba(244,214,194,0.55)',
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
itemText: {
color: '#5E2A28',
color: '#522B09',
fontSize: 15,
fontWeight: '600',
fontWeight: '500',
},
chevron: {
color: 'rgba(94,42,40,0.45)',
color: '#C7B4A1',
fontSize: 20,
marginTop: -2,
},
favContainer: {
borderRadius: 16,
backgroundColor: 'rgba(255,255,255,0.75)',
overflow: 'hidden',
marginBottom: 8,
flex: 1,
minHeight: 500, // 确保容器有足够高度
},
favEmpty: {
color: 'rgba(94,42,40,0.55)',
fontSize: 15,
textAlign: 'center',
paddingVertical: 26,
paddingHorizontal: 14,
paddingVertical: 100,
},
favList: {
paddingBottom: 10,
paddingHorizontal: 20,
paddingBottom: 80,
},
favRow: {
paddingHorizontal: 14,
paddingVertical: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(94,42,40,0.10)',
backgroundColor: 'rgba(255,255,255,0.15)',
favCard: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
},
favText: {
color: '#5E2A28',
favLeft: {
width: 90,
},
favDate: {
fontSize: 14,
color: '#772F00',
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
fontWeight: '600',
},
favRight: {
flex: 1,
},
favThumb: {
backgroundColor: '#FFF4EA',
borderRadius: 16,
padding: 20,
width: width * 0.6,
height: 161,
justifyContent: 'center',
position: 'relative',
borderWidth: 1,
borderColor: 'rgba(119, 47, 0, 0.05)',
},
favThumbText: {
fontSize: 15,
lineHeight: 22,
fontWeight: '600',
color: '#5E2A28',
fontWeight: '500',
textAlign: 'center',
},
favRemoveBtn: {
position: 'absolute',
top: 15,
right: 15,
width: 37,
height: 33,
alignItems: 'center',
justifyContent: 'center',
},
counter: {
@@ -563,15 +824,22 @@ const styles = StyleSheet.create({
},
remindRow: {
height: 54,
borderRadius: 16,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.75)',
paddingHorizontal: 14,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 18,
width: width - 40, // 屏幕宽度减去左右各 20pt
alignSelf: 'center',
},
rowLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
rowRight: {
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
rowIcon: {
width: 28,
height: 28,
@@ -587,6 +855,7 @@ const styles = StyleSheet.create({
},
okPressable: {
marginBottom: 6,
marginTop: 40, // 增加顶部间距以撑开高度
},
okBtn: {
height: 52,
@@ -602,61 +871,104 @@ const styles = StyleSheet.create({
},
widgetContent: {
paddingBottom: 18,
gap: 14,
},
widgetRow: {
flexDirection: 'row',
gap: 12,
},
widgetCard: {
flex: 1,
borderRadius: 16,
overflow: 'hidden',
backgroundColor: 'rgba(255,255,255,0.75)',
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(94,42,40,0.12)',
paddingTop: 10,
},
widgetPreviewInner: {
height: 120,
padding: 12,
justifyContent: 'center',
questionBtn: {
position: 'absolute',
right: 20,
top: -34, // 放在标题栏右侧
zIndex: 10,
},
widgetPreviewLock: {
backgroundColor: 'rgba(244,214,194,0.65)',
widgetScroll: {
alignItems: 'center',
gap: 30,
paddingBottom: 40,
},
widgetPreviewHome: {
backgroundColor: 'rgba(243,208,225,0.55)',
widgetItem: {
alignItems: 'center',
width: '100%',
},
widgetPreviewDate: {
color: 'rgba(94,42,40,0.70)',
fontSize: 12,
fontWeight: '600',
marginBottom: 6,
widgetImg1: {
width: width * 0.9,
height: (width * 0.9) * (156 / 311),
},
widgetPreviewTime: {
color: '#ffffff',
fontSize: 44,
fontWeight: '800',
letterSpacing: 1,
widgetImg2: {
width: width * 0.9,
height: (width * 0.9) * (175 / 311),
},
widgetPreviewQuote: {
color: '#5E2A28',
fontSize: 14,
lineHeight: 20,
fontWeight: '700',
widgetLabel: {
marginTop: 12,
fontSize: 15,
color: 'rgba(94, 42, 40, 0.45)',
fontWeight: '500',
},
widgetCardLabel: {
paddingVertical: 10,
howToPage: {
flex: 1,
alignItems: 'center',
paddingTop: 20,
},
howToSlide: {
width: width - 32, // 减去 SheetModal 的 paddingHorizontal: 16 * 2
alignItems: 'center',
},
howToImg: {
width: width * 0.9,
height: (width * 0.9) * (234 / 326),
marginBottom: 40,
},
howToDesc: {
fontSize: 15,
lineHeight: 25,
color: '#522B09',
textAlign: 'center',
color: '#5E2A28',
fontSize: 13,
fontWeight: '700',
fontWeight: '500',
paddingHorizontal: 20,
},
widgetDesc: {
color: 'rgba(94,42,40,0.75)',
fontSize: 13,
lineHeight: 18,
pagination: {
flexDirection: 'row',
position: 'absolute',
top: (width * 0.9) * (234 / 326) + 35, // 根据新的图片高度动态计算
gap: 8,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
dotActive: {
backgroundColor: '#EEB054',
},
dotInactive: {
backgroundColor: 'rgba(238, 176, 84, 0.3)',
},
langPage: {
paddingHorizontal: 20,
paddingTop: 10,
},
langList: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
overflow: 'hidden',
width: width - 40, // 屏幕宽度减去左右各 20pt
alignSelf: 'center',
},
langItem: {
height: 62,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 24,
},
langItemBorder: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(94,42,40,0.1)',
},
langText: {
fontSize: 15,
color: '#522B09',
fontWeight: '500',
textTransform: 'capitalize',
},
});