feat: 首页支持风景图与纯色背景随滑动自动切换,优化点赞图标样式与交互
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated } from 'react-native';
|
||||
import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated, ImageBackground } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigation, useFocusEffect } from 'expo-router';
|
||||
import Animated, {
|
||||
@@ -31,6 +31,40 @@ import LikeIcon from '@/assets/images/icon/like_icon.svg';
|
||||
|
||||
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
// 预定义风景图列表
|
||||
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'),
|
||||
];
|
||||
|
||||
// 预定义颜色列表
|
||||
const THEME_COLORS = [
|
||||
'#F7D9BF',
|
||||
'#CBF2D8',
|
||||
'#F5CDDE',
|
||||
'#F2ECCB',
|
||||
'#E2CBF2',
|
||||
'#CBD9F2',
|
||||
];
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation();
|
||||
@@ -66,12 +100,26 @@ export default function HomeScreen() {
|
||||
}, [])
|
||||
);
|
||||
|
||||
const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2';
|
||||
const backgroundColor = useMemo(() => {
|
||||
if (themeMode === 'color') {
|
||||
const colorIndex = Math.floor(index / 10) % THEME_COLORS.length;
|
||||
return THEME_COLORS[colorIndex];
|
||||
}
|
||||
return '#F4D6C2'; // 风景模式下的默认底色(图片加载前显示)
|
||||
}, [themeMode, index]);
|
||||
|
||||
// 计算当前应该显示的风景图索引(滑动 10 次切换一张)
|
||||
const natureImageIndex = useMemo(() => {
|
||||
return Math.floor(index / 10) % NATURE_IMAGES.length;
|
||||
}, [index]);
|
||||
|
||||
const currentNatureImage = NATURE_IMAGES[natureImageIndex];
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShadowVisible: false,
|
||||
headerStyle: { backgroundColor },
|
||||
headerStyle: { backgroundColor: themeMode === 'scenery' ? 'transparent' : backgroundColor },
|
||||
headerTransparent: themeMode === 'scenery',
|
||||
headerRight: () => (
|
||||
<View style={styles.headerRight}>
|
||||
<CircleIconButton
|
||||
@@ -89,7 +137,7 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [backgroundColor, navigation, t]);
|
||||
}, [backgroundColor, themeMode, navigation, t]);
|
||||
|
||||
const textAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: translateY.value }],
|
||||
@@ -176,10 +224,11 @@ export default function HomeScreen() {
|
||||
|
||||
// 2. 保存到收藏夹,包含当前背景信息
|
||||
await addFavorite({
|
||||
favId: String(Date.now()), // 生成唯一 ID
|
||||
id: item.id,
|
||||
date: dateStr,
|
||||
themeMode: themeMode,
|
||||
background: backgroundColor, // 目前存储的是颜色值
|
||||
background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
|
||||
});
|
||||
|
||||
// 3. 爱心缩放动画
|
||||
@@ -202,8 +251,15 @@ export default function HomeScreen() {
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}>
|
||||
<Animated.View style={[styles.card, textAnimatedStyle]}>
|
||||
<Text style={styles.text}>{item.text}</Text>
|
||||
{themeMode === 'scenery' && (
|
||||
<ImageBackground
|
||||
source={currentNatureImage}
|
||||
style={StyleSheet.absoluteFill}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
)}
|
||||
<Animated.View style={[styles.card, textAnimatedStyle, themeMode === 'scenery' && styles.sceneryCard]}>
|
||||
<Text style={[styles.text, themeMode === 'scenery' && styles.sceneryText]}>{item.text}</Text>
|
||||
</Animated.View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
@@ -216,9 +272,13 @@ export default function HomeScreen() {
|
||||
style={styles.reactionInner}
|
||||
>
|
||||
{likeFilled ? (
|
||||
<LikeFilledIcon width={35} height={36} />
|
||||
<LikeFilledIcon width={35} height={36} style={{ color: '#EA6969' }} />
|
||||
) : (
|
||||
<LikeIcon width={35} height={36} />
|
||||
<LikeIcon
|
||||
width={35}
|
||||
height={36}
|
||||
style={{ color: themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28' }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
@@ -277,9 +337,15 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
card: {
|
||||
paddingHorizontal: 30,
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 30,
|
||||
zIndex: 5, // 降低层级,防止遮挡底部按钮
|
||||
},
|
||||
text: {
|
||||
fontSize: 22,
|
||||
@@ -288,6 +354,16 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
},
|
||||
sceneryCard: {
|
||||
// 风景模式下稍微收窄文案宽度,增加呼吸感
|
||||
paddingHorizontal: 50,
|
||||
},
|
||||
sceneryText: {
|
||||
color: '#FFFFFF',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 4,
|
||||
},
|
||||
actions: {
|
||||
position: 'absolute',
|
||||
bottom: SCREEN_HEIGHT * 0.16,
|
||||
@@ -295,6 +371,7 @@ const styles = StyleSheet.create({
|
||||
right: 0,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
zIndex: 20, // 提升层级,确保在最顶层可点击
|
||||
},
|
||||
reactionButton: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg width="32" height="27" viewBox="0 0 32 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" fill="#5E2A28" stroke="#5E2A28" stroke-width="2"/>
|
||||
<path d="M23.0879 1C24.9887 1 26.7592 1.93575 28.0762 3.42188C29.3956 4.91097 30.2001 6.88999 30.2002 8.84082C30.2002 13.2337 27.1054 17.3696 23.5225 20.499C21.7556 22.0422 19.9254 23.2909 18.4199 24.1494C17.6667 24.5789 17.0062 24.904 16.4863 25.1182C16.2262 25.2253 16.0121 25.3001 15.8467 25.3467C15.6728 25.3956 15.5993 25.4002 15.5996 25.4004C15.5928 25.3997 15.5178 25.3932 15.3525 25.3467C15.1871 25.3001 14.973 25.2253 14.7129 25.1182C14.193 24.904 13.5333 24.5788 12.7803 24.1494C11.2748 23.2909 9.44467 22.0423 7.67773 20.499C4.0947 17.3696 1 13.2338 1 8.84082C1.00008 6.89007 1.80381 4.91108 3.12207 3.42188C4.43787 1.93557 6.20587 1.00033 8.10059 1C9.51294 1.00117 10.8927 1.41742 12.0635 2.19434C12.7589 2.65582 13.3618 3.2322 13.8486 3.89355C14.2951 4.50017 15.0032 4.73135 15.5996 4.73145C16.1961 4.73144 16.905 4.5003 17.3516 3.89355C17.8376 3.23336 18.4389 2.65737 19.1328 2.19629C20.3012 1.4199 21.6779 1.00339 23.0879 1Z" fill="currentColor" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="35" height="36" viewBox="0 0 35 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.0879 6C26.9887 6 28.7592 6.93575 30.0762 8.42188C31.3956 9.91097 32.2001 11.89 32.2002 13.8408C32.2002 18.2337 29.1054 22.3696 25.5225 25.499C23.7556 27.0422 21.9254 28.2909 20.4199 29.1494C19.6667 29.5789 19.0062 29.904 18.4863 30.1182C18.2262 30.2253 18.0121 30.3001 17.8467 30.3467C17.6728 30.3956 17.5993 30.4002 17.5996 30.4004C17.5928 30.3997 17.5178 30.3932 17.3525 30.3467C17.1871 30.3001 16.973 30.2253 16.7129 30.1182C16.193 29.904 15.5333 29.5788 14.7803 29.1494C13.2748 28.2909 11.4447 27.0423 9.67773 25.499C6.0947 22.3696 3 18.2338 3 13.8408C3.00008 11.8901 3.80381 9.91108 5.12207 8.42188C6.43787 6.93557 8.20587 6.00033 10.1006 6C11.5129 6.00117 12.8927 6.41742 14.0635 7.19434C14.7589 7.65582 15.3618 8.2322 15.8486 8.89355C16.2951 9.50017 17.0032 9.73135 17.5996 9.73145C18.1961 9.73144 18.905 9.5003 19.3516 8.89355C19.8376 8.23336 20.4389 7.65737 21.1328 7.19629C22.3012 6.4199 23.6779 6.00339 25.0879 6Z" stroke="#5E2A28" stroke-width="2"/>
|
||||
<path d="M25.0879 6C26.9887 6 28.7592 6.93575 30.0762 8.42188C31.3956 9.91097 32.2001 11.89 32.2002 13.8408C32.2002 18.2337 29.1054 22.3696 25.5225 25.499C23.7556 27.0422 21.9254 28.2909 20.4199 29.1494C19.6667 29.5789 19.0062 29.904 18.4863 30.1182C18.2262 30.2253 18.0121 30.3001 17.8467 30.3467C17.6728 30.3956 17.5993 30.4002 17.5996 30.4004C17.5928 30.3997 17.5178 30.3932 17.3525 30.3467C17.1871 30.3001 16.973 30.2253 16.7129 30.1182C16.193 29.904 15.5333 29.5788 14.7803 29.1494C13.2748 28.2909 11.4447 27.0423 9.67773 25.499C6.0947 22.3696 3 18.2338 3 13.8408C3.00008 11.8901 3.80381 9.91108 5.12207 8.42188C6.43787 6.93557 8.20587 6.00033 10.1006 6C11.5129 6.00117 12.8927 6.41742 14.0635 7.19434C14.7589 7.65582 15.3618 8.2322 15.8486 8.89355C16.2951 9.50017 17.0032 9.73135 17.5996 9.73145C18.1961 9.73144 18.905 9.5003 19.3516 8.89355C19.8376 8.23336 20.4389 7.65737 21.1328 7.19629C22.3012 6.4199 23.6779 6.00339 25.0879 6Z" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
client/assets/theme/nature/1.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
client/assets/theme/nature/10.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
client/assets/theme/nature/11.png
Normal file
|
After Width: | Height: | Size: 532 KiB |
BIN
client/assets/theme/nature/12.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
client/assets/theme/nature/13.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
client/assets/theme/nature/14.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
client/assets/theme/nature/15.png
Normal file
|
After Width: | Height: | Size: 693 KiB |
BIN
client/assets/theme/nature/17.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
BIN
client/assets/theme/nature/18.png
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
client/assets/theme/nature/19.png
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
client/assets/theme/nature/2.png
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
client/assets/theme/nature/20.png
Normal file
|
After Width: | Height: | Size: 461 KiB |
BIN
client/assets/theme/nature/22.png
Normal file
|
After Width: | Height: | Size: 606 KiB |
BIN
client/assets/theme/nature/3.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
client/assets/theme/nature/4.png
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
client/assets/theme/nature/5.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
client/assets/theme/nature/6.png
Normal file
|
After Width: | Height: | Size: 721 KiB |
BIN
client/assets/theme/nature/7.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
client/assets/theme/nature/8.png
Normal file
|
After Width: | Height: | Size: 415 KiB |
BIN
client/assets/theme/nature/9.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
@@ -50,6 +50,29 @@ type Props = {
|
||||
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();
|
||||
|
||||
@@ -260,11 +283,11 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
|
||||
setFavorites(list);
|
||||
}
|
||||
|
||||
async function handleRemove(id: string) {
|
||||
async function handleRemove(favId: string) {
|
||||
// 1. 调用存储层移除收藏
|
||||
await removeFavorite(id);
|
||||
await removeFavorite(favId);
|
||||
// 2. 更新本地状态
|
||||
setFavorites(prev => prev.filter(item => item.id !== id));
|
||||
setFavorites(prev => prev.filter(item => item.favId !== favId));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -274,7 +297,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
|
||||
) : (
|
||||
<FlatList
|
||||
data={favorites}
|
||||
keyExtractor={(it) => it.id}
|
||||
keyExtractor={(it) => it.favId}
|
||||
contentContainerStyle={styles.favList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
@@ -290,11 +313,30 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
|
||||
<View style={styles.favRight}>
|
||||
<View style={[
|
||||
styles.favThumb,
|
||||
{ backgroundColor: item.background } // 动态同步 Home 页的背景
|
||||
item.themeMode === 'scenery' ? {} : { backgroundColor: item.background }
|
||||
]}>
|
||||
<Text style={styles.favThumbText} numberOfLines={4}>{item.text}</Text>
|
||||
{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.id)}
|
||||
onPress={() => handleRemove(item.favId)}
|
||||
style={styles.favRemoveBtn}
|
||||
hitSlop={10}
|
||||
>
|
||||
@@ -765,6 +807,7 @@ const styles = StyleSheet.create({
|
||||
position: 'relative',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(119, 47, 0, 0.05)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
favThumbText: {
|
||||
fontSize: 15,
|
||||
|
||||
@@ -70,6 +70,7 @@ export async function setReaction(contentId: string, reaction: Reaction): Promis
|
||||
}
|
||||
|
||||
export type FavoriteItem = {
|
||||
favId: string; // 唯一标识,支持重复点赞同一文案
|
||||
id: string;
|
||||
date: string;
|
||||
themeMode: ThemeMode;
|
||||
@@ -82,15 +83,15 @@ export async function getFavorites(): Promise<FavoriteItem[]> {
|
||||
|
||||
export async function addFavorite(item: FavoriteItem): Promise<void> {
|
||||
const list = await getFavorites();
|
||||
if (list.some(i => i.id === item.id)) return;
|
||||
// 允许重复点赞,不再根据 id 去重
|
||||
const newList = [item, ...list];
|
||||
console.log('Adding to favorites, new list size:', newList.length);
|
||||
await setJson(KEY_FAVORITES_ITEMS, newList);
|
||||
}
|
||||
|
||||
export async function removeFavorite(contentId: string): Promise<void> {
|
||||
export async function removeFavorite(favId: string): Promise<void> {
|
||||
const list = await getFavorites();
|
||||
const next = list.filter(item => item.id !== contentId);
|
||||
const next = list.filter(item => item.favId !== favId);
|
||||
await setJson(KEY_FAVORITES_ITEMS, next);
|
||||
}
|
||||
|
||||
|
||||