1.项目介绍
使用新版本react-native 0.71快速开发APP,初步实现了登录、首页、设置和一些demo模块,并自定义了一些通用组件。其中包含路由组件,文本框组件,单元格组件,按钮组件,表格组件和弹窗选择组件简单案列,这些组件都是最简单的实现,是给新手朋友参考学习的。后期可以根据自己需求拓展。
本项目使用了比较新版本的rn框架(0.71),并且使用了ts框架开发。状态管理库没有使用react reducer,使用了Zustand: 一个轻量、现代的状态管理库(类似vue中的Pinia)。
2. 创建项目
创建react-native项目并安装我们需要的依赖库
1.npx react-native init birckdogApp --version 0.71.0
2.npm install zustand
3.npm install @react-navigation/native
4.npm install @react-navigation/native-stack
5.npm install @react-navigation/bottom-tabs
6.npm install react-native-screens react-native-safe-area-context
7.npm install @react-native-async-storage/async-storage
8.npm install react-native-vector-icons --save
9.npm i --save-dev @types/react-native-vector-icons
2.1 我需要对每个项目依赖的maven改成阿里云的仓库
2.2 react-native-vector-icons库需要在主项目的build.gradle文件中添加引用
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle");
3.登录页面
3.1使用zustand构建一个简单UserInfo状态管理对象
使用type定义UserInfo的类型和UserStoreInfo类型,使用zustand的create创建一个useUserInfo 状态管理对象。
useUserInfo 状态管理中user对象用于存储用户数据。
useUserInfo 状态管理中setUserInfos方法给user对象赋值,(user: UserInfo | null) => Promise<void>表示该方法参数为user,参数类型是UserInfo并且参数可以为空,方法返回类型是void的类型Promise对象。
useUserInfo 状态管理中userInitialize方法是把存储在AsyncStorage数据加载到user对象中,因为操作AsyncStorage是异步操作,所以要在方法前面加async。我们这个方法我们会在App主程序启动的时候去加载用户信息,因为zustand在app关闭之后所有的状态管理对象都会丢失。
import { create } from 'zustand';
import AsyncStorage from '@react-native-async-storage/async-storage';
type UserInfo = {
id: string;
account: string;
userName: string;
token: string;
};
type UserStoreInfo = {
user: UserInfo | null;
setUserInfos: (user: UserInfo | null) => Promise<void>;
userInitialize: () => Promise<void>;
};
const useUserInfo = create<UserStoreInfo>((set) => ({
user: null,
setUserInfos: (user) => {
return new Promise<void>(async resolve => {
try {
// 将用户信息保存到本地存储
await AsyncStorage.setItem('user', JSON.stringify(user));
set({ user });
} catch (error) {
console.error('Failed to set user in AsyncStorage:', error);
}
resolve();
});
},
userInitialize: async () => {
return new Promise<void>(async resolve => {
try {
// 从本地存储中获取用户信息
const user = await AsyncStorage.getItem('user') as string;
set({ user: JSON.parse(user) });
} catch (error) {
console.error('Failed to get user from AsyncStorage:', error);
}
resolve();
});
},
}));
export default useUserInfo;
3.2创建登录页面
登录页面我们使用了自己定义的BdButton组件,使用自己封装的组件可以更好的统一UI风格,简化代码。后面我们会讲解如何自定组件。
import React, { useState } from 'react';
import { View, TextInput, StyleSheet } from 'react-native';
import useUserInfo from '../../../src/stores/userInfo';
import BdButton from '../../../src/components/bd-Button';
const LoginScreen = () => {
const [account, setAccount] = useState('');
const [password, setPassword] = useState('');
const { setUserInfos } = useUserInfo();
const handleLoginUser = () => {
const newUser = {
id: Date.now().toString(),
account,
userName:'测试',
token: Date.now().toString(),
};
setUserInfos(newUser);
setAccount('');
setPassword('');
};
return (
<View style={styles.container}>
<View style={styles.loginContainer}>
<TextInput
style={styles.input}
placeholder="账号"
value={account}
onChangeText={setAccount}
/>
<TextInput
style={styles.input}
placeholder="密码"
value={password}
onChangeText={setPassword}
/>
<BdButton
style={styles.button}
text="登录"
onPress={handleLoginUser}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F7F8FA',
},
loginContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginBottom:200,
},
input: {
width: '90%',
height: 40,
borderWidth: 1,
borderColor: '#646566',
marginBottom: 10,
paddingHorizontal: 10,
},
button: {
width: '90%',
},
});
export default LoginScreen;
3.3程序入口判断登录逻辑
App.tsx使我们程序的主入口,这边使用useEffect钩子在app启动的时候初始化用户数据。
StatusBar 是控制应用状态栏的组件,这边设置手机最上面一条状态栏背景是浅灰色,状态栏中的字体样式深色模式。
Navigator 是我们的页面路由。
import React, { useState, useEffect } from 'react';
import { StatusBar } from 'react-native';
import useUserInfo from '../src/stores/userInfo';
import LoginScreen from '../src/views/login/index';
import Navigator from '../src/components/navigator';
import { BdStyleConfig } from '../src/theme/bd-styles';
const App = () => {
const { user, userInitialize } = useUserInfo();
const [isLoad, setIsLoad] = useState(true);
useEffect(() => {
userInitialize().then(()=>{
setIsLoad(false);
});
},[userInitialize]);
return (
<>
<StatusBar backgroundColor={BdStyleConfig.color_lightGrey} barStyle='dark-content' />
{ isLoad ? '' : (user?.token ? <Navigator /> : <LoginScreen />) }
</>
);
};
export default App;
4.构建路由
使用@react-navigation/bottom-tabs构建主程序底部tab页签(首页和设置)
使用@react-navigation/native-stack构建每个tab页面内部路由(首页中有物料上架、仓库收料等子路由页面)
import React from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import MyHomeScreen from '../../src/views/myHome/index';
import SettingScreen from '../../src/views/setting/index';
import MaterialListingScreen from '../../src/views/business//materialListing/index';
import WarehouseReceiptScreen from '../../src/views/business//warehouseReceipt/index';
import MaterialSearchScreen from '../../src/views/business//warehouseReceipt/materialSearch';
import PrintSettingScreen from '../../src/views/business//printSetting/index';
import { BdStyles, BdStyleConfig } from '../../src/theme/bd-styles';
const Tab = createBottomTabNavigator();
function HomeStack() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions = {({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName = '';
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Setting') {
iconName = focused ? 'settings' : 'settings-outline';
};
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: BdStyleConfig.color_primary,
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" options={{ headerShown:false, title:'首页' }} component={MyHomeStack} />
<Tab.Screen name="Setting" options={{ headerShown:false, title:'设置' }} component={SettingScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
const Stack = createNativeStackNavigator();
function MyHomeStack() {
return (
<Stack.Navigator screenOptions={{
headerStyle: {
backgroundColor: BdStyleConfig.color_lightGrey,
},
headerTintColor: '#000',
headerTitleStyle: {
color:'#000',
fontSize:18,
},
headerTitleAlign: 'center', // 将标题居中
animation: 'slide_from_right', // 通过animation属性设置动画效果
}}>
<Stack.Screen name="MyHome" options={{ title:'首页' }} component={MyHomeScreen} />
<Stack.Screen name="MaterialListing" options={{ title:'物料上架' }} component={MaterialListingScreen} />
<Stack.Screen name="WarehouseReceipt" options={{ title:'仓库收料' }} component={WarehouseReceiptScreen} />
<Stack.Screen name="MaterialSearch" options={{ title:'收料查询' }} component={MaterialSearchScreen} />
<Stack.Screen name="PrintSetting" options={{ title:'打印设置' }} component={PrintSettingScreen} />
</Stack.Navigator>
);
}
export default function Navigator() {
return (
<HomeStack />
);
}
5.创建首页
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import { BdStyles, BdStyleConfig } from '../../../src/theme/bd-styles';
const MyHomeScreen = ({ navigation }: { navigation: NavigationProp<ParamListBase> }) => {
const handleMenuPress = (menu:string) => {
navigation.navigate(menu);
};
return (
<View style={BdStyles.container}>
<View style={styles.row}>
<TouchableOpacity
style={[styles.menuItem, styles.menuBorder]}
onPress={() => handleMenuPress('WarehouseReceipt')}
>
<Ionicons name="archive-outline" size={30} color="#646566" />
<Text style={styles.menuTitle}>仓库收料</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.menuItem, styles.menuBorder]}
onPress={() => handleMenuPress('MaterialListing')}
>
<Ionicons name="arrow-up-circle-outline" size={30} color="#646566" />
<Text style={styles.menuTitle}>物料上架</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.menuItem, styles.menuBorder]}
>
<Ionicons name="library-outline" size={30} color="#646566" />
<Text style={styles.menuTitle}>工单拣料</Text>
</TouchableOpacity>
</View>
/**其他菜单**/
</View>
);
};
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
},
menuItem: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingBottom:15,
paddingTop:15,
backgroundColor:BdStyleConfig.color_white,
},
menuBorder: {
borderColor: '#ebedf0',
borderWidth: 0.5,
},
menuTitle: {
color: '#646566',
marginTop: 5,
},
});
export default MyHomeScreen;
6.创建设置
这个里我们使用了自定义单元格组件BdCell
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import useUserInfo from '../../../src/stores/userInfo';
import Ionicons from 'react-native-vector-icons/Ionicons';
import BdCell from '../../components/bd-cell';
const SettingScreen = () => {
const { user, setUserInfos } = useUserInfo();
const handleLoginOut = () => {
Alert.alert(
"消息",
"是否确定退出登录!",
[
{
text: "取消",
onPress: () => console.log("Cancel Pressed"),
style: "cancel"
},
{ text: "确定", onPress: () => setUserInfos(null) }
]
);
};
return (
<View style={styles.container}>
<TouchableOpacity style={styles.userInfoContainer}>
<Ionicons name="person-circle" size={80} color="#c1c1c1" />
<View style={{ alignItems: 'center', justifyContent: 'center',}}>
<Text style={{ fontSize:25, color:'#000', fontWeight: 'bold', marginBottom:5 }}>{ user?.userName }</Text>
<Text style={{ fontSize:13, color:'#c1c1c1',paddingLeft:7 }}>欢迎您!</Text>
</View>
</TouchableOpacity>
<View style={styles.listContainer}>
<BdCell title="系统设置" style={styles.cellStyle} />
<BdCell title="退出" onPress={handleLoginOut} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor:'#F7F8FA',
},
userInfoContainer: {
flexDirection: 'row',
backgroundColor:'#fff',
padding:10,
},
listContainer: {
paddingTop:10,
},
cellStyle:{
borderBottomColor:'#F7F8FA',
borderBottomWidth:1,
}
});
export default SettingScreen;
7.自定义组件开发
7.1按钮组件BdButton
BdButton自定义按钮组件我们首先用定义了ButtonProps类型为组件包含多少属性,style属性让我们可以改写组件样式,style?中的?是可选连操作符,定义在这边表示这个参数可以为空不传。
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, ViewStyle } from 'react-native';
import { BdStyles, BdStyleConfig } from '../../src/theme/bd-styles';
interface ButtonProps {
style?: ViewStyle;
onPress?: () => void;
text: string;
}
const BdButton: React.FC<ButtonProps> = ({ style, onPress, text }) => {
return (
<TouchableOpacity style={[styles.button, style]} onPress={onPress}>
<Text style={styles.text}>{text}</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
backgroundColor:BdStyleConfig.color_primary,
padding: 10,
borderRadius: 5,
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: 'white',
fontWeight: 'bold',
},
});
export default BdButton;
7.2表格组件BdTable
表格组件的简单使用,我们只要给组件传data和columns2个属性就可以生成我们的表格。
const tableData = [
{ id: '1001', count: 5, total: 15, remainCount: 3 },
{ id: '1002', count: 7, total: 17, remainCount: 2 },
{ id: '1003', count: 3, total: 12, remainCount: 1 },
// 其他数据...
]
const tableColumns:BdTablePropsColumn[] = [
{ field: 'id', title: '料号', align: 'center', },
{ field: 'count', title: '件数', align: 'center', },
{ field: 'total', title: '总数量', align: 'center', },
{ field: 'remainCount', title: '剩余数量', align: 'center', },
]
const MaterialSearchScreen = () => {
return (
<View style={BdStyles.container}>
<BdTable
data={tableData}
columns={tableColumns}
/>
</View>
);
};
表格组件代码
import React from 'react';
import { View, Text, VirtualizedList, StyleSheet, TouchableOpacity } from 'react-native';
export interface BdTableProps {
data: any[];
columns: BdTablePropsColumn[];
onClickRow?: ((item:any) => void) | undefined;
hideHeader?: boolean;
}
export interface BdTablePropsColumn {
field: string,
title: string,
align: string,
}
const BdTable: React.FC<BdTableProps> = ({ columns, data, onClickRow, hideHeader }) => {
const renderItem = (item:any) => (
<TouchableOpacity key={new Date().getTime()} style={styles.row} onPress={() =>{ onClickRow?onClickRow(item.item):'' }}>
{columns.map((column) => (
<Text style={styles.cell}>{item.item[column.field]}</Text>
))}
</TouchableOpacity>
);
const tableHeader = () => (
<View style={styles.row}>
{columns.map((column) => (
<Text key={column.field} style={styles.headerCell}>{column.title}</Text>
))}
</View>
);
const keyExtractor = (item:any) => item.id?item.id.toString():new Date().getTime();
const getItemCount = () => data.length;
const getItem = (data: [], index: number) => data[index];
return (
<VirtualizedList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
ListHeaderComponent={ !hideHeader?tableHeader:undefined }
getItemCount={getItemCount}
getItem={getItem}
/>
);
};
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor:'#fff',
},
cell: {
flex: 1,
textAlign: 'center',
borderColor: '#e4eaec',
borderWidth:1,
padding:4,
},
headerCell: {
flex: 1,
textAlign: 'center',
borderColor: '#e4eaec',
borderWidth:1,
fontWeight:'bold',
padding:4,
},
});
export default BdTable;