前言

参考博主的文章,下面为博主链接。实现了 仓库列表展示(Star 列表)、网络层封装、仓库卡片组件 RepoItem、首页渲染与路由组织结构。如图所示:

【开源鸿蒙跨平台开发学习笔记】Day07:React Native 开发 HarmonyOS-GitCode口袋工具开发-3-CSDN博客https://blog.csdn.net/qiaomu8559968/article/details/155497422?spm=1001.2014.3001.5502

如果显示的图片是这样的,是因为

在Gitcode中作者未上传仓库的图片,原文章代码也没做使用默认图片占位的操作。"AtomGit | GitCode - 全球开发者的开源社区,开源代码托管平台"

如果项目没上传头像,就使用CSDN默认的程序猿头像,本文章修改后的代码:

import React, {useEffect, useState} from 'react';
import {
  View, 
  Text, 
  StyleSheet, 
  Image, 
  ActivityIndicator, 
  FlatList, 
  TouchableOpacity, 
  Linking
} from 'react-native';
import {fetchUserProfile, fetchStarred} from '../api';
import {UserProfile} from '../types/user';
import {Repo} from '../types/repo';
import {RepoItem} from '../components/RepoItem';
import {getErrorMessage} from '../api/client';

interface ExploreScreenProps {
  username?: string;
}

export function ExploreScreen({username = 'Deng666'}: ExploreScreenProps) {
  const [user, setUser] = useState<UserProfile | null>(null);
  const [starred, setStarred] = useState<Repo[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [hasError, setHasError] = useState<string>('');

  useEffect(() => {
    let mounted = true;
    setIsLoading(true);
    setHasError('');
    Promise.all([
      fetchUserProfile(username),
      fetchStarred(username),
    ])
      .then(([userData, starredData]) => {
        if (!mounted) return;
        setUser(userData);
        setStarred((starredData || []).map(r => ({...r, isStarred: true})));
      })
      .catch(e => {
        if (!mounted) return;
        setHasError(getErrorMessage(e));
      })
      .finally(() => {
        if (!mounted) return;
        setIsLoading(false);
      });
    return () => { mounted = false; };
  }, [username]);

  if (isLoading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator />
        <Text style={styles.loadingText}>加载中</Text>
      </View>
    );
  }

  if (hasError) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>请求失败:{hasError}</Text>
      </View>
    );
  }

  if (!user) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>暂无数据</Text>
      </View>
    );
  }

  // 按照文章写法,分离用户信息头部组件
  const UserInfoHeader = () => (
    <View style={styles.header}>
      <Image source={{uri: user.avatar_url}} style={styles.avatar} />
      <Text style={styles.title}>{user.name || user.login}</Text>
      <Text style={styles.subtitle}>类型:{(user as any).type || 'User'}</Text>
      <Text style={styles.subtitle}>粉丝:{user.followers || 0},关注:{user.following || 0}</Text>
      <TouchableOpacity 
        onPress={() => Linking.openURL(String(user.html_url))} 
        style={styles.linkButton} 
        activeOpacity={0.9}
      >
        <Text style={styles.linkText}>打开主页</Text>
      </TouchableOpacity>
      <Text style={styles.sectionTitle}>已 Star 的仓库</Text>
    </View>
  );

  return (
    <FlatList
      data={starred}
      keyExtractor={(item, index) => String(item.id ?? index)}
      renderItem={({item}) => {
        // 🎯 关键修复:处理头像URL
        // 如果仓库所有者的头像URL不存在或为空,使用默认图片
        const avatarUrl = item.owner?.avatar_url;
        const logoUrl = avatarUrl && avatarUrl.trim() !== '' 
          ? avatarUrl 
          : 'https://cdn-img.gitcode.com/logo.png'; // GitCode 默认logo
        
        return (
          <RepoItem
            logo={logoUrl} // ✅ 使用处理后的URL
            name={item.name || item.path || item.full_name || '未知仓库'}
            description={item.description || ''}
            language={item.language || ''}
            stars={item.stargazers_count || 0}
            commits={item.commits_count || item.watchers_count || 0}
            isStarred={!!item.isStarred}
            onToggleStar={() => {
              setStarred(prev =>
                prev.map(r =>
                  (r.id ?? r.name) === (item.id ?? item.name)
                    ? {...r, isStarred: !r.isStarred}
                    : r,
                ),
              );
            }}
          />
        );
      }}
      ListHeaderComponent={<UserInfoHeader />}
      contentContainerStyle={styles.listContent}
      style={styles.flatList}
    />
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1, 
    alignItems: 'center', 
    justifyContent: 'center', 
    backgroundColor: '#FFFFFF'
  },
  loadingText: {
    marginTop: 8, 
    fontSize: 14, 
    color: '#666'
  },
  errorText: {
    fontSize: 14, 
    color: '#d00'
  },
  // FlatList 容器样式 - 关键修复
  flatList: {
    flex: 1,
    width: '100%', // 确保宽度100%
  },
  // ListHeaderComponent 样式
  header: {
    alignItems: 'center', 
    paddingVertical: 24, 
    backgroundColor: '#fff',
    width: '100%', // 确保头部宽度100%
  },
  avatar: {
    width: 120, 
    height: 120, 
    borderRadius: 60, 
    backgroundColor: '#eee'
  },
  title: {
    marginTop: 16, 
    fontSize: 24, 
    fontWeight: '700'
  },
  subtitle: {
    marginTop: 8, 
    fontSize: 16, 
    color: '#666'
  },
  sectionTitle: {
    marginTop: 24, 
    fontSize: 18, 
    fontWeight: '600', 
    color: '#222', 
    alignSelf: 'flex-start', 
    marginLeft: 24
  },
  bio: {
    marginTop: 12, 
    fontSize: 14, 
    color: '#333', 
    paddingHorizontal: 24, 
    textAlign: 'center'
  },
  linkButton: {
    marginTop: 16, 
    paddingHorizontal: 16, 
    paddingVertical: 10, 
    borderRadius: 6, 
    backgroundColor: '#007aff'
  },
  linkText: {
    color: '#fff', 
    fontSize: 14, 
    fontWeight: '600'
  },
  // 关键修复:按照文章写法设置 contentContainerStyle
  listContent: {
    flexGrow: 1,
    backgroundColor: '#f8f9fa', // 文章中的背景色
  },
});

