feat: 完善个人中心与小组件功能,优化 Onboarding 交互与手势体验
This commit is contained in:
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ type Props = {
|
||||
export default function ThemeModal({ visible, mode, onSelect, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose}>
|
||||
<SheetModal visible={visible} title={t('theme.title')} onClose={onClose} height={350}>
|
||||
<View style={styles.row}>
|
||||
<ThemeCard
|
||||
title={t('theme.scenery')}
|
||||
@@ -24,7 +24,7 @@ export default function ThemeModal({ visible, mode, onSelect, onClose }: Props)
|
||||
onPress={() => onSelect('scenery')}
|
||||
>
|
||||
<Image
|
||||
source={require('../../assets/images/index/index_flowers.png')}
|
||||
source={require('../../assets/images/theme/theme_landscape.png')}
|
||||
resizeMode="cover"
|
||||
style={styles.previewImage}
|
||||
/>
|
||||
@@ -35,7 +35,11 @@ export default function ThemeModal({ visible, mode, onSelect, onClose }: Props)
|
||||
selected={mode === 'color'}
|
||||
onPress={() => onSelect('color')}
|
||||
>
|
||||
<View style={styles.colorPreview} />
|
||||
<Image
|
||||
source={require('../../assets/images/theme/theme_color.png')}
|
||||
resizeMode="cover"
|
||||
style={styles.previewImage}
|
||||
/>
|
||||
</ThemeCard>
|
||||
</View>
|
||||
</SheetModal>
|
||||
@@ -56,11 +60,20 @@ function ThemeCard({
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={[styles.card, selected ? styles.cardSelected : styles.cardUnselected]}
|
||||
style={styles.cardContainer}
|
||||
hitSlop={6}
|
||||
>
|
||||
<View style={styles.preview}>{children}</View>
|
||||
<Text style={styles.cardTitle}>{title}</Text>
|
||||
<View style={[styles.previewWrapper, selected && styles.selectedWrapper]}>
|
||||
<View style={styles.previewInner}>
|
||||
{children}
|
||||
{/* 文案展示在图片中心 */}
|
||||
<View style={styles.textOverlay}>
|
||||
<Text style={[styles.overlayTitle, selected && styles.selectedOverlayTitle]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
@@ -68,43 +81,57 @@ function ThemeCard({
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 14,
|
||||
paddingBottom: 18,
|
||||
gap: 30,
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 50,
|
||||
paddingTop: 20,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
borderRadius: 18,
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(255,255,255,0.55)',
|
||||
cardContainer: {
|
||||
alignItems: 'center',
|
||||
width: 143,
|
||||
},
|
||||
cardSelected: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#F99CC0',
|
||||
previewWrapper: {
|
||||
width: 138,
|
||||
height: 203,
|
||||
borderRadius: 26,
|
||||
padding: 6.5,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
cardUnselected: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: 'rgba(94,42,40,0.18)',
|
||||
selectedWrapper: {
|
||||
borderWidth: 4,
|
||||
borderColor: '#E7837A',
|
||||
borderRadius: 26,
|
||||
},
|
||||
preview: {
|
||||
height: 96,
|
||||
borderRadius: 14,
|
||||
previewInner: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 21,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#fff',
|
||||
marginBottom: 10,
|
||||
backgroundColor: '#F5F5F5',
|
||||
position: 'relative',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
colorPreview: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F3D0E1',
|
||||
textOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.05)', // 轻微遮罩增加文字可读性
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#5E2A28',
|
||||
fontSize: 14,
|
||||
overlayTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 3,
|
||||
},
|
||||
selectedOverlayTitle: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user