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;
相关推荐
wen's9 小时前
React Native安卓刘海屏适配终极方案:仅需修改 AndroidManifest.xml!
android·xml·react native
Misha韩9 小时前
React Native 一些API详解
react native·react.js
小李飞飞砖9 小时前
React Native 组件间通信方式详解
javascript·react native·react.js
小李飞飞砖9 小时前
React Native 状态管理方案全面对比
javascript·react native·react.js
朝阳391 天前
ReactNative【实战系列教程】我的小红书 6 -- 购物(含商品搜索、商品分类、商品列表)
react native
Misha韩2 天前
React Native 基础组件详解<二>
react native·组件
朝阳392 天前
ReactNative【实战】瀑布流布局列表(含图片自适应、点亮红心动画)
react native
_一两风4 天前
深入理解 React 事件机制与 DOM 事件系统
react native·react.js
Misha韩4 天前
React Native 初始化项目和模拟器运行
react native