一、修改代码

如果文件不存在则新建。

1.1 修改repo.tsx代码

修改"src/types/repo.tsx"的代码

export interface RepoOwner {
  id?: string | number;
  login?: string;
  name?: string;
  avatar_url?: string;
  html_url?: string;
  type?: string;
}

export interface Repo {
  id: string | number;
  name?: string;
  path?: string;
  full_name?: string;
  description?: string;
  language?: string;
  stargazers_count?: number;
  watchers_count?: number;
  commits_count?: number;
  web_url?: string;
  html_url?: string;
  owner?: RepoOwner;
  isStarred?: boolean;
}

1.2 修改client.tsx代码

修改"src/api/client.tsx"的代码

import axios from 'axios';

//改为你的令牌
const DEFAULT_TOKEN = '你的令牌';
let privateToken = DEFAULT_TOKEN;

export function setPrivateToken(token?: string) {
  if (token) privateToken = token;
}

export const http = axios.create({
  baseURL: 'https://api.gitcode.com/api/v5/',
  timeout: 10000,
});

http.interceptors.request.use(config => {
  const headers = config.headers ?? {};
  headers['private-token'] = privateToken;
  config.headers = headers;
  return config;
});

http.interceptors.response.use(res => res, err => Promise.reject(err));

export function getErrorMessage(error: unknown): string {
  if (axios.isAxiosError(error)) {
    const status = error.response?.status;
    const data = error.response?.data as any;
    const msg = typeof data === 'string' ? data : data?.message || data?.error || error.message;
    return status ? `${status} ${msg}` : msg;
  }
  return String((error as any)?.message || error);
}

1.3 修改user.tsx代码

修改"src/api/user.tsx"的代码

// src/api/user.ts
import {http} from './client';
import {UserProfile} from '../types/user';
import {Repo} from '../types/repo';

export async function fetchUserProfile(username: string): Promise<UserProfile> {
  const res = await http.get<UserProfile>(`users/${encodeURIComponent(username)}`);
  return res.data;
}

export async function fetchStarred(username: string): Promise<Repo[]> {
  const res = await http.get<Repo[]>(`users/${encodeURIComponent(username)}/starred`);
  return res.data;
}

1.4 修改RepoItem.tsx代码

修改"src/components/RepoItem.tsx"的代码

import React from 'react';
import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native';

export interface RepoItemProps {
  logo: string;
  name: string;
  description: string;
  language: string;
  stars: number;
  commits: number;
  isStarred: boolean;
  onToggleStar: () => void;
}

