ReactNative【实战系列教程】我的小红书 8 -- 我(含左侧弹窗菜单,右下角图标等)

最终效果

点左上角菜单按钮,弹出左侧菜单后

代码实现

app/(tabs)/mine.tsx

c 复制代码
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}>
        <TouchableOpacity
          style={styles.menuButton}
          onPress={() => {
            sideMenuRef.current?.show();
          }}
        >
          <Entypo name="menu" size={24} color="white" />
        </TouchableOpacity>
        <View style={{ flex: 1 }} />
        <Entypo
          style={styles.rightMenuImg}
          name="shopping-cart"
          size={24}
          color="white"
        />
        <Entypo
          style={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 (
      <View
        onLayout={(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>
              <MaterialCommunityIcons
                style={{
                  marginLeft: 6,
                }}
                name="qrcode"
                size={12}
                color="white"
              />
            </View>
          </View>
        </View>
        <Text style={styles.descTxt}>{desc}</Text>
        <View style={styles.sexLayout}>
          <MaterialCommunityIcons
            name={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}>
        <TouchableOpacity
          style={styles.tabButton}
          onPress={() => {
            setTabIndex(0);
          }}
        >
          <Text style={tabIndex === 0 ? styles.tabTxtSelected : styles.tabTxt}>
            笔记
          </Text>
          {tabIndex === 0 && <View style={styles.line} />}
        </TouchableOpacity>
        <TouchableOpacity
          style={styles.tabButton}
          onPress={() => {
            setTabIndex(1);
          }}
        >
          <Text style={tabIndex === 1 ? styles.tabTxtSelected : styles.tabTxt}>
            收藏
          </Text>
          {tabIndex === 1 && <View style={styles.line} />}
        </TouchableOpacity>
        <TouchableOpacity
          style={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 (
            <TouchableOpacity
              key={`${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}>
                <Image
                  style={styles.avatarImg}
                  source={{ uri: item.avatarUrl }}
                />
                <Text style={styles.nameTxt}>{item.userName}</Text>
                <Heart
                  value={item.isFavorite}
                  onValueChanged={(value: boolean) => {
                    console.log(value);
                  }}
                />
                <Text style={styles.countTxt}>{item.favoriteCount}</Text>
              </View>
            </TouchableOpacity>
          );
        })}
      </View>
    );
  };
  return (
    <View style={styles.page}>
      <Image
        style={[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

左侧弹窗菜单

c 复制代码
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 }]}>
        <ScrollView
          style={styles.scrollView}
          contentContainerStyle={styles.container}
          showsVerticalScrollIndicator={false}
        >
          {MENUS.map((item, index) => {
            return (
              <View key={`${index}`}>
                {item.map((subItem, subIndex) => {
                  return (
                    <TouchableOpacity
                      key={`${index}-${subIndex}`}
                      style={styles.menuItem}
                      onPress={onMenuItemPress(subItem)}
                    >
                      <Image
                        style={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 (
              <TouchableOpacity
                key={`${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 (
    <Modal
      transparent={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

c 复制代码
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.View
        style={{
          width: size,
          height: size,
          borderRadius: size / 2,
          borderWidth: size / 20,
          position: "absolute",
          borderColor: "#ff2442",
          transform: [{ scale: scale }],
          opacity: alpha,
        }}
      />
    </TouchableOpacity>
  );
};

components/Empty.tsx

c 复制代码
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

c 复制代码
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;
相关推荐
墨狂之逸才14 小时前
React Native 状态管理大比拼:Event Bus 还是 Context?小白一看就懂!
react native
爱滑雪的码农14 小时前
React Native 完整开发全流程(从零到上线)
javascript·react native·react.js
沐言人生14 小时前
ReactNative 源码分析12——Native View创建流程onBatchComplete
android·react native
沐言人生3 天前
ReactNative 源码分析11——Native View创建流程setChildren和manageChildren
android·react native
沐言人生4 天前
ReactNative 源码分析10——Native View创建流程createView
android·react native
坏小虎4 天前
【聊天列表组件选型建议】FlashList、FlatList、LegendList三种列表组件
javascript·react native·react.js
sealaugh325 天前
react native(学习笔记第五课) 英语打卡微应用(4)- frontend的列表展示
笔记·学习·react native
沐言人生6 天前
ReactNative 源码分析9——Native View初始化
android·react native
接着奏乐接着舞6 天前
react native expo打包
javascript·react native·react.js
jxm_csdn7 天前
Expo Go 本地命令行编译 apk(Ubutnu22.04)
react native