Files
mindfulness/client/components/home/ProfileModal.tsx

1025 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import Animated, {
Easing,
FadeIn,
FadeOut,
SlideInLeft,
SlideInRight,
SlideOutLeft,
SlideOutRight,
Layout,
} from 'react-native-reanimated';
import SheetModal from '@/components/ui/SheetModal';
import { MOCK_CONTENT } from '@/src/constants/mockContent';
import {
getDailyReminderSettings,
getFavorites,
setDailyReminderSettings,
removeFavorite,
getRecoFeedCache,
getUserProfile,
type DailyReminderSettings,
type FavoriteItem,
} from '@/src/storage/appStorage';
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;
name?: string;
onClose: () => void;
};
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo';
type NavDirection = 'forward' | 'back';
const NATURE_IMAGES = [
require('@/assets/theme/nature/1.png'),
require('@/assets/theme/nature/2.png'),
require('@/assets/theme/nature/3.png'),
require('@/assets/theme/nature/4.png'),
require('@/assets/theme/nature/5.png'),
require('@/assets/theme/nature/6.png'),
require('@/assets/theme/nature/7.png'),
require('@/assets/theme/nature/8.png'),
require('@/assets/theme/nature/9.png'),
require('@/assets/theme/nature/10.png'),
require('@/assets/theme/nature/11.png'),
require('@/assets/theme/nature/12.png'),
require('@/assets/theme/nature/13.png'),
require('@/assets/theme/nature/14.png'),
require('@/assets/theme/nature/15.png'),
require('@/assets/theme/nature/17.png'),
require('@/assets/theme/nature/18.png'),
require('@/assets/theme/nature/19.png'),
require('@/assets/theme/nature/20.png'),
require('@/assets/theme/nature/22.png'),
];
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) {
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);
}
function handleClose() {
setNavDirection('back');
setPage('root');
onClose();
}
function handleSheetClose() {
// 需求:二级页点击 X 等同于点击“返回”
if (!isRoot) {
go('root', 'back');
return;
}
handleClose();
}
const title = useMemo(() => {
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]);
const transition = useMemo(() => {
const duration = 220;
const easing = Easing.out(Easing.cubic);
// 进入二级页:从右侧滑入;返回:从左侧滑入
const entering =
navDirection === 'forward'
? SlideInRight.duration(duration).easing(easing)
: SlideInLeft.duration(duration).easing(easing);
// 离开:进入二级页时旧页面向左滑出;返回时旧页面向右滑出
const exiting =
navDirection === 'forward'
? SlideOutLeft.duration(duration).easing(easing)
: SlideOutRight.duration(duration).easing(easing);
return {
entering,
exiting,
fadeIn: FadeIn.duration(duration).easing(easing),
fadeOut: FadeOut.duration(duration).easing(easing),
};
}, [navDirection]);
return (
<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}
>
<Animated.View
entering={transition.fadeIn}
exiting={transition.fadeOut}
style={!isRoot ? { flex: 1 } : undefined}
>
{page === 'root' ? (
<RootPage
name={currentName}
onOpenFavorites={() => go('favorites', 'forward')}
onOpenWidget={() => go('widget', 'forward')}
onOpenDailyReminder={() => go('dailyReminder', 'forward')}
onOpenLanguage={() => go('language', 'forward')}
/>
) : page === 'favorites' ? (
<FavoritesPage visible={visible} page={page} />
) : page === 'dailyReminder' ? (
<DailyReminderPage visible={visible} onDone={() => go('root', 'back')} />
) : page === 'language' ? (
<LanguagePage />
) : page === 'widgetHowTo' ? (
<WidgetHowToPage />
) : (
<WidgetPage onOpenHowTo={() => go('widgetHowTo', 'forward')} />
)}
</Animated.View>
</Animated.View>
</View>
</SheetModal>
);
}
function toastTodo(t: (key: string) => string) {
Alert.alert(t('profile.todoTitle'), t('profile.todoDesc'));
}
function RootPage({
name,
onOpenFavorites,
onOpenWidget,
onOpenDailyReminder,
onOpenLanguage,
}: {
name?: string;
onOpenFavorites: () => void;
onOpenWidget: () => void;
onOpenDailyReminder: () => void;
onOpenLanguage: () => void;
}) {
const { t } = useTranslation();
return (
<>
<View style={styles.header}>
<AvatarIcon width={234} height={183} />
<Text style={styles.name}>{name || 'Hali'}</Text>
</View>
<View style={styles.quickRow}>
<QuickCard title={t('profile.favorites')} onPress={onOpenFavorites}>
<MyLikeIcon width={34} height={34} />
</QuickCard>
<QuickCard title={t('profile.widget')} onPress={onOpenWidget}>
<SmallComponentIcon width={34} height={34} />
</QuickCard>
</View>
<View style={styles.list}>
<ListItem
icon={<RemindIcon width={22} height={22} />}
title={t('profile.dailyReminder')}
onPress={onOpenDailyReminder}
/>
<ListItem
icon={<PrivacyIcon width={22} height={22} />}
title={t('profile.privacy')}
onPress={() => toastTodo(t)}
/>
<ListItem
icon={<TermsIcon width={22} height={22} />}
title={t('profile.terms')}
onPress={() => toastTodo(t)}
/>
<ListItem
icon={<LanguageIcon width={22} height={22} />}
title={t('profile.language')}
onPress={onOpenLanguage}
/>
</View>
</>
);
}
function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
const { t } = useTranslation();
const [favorites, setFavorites] = useState<(FavoriteItem & { text: string })[]>([]);
useEffect(() => {
if (!visible) return;
refreshFavorites();
}, [visible, page]); // 当页面切换回 favorites 时也刷新一次
async function refreshFavorites() {
const storedFavs = await getFavorites();
const textMap = new Map<string, string>();
// 1) Mock 文案
MOCK_CONTENT.forEach((c) => textMap.set(String(c.id), t(c.textKey)));
// 2) 后端推荐缓存文案(避免收藏后 cache 覆盖就丢文案)
const cache = await getRecoFeedCache();
cache?.items?.forEach((c) => textMap.set(String(c.content_id), c.text));
// 3) 组装:优先使用收藏时写入的 text其次从 map 回填
const list = storedFavs.map((fav) => ({
...fav,
text: fav.text || textMap.get(String(fav.id)) || t('favorites.unknownText'),
}));
setFavorites(list);
}
async function handleRemove(favId: string) {
// 1. 调用存储层移除收藏
await removeFavorite(favId);
// 2. 更新本地状态
setFavorites(prev => prev.filter(item => item.favId !== favId));
}
return (
<View style={styles.favContainer}>
{favorites.length === 0 ? (
<Text style={styles.favEmpty}>{t('favorites.empty')}</Text>
) : (
<FlatList
data={favorites}
keyExtractor={(it) => it.favId}
contentContainerStyle={styles.favList}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<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,
item.themeMode === 'scenery' ? {} : { backgroundColor: item.background }
]}>
{item.themeMode === 'scenery' ? (
<View style={StyleSheet.absoluteFill}>
<Image
source={NATURE_IMAGES[parseInt(item.background)]}
style={{
width: width * 0.6,
height: 800, // 假设原图较高,设置一个较大的高度
position: 'absolute',
bottom: 0, // 关键:将图片底部对齐容器底部
}}
resizeMode="cover"
/>
</View>
) : null}
<Text style={[
styles.favThumbText,
item.themeMode === 'scenery' && { color: '#FFFFFF', textShadowColor: 'rgba(0,0,0,0.5)', textShadowOffset: {width:0, height:1}, textShadowRadius: 3 }
]} numberOfLines={4}>
{item.text}
</Text>
<Pressable
onPress={() => handleRemove(item.favId)}
style={styles.favRemoveBtn}
hitSlop={10}
>
<MyLikeIcon width={37 * 0.7} height={33 * 0.7} />
</Pressable>
</View>
</View>
</Animated.View>
)}
/>
)}
</View>
);
}
function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () => void }) {
const { t } = useTranslation();
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(granted);
setHasSystemPermission(granted);
setLoading(false);
})();
return () => {
cancelled = true;
};
}, [visible]);
const handleTogglePush = async (value: boolean) => {
if (value) {
// 获取当前权限状态
const settings = await Notifications.getPermissionsAsync();
// 如果已经拒绝,弹窗提示
if (settings.status === 'denied') {
Alert.alert(
t('common.notice'),
t('permissions.notificationsDenied')
);
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));
}
async function onOk() {
if (loading) return;
setLoading(true);
const next: DailyReminderSettings = { timesPerDay, pushEnabled };
await setDailyReminderSettings(next);
setLoading(false);
onDone();
}
return (
<>
<View style={styles.counter}>
<Pressable
onPress={() => setTimesPerDay((v) => clamp(v - 1))}
hitSlop={10}
style={styles.circleBtn}
accessibilityRole="button"
accessibilityLabel={t('dailyReminder.minus')}
>
<Text style={styles.circleText}></Text>
</Pressable>
<View style={styles.countCenter}>
<Text style={styles.countNumber}>{timesPerDay}</Text>
<Text style={styles.countUnit}>{t('dailyReminder.timesUnit')}</Text>
</View>
<Pressable
onPress={() => setTimesPerDay((v) => clamp(v + 1))}
hitSlop={10}
style={styles.circleBtn}
accessibilityRole="button"
accessibilityLabel={t('dailyReminder.plus')}
>
<Text style={styles.circleText}></Text>
</Pressable>
</View>
{!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>
</View>
)}
<Pressable onPress={onOk} disabled={loading} style={styles.okPressable}>
<LinearGradient
colors={['#F69F7B', '#F99CC0']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.okBtn, loading && styles.okBtnDisabled]}
>
<Text style={styles.okText}>{t('dailyReminder.ok')}</Text>
</LinearGradient>
</Pressable>
</>
);
}
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}>
<Pressable style={styles.questionBtn} onPress={onOpenHowTo}>
<QuestionIcon width={19} height={19} />
</Pressable>
<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>
);
}
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 { t, i18n } = useTranslation();
const currentLang = i18n.language;
const languages = [
{ id: 'zh-TW', label: t('language.zhTW') },
{ id: 'en', label: t('language.en') },
];
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>
);
}
function QuickCard({
title,
onPress,
children,
}: {
title: string;
onPress: () => void;
children: React.ReactNode;
}) {
return (
<Pressable onPress={onPress} style={styles.quickCard} hitSlop={6}>
<Text style={styles.quickTitle}>{title}</Text>
<View style={styles.quickIconWrap}>{children}</View>
</Pressable>
);
}
function ListItem({
icon,
title,
onPress,
}: {
icon: React.ReactNode;
title: string;
onPress: () => void;
}) {
return (
<Pressable onPress={onPress} style={styles.item} hitSlop={6}>
<View style={styles.itemLeft}>
<View style={styles.itemIcon}>{icon}</View>
<Text style={styles.itemText}>{title}</Text>
</View>
<Text style={styles.chevron}></Text>
</Pressable>
);
}
const styles = StyleSheet.create({
pageWrap: {
// 给页面切换动画一个稳定的容器,避免布局抖动
},
backRow: {
alignSelf: 'flex-start',
paddingVertical: 4,
paddingHorizontal: 2,
marginBottom: 6,
},
backText: {
color: 'rgba(94,42,40,0.75)',
fontSize: 13,
fontWeight: '700',
},
header: {
alignItems: 'center',
paddingTop: 6,
paddingBottom: 14,
},
// 头像是默认图片234x183没有圆角/边框/背景颜色
avatarImage: {
width: 234,
height: 183,
marginBottom: 10,
},
name: {
color: '#5E2A28',
fontSize: 18,
fontWeight: '700',
},
quickRow: {
flexDirection: 'row',
gap: 12,
paddingBottom: 14,
},
quickCard: {
flex: 1,
borderRadius: 20,
padding: 14,
backgroundColor: 'rgba(244,214,194,0.65)',
height: 142,
justifyContent: 'space-between',
},
quickTitle: {
color: '#482A0D',
fontSize: 15,
fontWeight: '500',
},
quickIconWrap: {
alignItems: 'flex-end',
justifyContent: 'flex-end',
flex: 1
},
list: {
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.75)',
overflow: 'hidden',
marginBottom: 8,
},
item: {
height: 52,
paddingHorizontal: 18,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(94,42,40,0.10)',
},
itemLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
itemIcon: {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
itemText: {
color: '#522B09',
fontSize: 15,
fontWeight: '500',
},
chevron: {
color: '#C7B4A1',
fontSize: 20,
marginTop: -2,
},
favContainer: {
flex: 1,
minHeight: 500, // 确保容器有足够高度
},
favEmpty: {
color: 'rgba(94,42,40,0.55)',
fontSize: 15,
textAlign: 'center',
paddingVertical: 100,
},
favList: {
paddingHorizontal: 20,
paddingBottom: 80,
},
favCard: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
},
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)',
overflow: 'hidden',
},
favThumbText: {
fontSize: 15,
lineHeight: 22,
color: '#5E2A28',
fontWeight: '500',
textAlign: 'center',
},
favRemoveBtn: {
position: 'absolute',
top: 15,
right: 15,
width: 37,
height: 33,
alignItems: 'center',
justifyContent: 'center',
},
counter: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 22,
paddingTop: 8,
paddingBottom: 22,
},
circleBtn: {
width: 42,
height: 42,
borderRadius: 21,
backgroundColor: 'rgba(244,214,194,0.75)',
alignItems: 'center',
justifyContent: 'center',
},
circleText: {
color: '#5E2A28',
fontSize: 20,
fontWeight: '700',
marginTop: -1,
},
countCenter: {
flexDirection: 'row',
alignItems: 'flex-end',
gap: 6,
},
countNumber: {
color: '#111',
fontSize: 54,
fontWeight: '800',
letterSpacing: -1,
},
countUnit: {
color: '#111',
fontSize: 16,
fontWeight: '700',
marginBottom: 10,
},
remindRow: {
height: 54,
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,
borderRadius: 10,
backgroundColor: 'rgba(244,214,194,0.55)',
alignItems: 'center',
justifyContent: 'center',
},
rowText: {
color: '#5E2A28',
fontSize: 15,
fontWeight: '600',
},
okPressable: {
marginBottom: 6,
marginTop: 40, // 增加顶部间距以撑开高度
},
okBtn: {
height: 52,
borderRadius: 26,
alignItems: 'center',
justifyContent: 'center',
},
okBtnDisabled: { opacity: 0.7 },
okText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
widgetContent: {
flex: 1,
paddingTop: 10,
},
questionBtn: {
position: 'absolute',
right: 20,
top: -34, // 放在标题栏右侧
zIndex: 10,
},
widgetScroll: {
alignItems: 'center',
gap: 30,
paddingBottom: 40,
},
widgetItem: {
alignItems: 'center',
width: '100%',
},
widgetImg1: {
width: width * 0.9,
height: (width * 0.9) * (156 / 311),
},
widgetImg2: {
width: width * 0.9,
height: (width * 0.9) * (175 / 311),
},
widgetLabel: {
marginTop: 12,
fontSize: 15,
color: 'rgba(94, 42, 40, 0.45)',
fontWeight: '500',
},
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',
fontWeight: '500',
paddingHorizontal: 20,
},
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',
},
});