export function RepoItem(props: RepoItemProps) {
  const {logo, name, description, language, stars, commits, isStarred, onToggleStar} = props;
  return (
    <View style={styles.container}>
      <Image source={{uri: logo}} style={styles.logo} />
      <View style={styles.content}>
        <View style={styles.row}>
          <Text style={styles.name}>{name}</Text>
          <TouchableOpacity onPress={onToggleStar} style={styles.starBtn}>
            <Text style={styles.starText}>{isStarred ? '★ Starred' : '☆ Star'}</Text>
          </TouchableOpacity>
        </View>
        {!!description && <Text style={styles.desc}>{description}</Text>}
        <View style={styles.footer}>
          {!!language && <Text style={styles.lang}>{language}</Text>}
          <Text style={styles.meta}>⭐ {stars}</Text>
          <Text style={styles.meta}>🔗 {commits}</Text>
        </View>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {width: '100%', flexDirection: 'row', padding: 12, borderBottomWidth: 1, borderColor: '#eee', backgroundColor: '#fff'},
  logo: {width: 48, height: 48, borderRadius: 8, backgroundColor: '#eee'},
  content: {flex: 1, marginLeft: 12},
  row: {flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between'},
  name: {fontSize: 16, fontWeight: '700'},
  starBtn: {paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, backgroundColor: '#007aff'},
  starText: {color: '#fff', fontSize: 12},
  desc: {marginTop: 6, fontSize: 13, color: '#333'},
  footer: {flexDirection: 'row', alignItems: 'center', marginTop: 8},
  lang: {fontSize: 12, color: '#666', marginRight: 12},
  meta: {fontSize: 12, color: '#888', marginRight: 12},
});

1.5 修改ExploreScreen.tsx代码

修改"src/screens/ExploreScreen.tsx"的代码

import React, {useEffect, useState} from 'react';
import {
  View, 
  Text, 
  StyleSheet, 
  Image, 
  ActivityIndicator, 
  FlatList, 
  TouchableOpacity, 
  Linking
} from 'react-native';
import {fetchUserProfile, fetchStarred} from '../api';
import {UserProfile} from '../types/user';
import {Repo} from '../types/repo';
import {RepoItem} from '../components/RepoItem';
import {getErrorMessage} from '../api/client';

interface ExploreScreenProps {
  username?: string;
}

export function ExploreScreen({username = 'Deng666'}: ExploreScreenProps) {
  const [user, setUser] = useState<UserProfile | null>(null);
  const [starred, setStarred] = useState<Repo[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [hasError, setHasError] = useState<string>('');

  useEffect(() => {
    let mounted = true;
    setIsLoading(true);
    setHasError('');
    Promise.all([
      fetchUserProfile(username),
      fetchStarred(username),
    ])
      .then(([userData, starredData]) => {
        if (!mounted) return;
        setUser(userData);
        setStarred((starredData || []).map(r => ({...r, isStarred: true})));
      })
      .catch(e => {
        if (!mounted) return;
        setHasError(getErrorMessage(e));
      })
      .finally(() => {
        if (!mounted) return;
        setIsLoading(false);
      });
    return () => { mounted = false; };
  }, [username]);

  if (isLoading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator />
        <Text style={styles.loadingText}>加载中</Text>
      </View>
    );
  }

  if (hasError) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>请求失败:{hasError}</Text>
      </View>
    );
  }

  if (!user) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>暂无数据</Text>
      </View>
    );
  }

  // 按照文章写法,分离用户信息头部组件
  const UserInfoHeader = () => (
    <View style={styles.header}>
      <Image source={{uri: user.avatar_url}} style={styles.avatar} />
      <Text style={styles.title}>{user.name || user.login}</Text>
      <Text style={styles.subtitle}>类型:{(user as any).type || 'User'}</Text>
      <Text style={styles.subtitle}>粉丝:{user.followers || 0},关注:{user.following || 0}</Text>
      <TouchableOpacity 
        onPress={() => Linking.openURL(String(user.html_url))} 
        style={styles.linkButton} 
        activeOpacity={0.9}
      >
        <Text style={styles.linkText}>打开主页</Text>
      </TouchableOpacity>
      <Text style={styles.sectionTitle}>已 Star 的仓库</Text>
    </View>
  );

  return (
    <FlatList
      data={starred}
      keyExtractor={(item, index) => String(item.id ?? index)}
      renderItem={({item}) => {
        // 🎯 关键修复:处理头像URL
        // 如果仓库所有者的头像URL不存在或为空,使用默认图片
        const avatarUrl = item.owner?.avatar_url;
        const logoUrl = avatarUrl && avatarUrl.trim() !== '' 
          ? avatarUrl 
          : 'https://cdn-img.gitcode.com/logo.png'; // GitCode 默认logo
        
        return (
          <RepoItem
            logo={logoUrl} // ✅ 使用处理后的URL
            name={item.name || item.path || item.full_name || '未知仓库'}
            description={item.description || ''}
            language={item.language || ''}
            stars={item.stargazers_count || 0}
            commits={item.commits_count || item.watchers_count || 0}
            isStarred={!!item.isStarred}
            onToggleStar={() => {
              setStarred(prev =>
                prev.map(r =>
                  (r.id ?? r.name) === (item.id ?? item.name)
                    ? {...r, isStarred: !r.isStarred}
                    : r,
                ),
              );
            }}
          />
        );
      }}
      ListHeaderComponent={<UserInfoHeader />}
      contentContainerStyle={styles.listContent}
      style={styles.flatList}
    />
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1, 
    alignItems: 'center', 
    justifyContent: 'center', 
    backgroundColor: '#FFFFFF'
  },
  loadingText: {
    marginTop: 8, 
    fontSize: 14, 
    color: '#666'
  },
  errorText: {
    fontSize: 14, 
    color: '#d00'
  },
  // FlatList 容器样式 - 关键修复
  flatList: {
    flex: 1,
    width: '100%', // 确保宽度100%
  },
  // ListHeaderComponent 样式
  header: {
    alignItems: 'center', 
    paddingVertical: 24, 
    backgroundColor: '#fff',
    width: '100%', // 确保头部宽度100%
  },
  avatar: {
    width: 120, 
    height: 120, 
    borderRadius: 60, 
    backgroundColor: '#eee'
  },
  title: {
    marginTop: 16, 
    fontSize: 24, 
    fontWeight: '700'
  },
  subtitle: {
    marginTop: 8, 
    fontSize: 16, 
    color: '#666'
  },
  sectionTitle: {
    marginTop: 24, 
    fontSize: 18, 
    fontWeight: '600', 
    color: '#222', 
    alignSelf: 'flex-start', 
    marginLeft: 24
  },
  bio: {
    marginTop: 12, 
    fontSize: 14, 
    color: '#333', 
    paddingHorizontal: 24, 
    textAlign: 'center'
  },
  linkButton: {
    marginTop: 16, 
    paddingHorizontal: 16, 
    paddingVertical: 10, 
    borderRadius: 6, 
    backgroundColor: '#007aff'
  },
  linkText: {
    color: '#fff', 
    fontSize: 14, 
    fontWeight: '600'
  },
  // 关键修复:按照文章写法设置 contentContainerStyle
  listContent: {
    flexGrow: 1,
    backgroundColor: '#f8f9fa', // 文章中的背景色
  },
});

