最终效果
点左上角菜单按钮,弹出左侧菜单后
代码实现
app/(tabs)/mine.tsx
import icon_add from "@/assets/icons/icon_add.png";
import mine_bg from "@/assets/images/mine_bg.png";
import Heart from "@/components/Heart";
import articleList from "@/mock/articleList";
import SideMenu, { SideMenuRef } from "@/modules/mine/components/SideMenu";
import Entypo from "@expo/vector-icons/Entypo";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { useRouter } from "expo-router";
import { useCallback, useRef, useState } from "react";
import {Dimensions,Image,LayoutChangeEvent,ScrollView,StyleSheet,Text,TouchableOpacity,View,
} from "react-native";
import icon_no_collection from "../../assets/icons/icon_no_collection.webp";
import icon_no_favorate from "../../assets/icons/icon_no_favorate.webp";
import icon_no_note from "../../assets/icons/icon_no_note.webp";
import Empty from "../../components/Empty";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const EMPTY_CONFIG = [{ icon: icon_no_note, tips: "快去发布今日的好心情吧~" },{ icon: icon_no_collection, tips: "快去收藏你喜欢的作品吧~" },{ icon: icon_no_favorate, tips: "喜欢点赞的人运气不会太差哦~" },
];
export default function MineScreen() {const sideMenuRef = useRef<SideMenuRef>(null);const router = useRouter();const [bgImgHeight, setBgImgHeight] = useState<number>(400);const [tabIndex, setTabIndex] = useState<number>(0);const onArticlePress = useCallback((article: ArticleSimple) => () => {router.push(`/articleDetail?id=${article.id}`);},[]);const renderTitle = () => {const styles = StyleSheet.create({titleLayout: {width: "100%",height: 48,flexDirection: "row",alignItems: "center",},menuButton: {height: "100%",paddingHorizontal: 16,justifyContent: "center",},menuImg: {width: 28,height: 28,resizeMode: "contain",},rightMenuImg: {marginRight: 14,},});return (<View style={styles.titleLayout}><TouchableOpacitystyle={styles.menuButton}onPress={() => {sideMenuRef.current?.show();}}><Entypo name="menu" size={24} color="white" /></TouchableOpacity><View style={{ flex: 1 }} /><Entypostyle={styles.rightMenuImg}name="shopping-cart"size={24}color="white"/><Entypostyle={styles.rightMenuImg}name="share"size={24}color="white"/></View>);};const renderInfo = () => {const userInfo = {avatar:"https://img0.baidu.com/it/u=919979501,2820948992&fm=253&app=120&f=JPEG?w=800&h=800",nickName: "清禾",redBookId: "635942",desc: "钟爱编程,偏前端开发,欢迎私信我加入EC尽享编程俱乐部共同学习,交流成长!",sex: "female",};const { avatar, nickName, redBookId, desc, sex } = userInfo;const styles = StyleSheet.create({avatarLayout: {width: "100%",flexDirection: "row",alignItems: "flex-end",padding: 16,},avatarImg: {width: 96,height: 96,resizeMode: "cover",borderRadius: 48,},addImg: {width: 28,height: 28,marginLeft: -28,marginBottom: 2,},nameLayout: {marginLeft: 20,},nameTxt: {fontSize: 22,color: "white",fontWeight: "bold",},idLayout: {flexDirection: "row",alignItems: "center",marginTop: 16,marginBottom: 20,},idTxt: {fontSize: 12,color: "#bbb",},qrcodeImg: {width: 12,height: 12,marginLeft: 6,tintColor: "#bbb",},descTxt: {fontSize: 14,color: "white",paddingHorizontal: 16,},sexLayout: {width: 32,height: 24,backgroundColor: "#ffffff50",borderRadius: 12,marginTop: 12,marginLeft: 16,justifyContent: "center",alignItems: "center",},sexImg: {width: 12,height: 12,resizeMode: "contain",},infoLayout: {width: "100%",paddingRight: 16,flexDirection: "row",alignItems: "center",marginTop: 20,marginBottom: 28,},infoItem: {alignItems: "center",paddingHorizontal: 12,},infoValue: {fontSize: 18,color: "white",},infoLabel: {fontSize: 12,color: "#ddd",marginTop: 6,},infoButton: {height: 32,paddingHorizontal: 16,borderWidth: 1,borderColor: "white",borderRadius: 16,justifyContent: "center",alignItems: "center",marginLeft: 16,},editTxt: {fontSize: 14,color: "#ffffff",},settingImg: {width: 20,height: 20,tintColor: "#ffffff",},});return (<ViewonLayout={(e: LayoutChangeEvent) => {const { height } = e.nativeEvent.layout;setBgImgHeight(height);}}><View style={styles.avatarLayout}><Image style={styles.avatarImg} source={{ uri: avatar }} /><Image style={styles.addImg} source={icon_add} /><View style={styles.nameLayout}><Text style={styles.nameTxt}>{nickName}</Text><View style={styles.idLayout}><Text style={styles.idTxt}>小红书号:{redBookId}</Text><MaterialCommunityIconsstyle={{marginLeft: 6,}}name="qrcode"size={12}color="white"/></View></View></View><Text style={styles.descTxt}>{desc}</Text><View style={styles.sexLayout}><MaterialCommunityIconsname={sex === "male" ? "gender-male" : "gender-female"}size={14}color="white"/></View><View style={styles.infoLayout}><View style={styles.infoItem}><Text style={styles.infoValue}>1</Text><Text style={styles.infoLabel}>关注</Text></View><View style={styles.infoItem}><Text style={styles.infoValue}>65</Text><Text style={styles.infoLabel}>粉丝</Text></View><View style={styles.infoItem}><Text style={styles.infoValue}>625</Text><Text style={styles.infoLabel}>获赞与收藏</Text></View><View style={{ flex: 1 }} /><TouchableOpacity style={styles.infoButton}><Text style={styles.editTxt}>编辑资料</Text></TouchableOpacity><TouchableOpacity style={styles.infoButton}><MaterialIcons name="settings" size={20} color="white" /></TouchableOpacity></View></View>);};const renderTabs = () => {const styles = StyleSheet.create({titleLayout: {width: "100%",height: 48,flexDirection: "row",alignItems: "center",justifyContent: "center",backgroundColor: "white",paddingHorizontal: 16,borderTopLeftRadius: 12,borderTopRightRadius: 12,borderBottomWidth: 1,borderBottomColor: "#eee",},icon: {width: 28,height: 28,},line: {width: 28,height: 2,backgroundColor: "#ff2442",borderRadius: 1,position: "absolute",bottom: 6,},tabButton: {height: "100%",flexDirection: "column",alignItems: "center",justifyContent: "center",paddingHorizontal: 14,},tabTxt: {fontSize: 17,color: "#999",},tabTxtSelected: {fontSize: 17,color: "#333",},});return (<View style={styles.titleLayout}><TouchableOpacitystyle={styles.tabButton}onPress={() => {setTabIndex(0);}}><Text style={tabIndex === 0 ? styles.tabTxtSelected : styles.tabTxt}>笔记</Text>{tabIndex === 0 && <View style={styles.line} />}</TouchableOpacity><TouchableOpacitystyle={styles.tabButton}onPress={() => {setTabIndex(1);}}><Text style={tabIndex === 1 ? styles.tabTxtSelected : styles.tabTxt}>收藏</Text>{tabIndex === 1 && <View style={styles.line} />}</TouchableOpacity><TouchableOpacitystyle={styles.tabButton}onPress={() => {setTabIndex(2);}}><Text style={tabIndex === 2 ? styles.tabTxtSelected : styles.tabTxt}>赞过</Text>{tabIndex === 2 && <View style={styles.line} />}</TouchableOpacity></View>);};const renderList = () => {const noteList: ArticleSimple[] = [];const collectionList: ArticleSimple[] = [];const favorateList: ArticleSimple[] = articleList.filter((item) => item.isFavorite);const currentList = [noteList, collectionList, favorateList][tabIndex];if (!currentList?.length) {const config = EMPTY_CONFIG[tabIndex];return <Empty icon={config.icon} tips={config.tips} />;}const styles = StyleSheet.create({listContainer: {width: "100%",flexDirection: "row",flexWrap: "wrap",backgroundColor: "white",},item: {width: (SCREEN_WIDTH - 18) >> 1,backgroundColor: "white",marginLeft: 6,marginBottom: 6,borderRadius: 8,overflow: "hidden",marginTop: 8,},titleTxt: {fontSize: 14,color: "#333",marginHorizontal: 10,marginVertical: 4,},nameLayout: {width: "100%",flexDirection: "row",alignItems: "center",paddingHorizontal: 10,marginBottom: 10,},avatarImg: {width: 20,height: 20,resizeMode: "cover",borderRadius: 10,},nameTxt: {fontSize: 12,color: "#999",marginLeft: 6,flex: 1,},heart: {width: 20,height: 20,resizeMode: "contain",},countTxt: {fontSize: 14,color: "#999",marginLeft: 4,},itemImg: {width: (SCREEN_WIDTH - 18) >> 1,height: 240,},});return (<View style={styles.listContainer}>{currentList.map((item, index) => {return (<TouchableOpacitykey={`${item.id}-${index}`}style={styles.item}onPress={onArticlePress(item)}><Image style={styles.itemImg} source={{ uri: item.image }} /><Text style={styles.titleTxt}>{item.title}</Text><View style={styles.nameLayout}><Imagestyle={styles.avatarImg}source={{ uri: item.avatarUrl }}/><Text style={styles.nameTxt}>{item.userName}</Text><Heartvalue={item.isFavorite}onValueChanged={(value: boolean) => {console.log(value);}}/><Text style={styles.countTxt}>{item.favoriteCount}</Text></View></TouchableOpacity>);})}</View>);};return (<View style={styles.page}><Imagestyle={[styles.bgImg, { height: bgImgHeight + 64 }]}source={mine_bg}/>{renderTitle()}<ScrollView style={styles.scrollView}>{renderInfo()}{renderTabs()}{renderList()}</ScrollView><SideMenu ref={sideMenuRef} /></View>);
}
const styles = StyleSheet.create({scrollView: {width: "100%",flex: 1,},page: {width: "100%",height: "100%",backgroundColor: "white",},bgImg: {position: "absolute",top: 0,width: "100%",height: 400,},
});
相关组件
modules/mine/components/SideMenu.tsx
左侧弹窗菜单
import icon_browse_histroy from "@/assets/icons/icon_browse_history.png";
import icon_community from "@/assets/icons/icon_community.png";
import icon_coupon from "@/assets/icons/icon_coupon.png";
import icon_create_center from "@/assets/icons/icon_create_center.png";
import icon_draft from "@/assets/icons/icon_draft.png";
import icon_exit from "@/assets/icons/icon_exit.png";
import icon_fid_user from "@/assets/icons/icon_find_user.png";
import icon_free_net from "@/assets/icons/icon_free_net.png";
import icon_nice_goods from "@/assets/icons/icon_nice_goods.png";
import icon_orders from "@/assets/icons/icon_orders.png";
import icon_packet from "@/assets/icons/icon_packet.png";
import icon_red_vip from "@/assets/icons/icon_red_vip.png";
import icon_scan from "@/assets/icons/icon_scan.png";
import icon_service from "@/assets/icons/icon_service.png";
import icon_setting from "@/assets/icons/icon_setting.png";
import icon_shop_car from "@/assets/icons/icon_shop_car.png";
import icon_wish from "@/assets/icons/icon_wish.png";
import { remove } from "@/utils/Storage";
import { useRouter } from "expo-router";
import React, {forwardRef,useCallback,useImperativeHandle,useState,
} from "react";
import {Dimensions,Image,LayoutAnimation,Modal,ScrollView,StyleSheet,Text,TouchableOpacity,View,
} from "react-native";
const MENUS = [[{ icon: icon_fid_user, name: "发现好友" }],[{ icon: icon_draft, name: "我的草稿" },{ icon: icon_create_center, name: "创作中心" },{ icon: icon_browse_histroy, name: "浏览记录" },{ icon: icon_packet, name: "钱包" },{ icon: icon_free_net, name: "免流量" },{ icon: icon_nice_goods, name: "好物体验" },],[{ icon: icon_orders, name: "订单" },{ icon: icon_shop_car, name: "购物车" },{ icon: icon_coupon, name: "卡券" },{ icon: icon_wish, name: "心愿单" },{ icon: icon_red_vip, name: "小红书会员" },],[{ icon: icon_community, name: "社区公约" },{ icon: icon_exit, name: "退出登陆" },],
];
const BOTTOM_MENUS = [{ icon: icon_setting, txt: "设置" },{ icon: icon_service, txt: "帮助与客服" },{ icon: icon_scan, txt: "扫一扫" },
];
export interface SideMenuRef {show: () => void;hide: () => void;
}
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const ContentWidth = SCREEN_WIDTH * 0.75;
// eslint-disable-next-line react/display-name
export default forwardRef((props: any, ref) => {const [visible, setVisible] = useState<boolean>(false);const [open, setOpen] = useState<boolean>(false);const router = useRouter();const show = () => {setVisible(true);setTimeout(() => {LayoutAnimation.easeInEaseOut();setOpen(true);}, 100);};const hide = () => {LayoutAnimation.easeInEaseOut();setOpen(false);setTimeout(() => {setVisible(false);}, 300);};useImperativeHandle(ref, () => {return {show,hide,};});const onMenuItemPress = useCallback((item: any) => async () => {if (item.name === "退出登陆") {hide();await remove("userInfo");router.replace("/login");}},[]);const renderContent = () => {return (<View style={[styles.content, { marginLeft: open ? 0 : -ContentWidth }]}><ScrollViewstyle={styles.scrollView}contentContainerStyle={styles.container}showsVerticalScrollIndicator={false}>{MENUS.map((item, index) => {return (<View key={`${index}`}>{item.map((subItem, subIndex) => {return (<TouchableOpacitykey={`${index}-${subIndex}`}style={styles.menuItem}onPress={onMenuItemPress(subItem)}><Imagestyle={styles.menuItemIcon}source={subItem.icon}/><Text style={styles.menuItemTxt}>{subItem.name}</Text></TouchableOpacity>);})}{index !== MENUS.length - 1 && (<View style={styles.divideLine} />)}</View>);})}</ScrollView><View style={styles.bottomLayout}>{BOTTOM_MENUS.map((item) => {return (<TouchableOpacitykey={`${item.txt}`}style={styles.bottomMenuItem}><View style={styles.bottomMenuIconWrap}><Image style={styles.bottomMenuIcon} source={item.icon} /></View><Text style={styles.bottomMenuTxt}>{item.txt}</Text></TouchableOpacity>);})}</View></View>);};return (<Modaltransparent={true}visible={visible}statusBarTranslucent={false}animationType="fade"onRequestClose={hide}><TouchableOpacity style={styles.root} onPress={hide} activeOpacity={1}>{renderContent()}</TouchableOpacity></Modal>);
});
const styles = StyleSheet.create({root: {width: "100%",height: "100%",backgroundColor: "#000000C0",flexDirection: "row",},content: {height: "100%",width: ContentWidth,backgroundColor: "white",},scrollView: {width: "100%",flex: 1,},bottomLayout: {width: "100%",flexDirection: "row",paddingTop: 12,paddingBottom: 20,},bottomMenuItem: {flex: 1,alignItems: "center",},bottomMenuIconWrap: {width: 44,height: 44,backgroundColor: "#f0f0f0",borderRadius: 22,justifyContent: "center",alignItems: "center",},bottomMenuIcon: {width: 26,height: 26,},bottomMenuTxt: {fontSize: 13,color: "#666",marginTop: 8,},divideLine: {width: "100%",height: 1,backgroundColor: "#eee",},menuItem: {width: "100%",height: 64,flexDirection: "row",alignItems: "center",},menuItemIcon: {width: 32,height: 32,resizeMode: "contain",},menuItemTxt: {fontSize: 16,color: "#333",marginLeft: 14,},container: {paddingTop: 10,paddingHorizontal: 28,paddingBottom: 12,},
});
components/Heart.tsx
import AntDesign from "@expo/vector-icons/AntDesign";
import React, { useEffect, useRef, useState } from "react";
import { Animated, TouchableOpacity } from "react-native";
type Props = {value: boolean;onValueChanged?: (value: boolean) => void;size?: number;color?: string;
};
// eslint-disable-next-line react/display-name
export default (props: Props) => {const { value, onValueChanged, size = 20, color = "black" } = props;const [showState, setShowState] = useState<boolean>(false);const scale = useRef<Animated.Value>(new Animated.Value(0)).current;const alpha = useRef<Animated.Value>(new Animated.Value(0)).current;useEffect(() => {setShowState(value);}, [value]);const onHeartPress = () => {const newState = !showState;setShowState(newState);onValueChanged?.(newState);if (newState) {alpha.setValue(1);const scaleAnim = Animated.timing(scale, {toValue: 1.8,duration: 300,useNativeDriver: false,});const alphaAnim = Animated.timing(alpha, {toValue: 0,duration: 400,useNativeDriver: false,delay: 200,});Animated.parallel([scaleAnim, alphaAnim]).start();} else {scale.setValue(0);alpha.setValue(0);}};return (<TouchableOpacity onPress={onHeartPress}>{showState ? (<AntDesign name="heart" size={size} color="red" />) : (<AntDesign name="hearto" size={size} color={color} />)}<Animated.Viewstyle={{width: size,height: size,borderRadius: size / 2,borderWidth: size / 20,position: "absolute",borderColor: "#ff2442",transform: [{ scale: scale }],opacity: alpha,}}/></TouchableOpacity>);
};
components/Empty.tsx
import React from "react";
import { Image, StyleSheet, Text, View } from "react-native";
type Props = {icon: number;tips: string;
};
// eslint-disable-next-line react/display-name
export default ({ icon, tips }: Props) => {return (<View style={styles.root}><Image style={styles.icon} source={icon} /><Text style={styles.tipsTxt}>{tips}</Text></View>);
};
const styles = StyleSheet.create({root: {alignItems: "center",paddingTop: 120,},icon: {width: 96,height: 96,resizeMode: "contain",},tipsTxt: {fontSize: 14,color: "#bbb",marginTop: 16,},
});
模拟数据
mock/articleList.ts
const articleList: ArticleSimple[] = [{id: 1,title: "让我抱抱,一起温暖,真的好治愈",userName: "小飞飞爱猫咪",avatarUrl:"https://img2.baidu.com/it/u=902203086,3868774028&fm=253&app=138&f=JPEG?w=500&h=500",image:"http://gips2.baidu.com/it/u=195724436,3554684702&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960",favoriteCount: 325,isFavorite: true,},{id: 2,title: "不愧是网友给的配方,真的香迷糊了",userName: "大厨师小飞象",avatarUrl:"https://pic.rmb.bdstatic.com/bjh/events/eeae3b71dabc9a372afd7f9e112287086428.jpeg@h_1280",image:"http://gips0.baidu.com/it/u=3602773692,1512483864&fm=3028&app=3028&f=JPEG&fmt=auto?w=960&h=1280",favoriteCount: 1098,isFavorite: true,},{id: 3,title: "一觉醒来,满树的柑橘爬上了我的窗",userName: "小小风筝",avatarUrl:"https://img1.baidu.com/it/u=1811602911,3261262340&fm=253&app=138&f=JPEG?w=500&h=500",image:"http://gips3.baidu.com/it/u=1537137094,335954266&fm=3028&app=3028&f=JPEG&fmt=auto?w=720&h=1280",favoriteCount: 18700,isFavorite: false,},{id: 4,title: "满床清梦压星河",userName: "失忆",avatarUrl:"https://img1.baidu.com/it/u=3505470809,2700212068&fm=253&app=138&f=JPEG?w=500&h=500",image:"https://gips3.baidu.com/it/u=1014935733,598223672&fm=3074&app=3074&f=PNG?w=1440&h=2560",favoriteCount: 8700,isFavorite: true,},{id: 5,title: "手机拍出来的星星,没想到那么多人喜欢",userName: "慢慢",avatarUrl:"https://img1.baidu.com/it/u=1924685292,2387273894&fm=253&app=138&f=JPEG?w=500&h=500",image:"https://img2.baidu.com/it/u=2585843050,3523947274&fm=253&app=138&f=JPEG?w=1422&h=800",favoriteCount: 2655,isFavorite: false,},{id: 6,title: "告白如同田野间的风在青春里轰然",userName: "潇潇",avatarUrl:"https://img1.baidu.com/it/u=3843254675,2187553494&fm=253&app=120&f=JPEG?w=800&h=800",image:"https://img1.baidu.com/it/u=1926713654,274347830&fm=253&app=138&f=JPEG?w=1422&h=800",favoriteCount: 2655,isFavorite: false,},
];
export default articleList;