开源鸿蒙-基于React搭建GitCode口袋工具-3
参考博主的文章,下面为博主链接。实现了 仓库列表展示(Star 列表)、网络层封装、仓库卡片组件 RepoItem、首页渲染与路由组织结构。如图所示:【开源鸿蒙跨平台开发学习笔记】Day07:React Native 开发 HarmonyOS-GitCode口袋工具开发-3-CSDN博客如果显示的图片是这样的,是因为在Gitcode中作者未上传仓库的图片,原文章代码也没做使用默认图片占位的操作。
·
前言
参考博主的文章,下面为博主链接。实现了 仓库列表展示(Star 列表)、网络层封装、仓库卡片组件 RepoItem、首页渲染与路由组织结构。如图所示:

如果显示的图片是这样的,是因为
在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'},
});
二、模拟器运行
完成上述步骤后,直接模拟器运行,运行出来,如下图的效果。

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





所有评论(0)