1.6 修改AppRoot.tsx代码

修改"src/navigation/AppRoot.tsx"的代码

import React, {useMemo, useState} from 'react';
import {SafeAreaView, View, StyleSheet} from 'react-native';
import {BottomTabBar, TabItem} from '../components/BottomTabBar';
import HomeScreen from '../screens/HomeScreen';  // 改为默认导入
import { ExploreScreen } from '../screens/ExploreScreen';  // 改为默认导入
import SettingsScreen from '../screens/SettingsScreen';  // 改为默认导入

export function AppRoot() {
  const tabs: TabItem[] = [
    {key: 'home', title: '首页'},
    {key: 'explore', title: '探索'},
    {key: 'settings', title: '我的'},
  ];
  const [activeKey, setActiveKey] = useState(tabs[0].key);

  const ActiveComponent = useMemo(() => {
    if (activeKey === 'home') return HomeScreen;
    if (activeKey === 'explore') return ExploreScreen;
    if (activeKey === 'settings') return SettingsScreen;
    return HomeScreen;
  }, [activeKey]);

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.content}>
        <ActiveComponent />
      </View>
      <BottomTabBar tabs={tabs} activeKey={activeKey} onTabPress={setActiveKey} />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {flex: 1, backgroundColor: '#FFFFFF'},
  content: {flex: 1, alignItems: 'center', justifyContent: 'center'},
});

二、模拟器运行

完成上述步骤后,直接模拟器运行,运行出来,如下图的效果。

本次文章到此结束,感谢大家的观看,如果文章对你有帮助,请点赞支持一下吧~~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