移动应用开发教程 - React Native
React Native 移动选项适用于团队版或更高级别的许可证。因此,如果您没有商业许可证,建议通过下载示例应用程序的源代码来按照本文进行学习,具体方法将在下一章节中描述。
关于本教程
您必须拥有 ABP 团队版或更高级别的许可证 才能创建移动应用程序。
- 本教程假设您已完成 Web 应用程序开发教程,并使用 React Native 作为移动选项构建了一个名为
Acme.BookStore的基于 ABP 的应用程序。因此,如果您尚未完成 Web 应用程序开发教程,您需要先完成它,或从下方下载源代码并按照本教程操作。 - 在本教程中,我们将仅关注
Acme.BookStore应用程序的用户界面方面,并实现增删改查操作。 - 在开始之前,请确保您的计算机上已准备好 React Native 开发环境。
下载源代码
您可以使用以下链接下载本文中描述的应用程序的源代码:
如果您在 Windows 上遇到“文件名过长”或“解压”错误,请参阅 本指南。
书籍列表页面
React Native 应用程序没有动态代理生成功能,因此我们需要在 ./src/api 文件夹下手动创建 BookAPI 代理。
//./src/api/BookAPI.ts
import api from './API';
export const getList = () => api.get('/api/app/book').then(({ data }) => data);
export const get = id => api.get(`/api/app/book/${id}`).then(({ data }) => data);
export const create = input => api.post('/api/app/book', input).then(({ data }) => data);
export const update = (input, id) => api.put(`/api/app/book/${id}`, input).then(({ data }) => data);
export const remove = id => api.delete(`/api/app/book/${id}`).then(({ data }) => data);
将 书店 菜单项添加到导航
要创建菜单项,请导航到 ./src/navigators/DrawerNavigator.tsx 文件,并将 BookStoreStack 添加到 Drawer.Navigator 组件中。
//其他导入..
import BookStoreStackNavigator from './BookStoreNavigator';
const Drawer = createDrawerNavigator();
export default function DrawerNavigator() {
return (
<Drawer.Navigator
initialRouteName="Home"
drawerContent={DrawerContent}
defaultStatus="closed"
>
{/* 已添加的屏幕 */}
<Drawer.Screen
name="BookStoreStack"
component={BookStoreStackNavigator}
options={{ header: () => null }}
/>
{/* 已添加的屏幕 */}
</Drawer.Navigator>
);
}
在 ./src/navigators/BookStoreNavigator.tsx 中创建 BookStoreStackNavigator,此导航器将用于 BookStore 菜单项。
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button } from 'react-native-paper';
import i18n from 'i18n-js';
import { BookStoreScreen, CreateUpdateAuthorScreen, CreateUpdateBookScreen } from '../screens';
import { HamburgerIcon } from '../components';
import { useThemeColors } from '../hooks';
const Stack = createNativeStackNavigator();
export default function BookStoreStackNavigator() {
const { background, onBackground } = useThemeColors();
return (
<Stack.Navigator initialRouteName="BookStore">
<Stack.Screen
name="BookStore"
component={BookStoreScreen}
options={({ navigation }) => ({
title: i18n.t('BookStore::Menu:BookStore'),
headerLeft: () => <HamburgerIcon navigation={navigation} />,
headerStyle: { backgroundColor: background },
headerTintColor: onBackground,
headerShadowVisible: false,
})}
/>
<Stack.Screen
name="CreateUpdateBook"
component={CreateUpdateBookScreen}
options={({ route, navigation }) => ({
title: i18n.t(route.params?.bookId ? 'BookStore::Edit' : 'BookStore::NewBook'),
headerRight: () => (
<Button mode="text" onPress={() => navigation.navigate('BookStore')}>
{i18n.t('AbpUi::Cancel')}
</Button>
),
headerStyle: { backgroundColor: background },
headerTintColor: onBackground,
headerShadowVisible: false,
})}
/>
</Stack.Navigator>
);
}
- BookStoreScreen 将用于存放
books和authors页面
在 ./src/components/DrawerContent/DrawerContent.tsx 文件的 screens 对象中添加 BookStoreStack。DrawerContent 组件将用于渲染菜单项。
// 导入..
const screens = {
HomeStack: { label: "::Menu:Home", iconName: "home" },
DashboardStack: {
label: "::Menu:Dashboard",
requiredPolicy: "BookStore.Dashboard",
iconName: "chart-areaspline",
},
UsersStack: {
label: "AbpIdentity::Users",
iconName: "account-supervisor",
requiredPolicy: "AbpIdentity.Users",
},
// 添加此属性
BookStoreStack: {
label: "BookStore::Menu:BookStore",
iconName: "book",
},
// 添加此属性
TenantsStack: {
label: "Saas::Tenants",
iconName: "book-outline",
requiredPolicy: "Saas.Tenants",
},
SettingsStack: {
label: "AbpSettingManagement::Settings",
iconName: "cog",
navigation: null,
},
};
// 其他代码..
创建书籍列表页面
在创建书籍列表页面之前,我们需要在 ./src/screens/BookStore 文件夹下创建 BookStoreScreen.tsx 文件。此文件将用于存放 books 和 authors 页面。
import { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import i18n from 'i18n-js';
import { BottomNavigation } from 'react-native-paper';
import { BooksScreen } from '../../screens';
import { useThemeColors } from '../../hooks';
const BooksRoute = nav => <BooksScreen navigation={nav} />;
function BookStoreScreen({ navigation }) {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{
key: "books",
title: i18n.t("BookStore::Menu:Books"),
focusedIcon: "book",
unfocusedIcon: "book-outline",
},
]);
const renderScene = BottomNavigation.SceneMap({
books: BooksRoute,
});
return (
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
/>
);
}
export default BookStoreScreen;
在 ./src/screens/BookStore/Books 文件夹下创建 BooksScreen.tsx 文件。
import { useSelector } from "react-redux";
import { View } from "react-native";
import { List } from "react-native-paper";
import { getBooks } from "../../api/BookAPI";
import i18n from "i18n-js";
import DataList from "../../components/DataList/DataList";
import { createAppConfigSelector } from "../../store/selectors/AppSelectors";
import { useThemeColors } from '../../../hooks';
function BooksScreen({ navigation }) {
const { background, primary } = useThemeColors();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
return (
<View style={{ flex: 1, backgroundColor: background }}>
{currentUser?.isAuthenticated && (
<DataList
navigation={navigation}
fetchFn={getBooks}
render={({ item }) => (
<List.Item
key={item.id}
title={item.name}
description={i18n.t("BookStore::Enum:BookType." + item.type)}
/>
)}
/>
)}
</View>
);
}
export default BooksScreen;
getBooks函数用于从服务器获取书籍。i18nAPI 用于本地化给定的键。它使用来自application-localization端点的传入资源。DataList组件接收我们将传递给 API 请求函数的fetchFn属性,它用于获取数据并维护懒加载等逻辑。
创建新书
为日期功能添加 @react-native-community/datetimepicker 包
yarn expo install @react-native-community/datetimepicker
// 或
npx expo install @react-native-community/datetimepicker
将 CreateUpdateBook 屏幕添加到 BookStoreNavigator
与 BookStoreScreen 类似,我们需要将 CreateUpdateBookScreen 添加到 ./src/navigators/BookStoreNavigator.tsx 文件中。
//其他代码
import { Button } from "react-native-paper"; // 添加此行
import { CreateUpdateBookScreen } from '../screens'; // 添加此行
//其他代码
export default function BookStoreStackNavigator() {
return (
<Stack.Navigator initialRouteName="BookStore">
{/* 其他屏幕 */}
{/* 已添加此屏幕 */}
<Stack.Screen
name="CreateUpdateBook"
component={CreateUpdateBookScreen}
options={({ route, navigation }) => ({
title: i18n.t(
route.params?.bookId ? "BookStore::Edit" : "BookStore::NewBook"
),
headerRight: () => (
<Button
mode="text"
onPress={() => navigation.navigate("BookStore")}
>
{i18n.t("AbpUi::Cancel")}
</Button>
),
headerStyle: { backgroundColor: background },
headerTintColor: onBackground,
headerShadowVisible: false,
})}
/>
</Stack.Navigator>
);
}
为了导航到 CreateUpdateBookScreen,我们需要在 BooksScreen.tsx 文件中添加 CreateUpdateBook 按钮。
//其他导入..
import {
// 其他导入..,
StyleSheet,
} from "react-native";
import {
// 其他导入..,
AnimatedFAB,
} from "react-native-paper";
function BooksScreen({ navigation }) {
//其他代码..
return (
<View style={{ flex: 1, backgroundColor: background }}>
{/* 其他代码..*/}
{/* 包含的代码 */}
{currentUser?.isAuthenticated && (
<AnimatedFAB
icon={"plus"}
label={i18n.t("BookStore::NewBook")}
color="white"
extended={false}
onPress={() => navigation.navigate("CreateUpdateBook")}
visible={true}
animateFrom={"right"}
iconMode={"static"}
style={[styles.fabStyle, { backgroundColor: primary }]}
/>
)}
{/* 包含的代码 */}
</View>
);
}
// 添加的行
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: "absolute",
},
});
// 添加的行
export default BooksScreen;
添加 CreateUpdateBook 按钮后,我们需要在 ./src/screens/BookStore/Books/CreateUpdateBook 文件夹下添加 CreateUpdateBookScreen.tsx 文件。
import PropTypes from "prop-types";
import { create } from "../../../../api/BookAPI";
import LoadingActions from "../../../../store/actions/LoadingActions";
import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors";
import { connectToRedux } from "../../../../utils/ReduxConnect";
import CreateUpdateBookForm from "./CreateUpdateBookForm";
function CreateUpdateBookScreen({ navigation, startLoading, clearLoading }) {
const submit = (data) => {
startLoading({ key: "save" });
create(data)
.then(() => navigation.goBack())
.finally(() => clearLoading());
};
return <CreateUpdateBookForm submit={submit} />;
}
CreateUpdateBookScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
clearLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateBookScreen,
stateProps: (state) => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
clearLoading: LoadingActions.clear,
},
});
- 在此页面中,我们将存储逻辑、发送 POST/PUT 请求、获取选定的书籍数据等。
- 此页面将包装
CreateUpdateBookFrom组件,并传递 submit 函数及其他属性。
在 ./src/screens/BookStore/Books/CreateUpdateBook 文件夹下创建一个 CreateUpdateBookForm.tsx 文件,并向其中添加以下代码。
import * as Yup from 'yup';
import { useRef, useState } from 'react';
import { Platform, KeyboardAvoidingView, StyleSheet, View, ScrollView } from 'react-native';
import { useFormik } from 'formik';
import i18n from 'i18n-js';
import PropTypes from 'prop-types';
import { TextInput, Portal, Modal, Text, Divider, Button } from 'react-native-paper';
import DateTimePicker from '@react-native-community/datetimepicker';
import { FormButtons, ValidationMessage, AbpSelect } from '../../../../components';
import { useThemeColors } from '../../../../hooks';
const validations = {
name: Yup.string().required("AbpValidation::ThisFieldIsRequired."),
price: Yup.number().required("AbpValidation::ThisFieldIsRequired."),
type: Yup.string().nullable().required("AbpValidation::ThisFieldIsRequired."),
publishDate: Yup.string()
.nullable()
.required("AbpValidation::ThisFieldIsRequired."),
};
const props = {
underlineStyle: { backgroundColor: "transparent" },
underlineColor: "#333333bf",
};
function CreateUpdateBookForm({ submit }) {
const { primaryContainer, background, onBackground } = useThemeColors();
const [bookTypeVisible, setBookTypeVisible] = useState(false);
const [publishDateVisible, setPublishDateVisible] = useState(false);
const nameRef = useRef(null);
const priceRef = useRef(null);
const typeRef = useRef(null);
const publishDateRef = useRef(null);
const inputStyle = {
...styles.input,
backgroundColor: primaryContainer,
};
const bookTypes = new Array(8).fill(0).map((_, i) => ({
id: i + 1,
displayName: i18n.t(`BookStore::Enum:BookType.${i + 1}`),
}));
const onSubmit = (values) => {
if (!bookForm.isValid) {
return;
}
submit({ ...values });
};
const bookForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
name: "",
price: "",
type: "",
publishDate: null,
},
onSubmit,
});
const isInvalidControl = (controlName = null) => {
if (!controlName) {
return;
}
return (
((!!bookForm.touched[controlName] && bookForm.submitCount > 0) ||
bookForm.submitCount > 0) &&
!!bookForm.errors[controlName]
);
};
const onChange = (event, selectedDate) => {
if (!selectedDate) {
return;
}
setPublishDateVisible(false);
if (event && event.type !== "dismissed") {
bookForm.setFieldValue("publishDate", selectedDate, true);
}
};
return (
<View style={{ flex: 1, backgroundColor: background }}>
<AbpSelect
key="typeSelect"
title={i18n.t("BookStore::Type")}
visible={bookTypeVisible}
items={bookTypes}
hasDefualtItem={true}
hideModalFn={() => setBookTypeVisible(false)}
selectedItem={bookForm.values.type}
setSelectedItem={(id) => {
bookForm.setFieldValue("type", id, true);
bookForm.setFieldValue(
"typeDisplayName",
bookTypes.find((f) => f.id === id)?.displayName || null,
false
);
}}
/>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<ScrollView keyboardShouldPersistTaps="handled">
<View style={styles.inputContainer}>
<TextInput
mode="flat"
ref={nameRef}
error={isInvalidControl('name')}
onSubmitEditing={() => priceRef.current.focus()}
returnKeyType="next"
onChangeText={bookForm.handleChange('name')}
onBlur={bookForm.handleBlur('name')}
value={bookForm.values.name}
autoCapitalize="none"
label={i18n.t('BookStore::Name')}
style={inputStyle}
{...props}
/>
{isInvalidControl('name') && (
<ValidationMessage>{bookForm.errors.name as string}</ValidationMessage>
)}
</View>
<View style={styles.inputContainer}>
<TextInput
mode="flat"
ref={priceRef}
error={isInvalidControl('price')}
onSubmitEditing={() => typeRef.current.focus()}
returnKeyType="next"
onChangeText={bookForm.handleChange('price')}
onBlur={bookForm.handleBlur('price')}
value={bookForm.values.price}
autoCapitalize="none"
label={i18n.t('BookStore::Price')}
style={inputStyle}
{...props}
/>
{isInvalidControl('price') && (
<ValidationMessage>{bookForm.errors.price as string}</ValidationMessage>
)}
</View>
<View style={styles.inputContainer}>
<TextInput
ref={typeRef}
error={isInvalidControl('type')}
label={i18n.t('BookStore::Type')}
right={<TextInput.Icon onPress={() => setBookTypeVisible(true)} icon="menu-down" />}
style={inputStyle}
editable={false}
value={bookForm.values.typeDisplayName}
{...props}
/>
{isInvalidControl('type') && (
<ValidationMessage>{bookForm.errors.type as string}</ValidationMessage>
)}
</View>
<View style={styles.inputContainer}>
<TextInput
ref={publishDateRef}
error={isInvalidControl('publishDate')}
label={i18n.t('BookStore::PublishDate')}
right={
<TextInput.Icon
onPress={() => setPublishDateVisible(true)}
icon="calendar"
iconColor={bookForm.values.publishDate ? '#4CAF50' : '#666'}
/>
}
style={inputStyle}
editable={false}
value={formatDate(bookForm.values.publishDate)}
placeholder="Select publish date"
{...props}
/>
{isInvalidControl('publishDate') && (
<ValidationMessage>{bookForm.errors.publishDate as string}</ValidationMessage>
)}
</View>
<Portal>
<Modal
visible={publishDateVisible}
onDismiss={handleDateCancel}
contentContainerStyle={[styles.dateModal, { backgroundColor: background }]}>
<Text variant="titleLarge" style={styles.modalTitle}>
{i18n.t('BookStore::PublishDate')}
</Text>
<Divider style={styles.divider} />
<DateTimePicker
testID="publishDatePicker"
value={bookForm.values.publishDate || new Date()}
mode="date"
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
onChange={onChange}
maximumDate={new Date()}
textColor={onBackground}
/>
<View style={styles.modalButtons}>
<Button onPress={handleDateCancel} mode="text">
{i18n.t('AbpUi::Cancel')}
</Button>
<Button onPress={handleDateConfirm} mode="contained">
{i18n.t('AbpUi::Ok')}
</Button>
</View>
</Modal>
</Portal>
<FormButtons style={styles.button} submit={bookForm.handleSubmit} />
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
inputContainer: {
margin: 8,
marginLeft: 16,
marginRight: 16,
},
input: {
borderRadius: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
button: {
marginLeft: 16,
marginRight: 16,
},
dateModal: {
padding: 20,
margin: 20,
borderRadius: 12,
elevation: 5,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
modalTitle: {
textAlign: 'center',
marginBottom: 16,
fontWeight: '600',
},
divider: {
marginBottom: 16,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 20,
paddingHorizontal: 8,
},
});
CreateUpdateBookForm.propTypes = {
book: PropTypes.object,
authors: PropTypes.array.isRequired,
submit: PropTypes.func.isRequired,
};
export default CreateUpdateBookForm;
formik将管理表单状态、验证和值变更。Yup允许构建验证模式。AbpSelect组件用于选择书籍类型。submit方法将表单值传递给CreateUpdateBookScreen组件。
更新书籍
我们需要导航参数来获取 bookId,然后在创建和更新操作后再次导航。这就是为什么我们将导航参数传递给 BooksScreen 组件。
//导入..
// 添加导航参数
const BooksRoute = (nav) => <BooksScreen navigation={nav} />;
function BookStoreScreen({ navigation }) {
//其他代码..
const renderScene = BottomNavigation.SceneMap({
books: () => BooksRoute(navigation), // 使用这种方式
});
//其他代码..
}
export default BookStoreScreen;
替换 BookScreen.tsx 文件中 ./src/screens/BookStore/Books 文件夹下的代码。
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Alert, View, StyleSheet } from 'react-native';
import { List, IconButton, AnimatedFAB } from 'react-native-paper';
import { useActionSheet } from '@expo/react-native-action-sheet';
import i18n from 'i18n-js';
import { getList, remove } from '../../../api/BookAPI';
import { DataList } from '../../../components';
import { createAppConfigSelector } from '../../../store/selectors/AppSelectors';
import { useThemeColors } from '../../../hooks';
function BooksScreen({ navigation }) {
const { background, primary } = useThemeColors();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
const [refresh, setRefresh] = useState(null);
const { showActionSheetWithOptions } = useActionSheet();
const openContextMenu = (item: { id: string }) => {
const options = [];
if (policies['BookStore.Books.Delete']) {
options.push(i18n.t('AbpUi::Delete'));
}
if (policies['BookStore.Books.Edit']) {
options.push(i18n.t('AbpUi::Edit'));
}
options.push(i18n.t('AbpUi::Cancel'));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')),
},
index => {
switch (options[index]) {
case i18n.t('AbpUi::Edit'):
edit(item);
break;
case i18n.t('AbpUi::Delete'):
removeOnClick(item);
break;
}
},
);
};
const removeOnClick = (item: { id: string }) => {
Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [
{
text: i18n.t('AbpUi::Cancel'),
style: 'cancel',
},
{
style: 'default',
text: i18n.t('AbpUi::Ok'),
onPress: () => {
remove(item.id).then(() => {
setRefresh((refresh ?? 0) + 1);
});
},
},
]);
};
const edit = (item: { id: string }) => {
navigation.navigate('CreateUpdateBook', { bookId: item.id });
};
return (
<View style={{ flex: 1, backgroundColor: background }}>
{currentUser?.isAuthenticated && (
<DataList
navigation={navigation}
fetchFn={getList}
trigger={refresh}
render={({ item }) => (
<List.Item
key={item.id}
title={item.name}
description={`${item.authorName} | ${i18n.t(
'BookStore::Enum:BookType.' + item.type,
)}`}
right={props => (
<IconButton
{...props}
icon="dots-vertical"
rippleColor={'#ccc'}
size={20}
onPress={() => openContextMenu(item)}
/>
)}
/>
)}
/>
)}
{currentUser?.isAuthenticated && !!policies['BookStore.Books.Create'] && (
<AnimatedFAB
icon={'plus'}
label={i18n.t('BookStore::NewBook')}
color="white"
extended={false}
onPress={() => navigation.navigate('CreateUpdateBook')}
visible={true}
animateFrom={'right'}
iconMode={'static'}
style={[styles.fabStyle, { backgroundColor: primary }]}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: 'absolute',
},
});
export default BooksScreen;
替换 CreateUpdateBookScreen.tsx 文件中 ./src/screens/BookStore/Books/CreateUpdateBook/ 文件夹下的代码。
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { getAuthorLookup, get, create, update } from '../../../../api/BookAPI';
import LoadingActions from '../../../../store/actions/LoadingActions';
import { createLoadingSelector } from '../../../../store/selectors/LoadingSelectors';
import { connectToRedux } from '../../../../utils/ReduxConnect';
import CreateUpdateBookForm from './CreateUpdateBookForm';
function CreateUpdateBookScreen({ navigation, route, startLoading, clearLoading }) {
const { bookId } = route.params || {};
const [book, setBook] = useState(null);
const submit = (data: any) => {
startLoading({ key: 'save' });
(data.id ? update(data, data.id) : create(data))
.then(() => navigation.goBack())
.finally(() => clearLoading());
};
useEffect(() => {
if (bookId) {
startLoading({ key: 'fetchBookDetail' });
get(bookId)
.then((response: any) => setBook(response))
.finally(() => clearLoading());
}
}, [bookId]);
return <CreateUpdateBookForm submit={submit} book={book} />;
}
CreateUpdateBookScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
clearLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateBookScreen,
stateProps: state => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
clearLoading: LoadingActions.clear,
},
});
get方法用于从服务器获取书籍详情。update方法用于更新服务器上的书籍。route参数将用于从导航中获取 bookId。
将 CreateUpdateBookForm.tsx 文件替换为下面的代码。我们将使用此文件进行创建和更新操作。
//导入..
//验证模式
//props
function CreateUpdateBookForm({
submit,
book = null, // 添加带有默认值的 book 参数
}) {
//其他代码..
const bookForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
// 更新初始值
...book,
name: book?.name || "",
price: book?.price.toString() || "",
type: book?.type || "",
typeDisplayName:
book?.type && i18n.t("BookStore::Enum:BookType." + book.type),
publishDate: (book?.publishDate && new Date(book?.publishDate)) || null,
// 更新初始值
},
onSubmit,
});
//其他代码..
}
//其他代码..
book是一个可空的属性。如果 book 参数为 null,我们将创建一个新书。
删除书籍
替换 BooksScreen.tsx 文件中 ./src/screens/BookStore/Books 文件夹下的代码。
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Alert, View, StyleSheet } from 'react-native';
import { List, IconButton, AnimatedFAB } from 'react-native-paper';
import { useActionSheet } from '@expo/react-native-action-sheet';
import i18n from 'i18n-js';
import { getList, remove } from '../../../api/BookAPI';
import { DataList } from '../../../components';
import { createAppConfigSelector } from '../../../store/selectors/AppSelectors';
import { useThemeColors } from '../../../hooks';
function BooksScreen({ navigation }) {
const { background, primary } = useThemeColors();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
const [refresh, setRefresh] = useState(null);
const { showActionSheetWithOptions } = useActionSheet();
const openContextMenu = (item: { id: string }) => {
const options = [];
if (policies['BookStore.Books.Delete']) {
options.push(i18n.t('AbpUi::Delete'));
}
if (policies['BookStore.Books.Edit']) {
options.push(i18n.t('AbpUi::Edit'));
}
options.push(i18n.t('AbpUi::Cancel'));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')),
},
index => {
switch (options[index]) {
case i18n.t('AbpUi::Edit'):
edit(item);
break;
case i18n.t('AbpUi::Delete'):
removeOnClick(item);
break;
}
},
);
};
const removeOnClick = (item: { id: string }) => {
Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [
{
text: i18n.t('AbpUi::Cancel'),
style: 'cancel',
},
{
style: 'default',
text: i18n.t('AbpUi::Ok'),
onPress: () => {
remove(item.id).then(() => {
setRefresh((refresh ?? 0) + 1);
});
},
},
]);
};
const edit = (item: { id: string }) => {
navigation.navigate('CreateUpdateBook', { bookId: item.id });
};
return (
<View style={{ flex: 1, backgroundColor: background }}>
{currentUser?.isAuthenticated && (
<DataList
navigation={navigation}
fetchFn={getList}
trigger={refresh}
render={({ item }) => (
<List.Item
key={item.id}
title={item.name}
description={`${item.authorName} | ${i18n.t(
'BookStore::Enum:BookType.' + item.type,
)}`}
right={props => (
<IconButton
{...props}
icon="dots-vertical"
rippleColor={'#ccc'}
size={20}
onPress={() => openContextMenu(item)}
/>
)}
/>
)}
/>
)}
{currentUser?.isAuthenticated && !!policies['BookStore.Books.Create'] && (
<AnimatedFAB
icon={'plus'}
label={i18n.t('BookStore::NewBook')}
color="white"
extended={false}
onPress={() => navigation.navigate('CreateUpdateBook')}
visible={true}
animateFrom={'right'}
iconMode={'static'}
style={[styles.fabStyle, { backgroundColor: primary }]}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: 'absolute',
},
});
export default BooksScreen;
删除选项已添加到上下文菜单列表中removeOnClick方法将处理删除过程。在删除操作之前会显示一个警告框。
授权
在标签页中隐藏 Books 项
从 appConfig 存储中添加 grantedPolicies 到 policies 变量
//其他导入..
import { useSelector } from "react-redux";
function BookStoreScreen({ navigation }) {
const [index, setIndex] = React.useState(0);
const [routes, setRoutes] = React.useState([]);
const currentUser = useSelector((state) => state.app.appConfig.currentUser);
const policies = useSelector(
(state) => state.app.appConfig.auth.grantedPolicies
);
const renderScene = BottomNavigation.SceneMap({
books: () => BooksRoute(navigation),
});
React.useEffect(() => {
if (!currentUser?.isAuthenticated || !policies) {
setRoutes([]);
return;
}
let _routes = [];
if (!!policies["BookStore.Books"]) {
_routes.push({
key: "books",
title: i18n.t("BookStore::Menu:Books"),
focusedIcon: "book",
unfocusedIcon: "book-outline",
});
}
setRoutes([..._routes]);
}, [Object.keys(policies)?.filter((f) => f.startsWith("BookStore")).length]);
return (
routes?.length > 0 && (
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
/>
)
);
}
export default BookStoreScreen;
- 在
useEffect函数中,我们将检查currentUser和policies变量。 - useEffect 的条件将是
BookStore权限组的策略。 - 如果用户拥有
BookStore.Books权限,则Books标签页将被显示
隐藏新建书籍按钮
新建书籍 按钮在 BooksScreen 中作为 + 图标按钮放置。要切换此按钮的可见性,我们需要像 BookStoreScreen 组件一样,将 policies 变量添加到 BooksScreen 组件中。打开 ./src/screens/BookStore/Books 文件夹中的 BooksScreen.tsx 文件,并包含以下代码。
//导入..
function BooksScreen({ navigation }) {
const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
//其他代码..
return (
{/*其他代码..*/}
{currentUser?.isAuthenticated &&
!!policies['BookStore.Books.Create'] && // 添加此行
(
<AnimatedFAB
icon={'plus'}
label={i18n.t('BookStore::NewBook')}
color="white"
extended={false}
onPress={() => navigation.navigate('CreateUpdateBook')}
visible={true}
animateFrom={'right'}
iconMode={'static'}
style={[styles.fabStyle, { backgroundColor: primary }]}
/>
)
}
)
}
- 现在,如果用户拥有
BookStore.Books.Create权限,则+图标按钮将被显示。
隐藏编辑和删除操作
在 ./src/screens/BookStore/Books/BooksScreen.tsx 文件中按如下更新您的代码。我们将为 编辑 和 删除 操作检查 policies 变量。
function BooksScreen() {
//...
const openContextMenu = (item) => {
const options = [];
if (policies["BookStore.Books.Delete"]) {
options.push(i18n.t("AbpUi::Delete"));
}
if (policies["BookStore.Books.Update"]) {
options.push(i18n.t("AbpUi::Edit"));
}
options.push(i18n.t("AbpUi::Cancel"));
};
//...
}
作者
创建 API 代理
//./src/api/AuthorAPI.ts
import api from './API';
export const getList = () => api.get('/api/app/author').then(({ data }) => data);
export const get = id => api.get(`/api/app/author/${id}`).then(({ data }) => data);
export const create = input => api.post('/api/app/author', input).then(({ data }) => data);
export const update = (input, id) => api.put(`/api/app/author/${id}`, input).then(({ data }) => data);
export const remove = id => api.delete(`/api/app/author/${id}`).then(({ data }) => data);
作者列表页面
将 Authors 标签页添加到 BookStoreScreen
打开 ./src/screens/BookStore/BookStoreScreen.tsx 文件并用下面的代码更新它。
//其他导入
import AuthorsScreen from "./Authors/AuthorsScreen";
//其他路由..
const AuthorsRoute = (nav) => <AuthorsScreen navigation={nav} />;
function BookStoreScreen({ navigation }) {
//其他代码..
const renderScene = BottomNavigation.SceneMap({
books: () => BooksRoute(navigation),
authors: () => AuthorsRoute(navigation), // 添加此行
});
// 添加此部分
if (!!policies["BookStore.Authors"]) {
_routes.push({
key: "authors",
title: i18n.t("BookStore::Menu:Authors"),
focusedIcon: "account-supervisor",
unfocusedIcon: "account-supervisor-outline",
});
}
// 添加此部分
}
export default BookStoreScreen;
在 ./src/screens/BookStore/Authors 文件夹下创建一个 AuthorsScreen.tsx 文件,并向其中添加以下代码。
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Alert, View, StyleSheet } from 'react-native';
import { List, IconButton, AnimatedFAB } from 'react-native-paper';
import { useActionSheet } from '@expo/react-native-action-sheet';
import i18n from 'i18n-js';
import { getList, remove } from '../../../api/AuthorAPI';
import { DataList } from '../../../components';
import { createAppConfigSelector } from '../../../store/selectors/AppSelectors';
import { useThemeColors } from '../../../hooks';
function AuthorsScreen({ navigation }) {
const { background, primary } = useThemeColors();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
const [refresh, setRefresh] = useState(null);
const { showActionSheetWithOptions } = useActionSheet();
const openContextMenu = (item: { id: string }) => {
const options = [];
if (policies['BookStore.Authors.Delete']) {
options.push(i18n.t('AbpUi::Delete'));
}
if (policies['BookStore.Authors.Edit']) {
options.push(i18n.t('AbpUi::Edit'));
}
options.push(i18n.t('AbpUi::Cancel'));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
destructiveButtonIndex: options.indexOf(i18n.t('AbpUi::Delete')),
},
(index: number) => {
switch (options[index]) {
case i18n.t('AbpUi::Edit'):
edit(item);
break;
case i18n.t('AbpUi::Delete'):
removeOnClick(item);
break;
}
},
);
};
const removeOnClick = ({ id }: { id: string }) => {
Alert.alert('Warning', i18n.t('BookStore::AreYouSureToDelete'), [
{
text: i18n.t('AbpUi::Cancel'),
style: 'cancel',
},
{
style: 'default',
text: i18n.t('AbpUi::Ok'),
onPress: () => {
remove(id).then(() => {
setRefresh((refresh ?? 0) + 1);
});
},
},
]);
};
const edit = ({ id }: { id: string }) => {
navigation.navigate('CreateUpdateAuthor', { authorId: id });
};
return (
<View style={{ flex: 1, backgroundColor: background }}>
{currentUser?.isAuthenticated && (
<DataList
navigation={navigation}
fetchFn={getList}
trigger={refresh}
render={({ item }) => (
<List.Item
key={item.id}
title={item.name}
description={item.shortBio || new Date(item.birthDate)?.toLocaleDateString()}
right={(props: any) => (
<IconButton
{...props}
icon="dots-vertical"
rippleColor={'#ccc'}
size={20}
onPress={() => openContextMenu(item)}
/>
)}
/>
)}
/>
)}
{currentUser?.isAuthenticated && policies['BookStore.Authors.Create'] && (
<AnimatedFAB
icon={'plus'}
label={i18n.t('BookStore::NewAuthor')}
color="white"
extended={false}
onPress={() => navigation.navigate('CreateUpdateAuthor')}
visible={true}
animateFrom={'right'}
iconMode={'static'}
style={[styles.fabStyle, { backgroundColor: primary }]}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: 'absolute',
},
});
export default AuthorsScreen;
在 ./src/screens/BookStore/Authors/CreateUpdateAuthor 文件夹下创建一个 CreateUpdateAuthorScreen.tsx 文件,并向其中添加以下代码。
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { get, create, update } from '../../../../api/AuthorAPI';
import LoadingActions from '../../../../store/actions/LoadingActions';
import { createLoadingSelector } from '../../../../store/selectors/LoadingSelectors';
import { connectToRedux } from '../../../../utils/ReduxConnect';
import CreateUpdateAuthorForm from './CreateUpdateAuthorForm';
function CreateUpdateAuthorScreen({ navigation, route, startLoading, clearLoading }) {
const { authorId } = route.params || {};
const [ author, setAuthor ] = useState(null);
const submit = (data: any) => {
startLoading({ key: 'save' });
(data.id ? update(data, data.id) : create(data))
.then(() => navigation.goBack())
.finally(() => clearLoading());
};
useEffect(() => {
if (authorId) {
startLoading({ key: 'fetchAuthorDetail' });
get(authorId)
.then((response: any) => setAuthor(response))
.finally(() => clearLoading());
}
}, [authorId]);
return <CreateUpdateAuthorForm submit={submit} author={author} />;
}
CreateUpdateAuthorScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
clearLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateAuthorScreen,
stateProps: (state: any) => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
clearLoading: LoadingActions.clear,
},
});
在 ./src/screens/BookStore/Authors/CreateUpdateAuthor 文件夹下创建一个 CreateUpdateAuthorForm.tsx 文件,并向其中添加以下代码。
import { useRef, useState } from 'react';
import { Platform, KeyboardAvoidingView, StyleSheet, View, ScrollView } from 'react-native';
import { useFormik } from 'formik';
import i18n from 'i18n-js';
import PropTypes from 'prop-types';
import * as Yup from 'yup';
import { Divider, Portal, TextInput, Text, Button, Modal } from 'react-native-paper';
import DateTimePicker from '@react-native-community/datetimepicker';
import { useThemeColors } from '../../../../hooks';
import { FormButtons, ValidationMessage } from '../../../../components';
const validations = {
name: Yup.string().required('AbpValidation::ThisFieldIsRequired.'),
birthDate: Yup.string().nullable().required('AbpValidation::ThisFieldIsRequired.'),
};
const props = {
underlineStyle: { backgroundColor: 'transparent' },
underlineColor: '#333333bf',
};
function CreateUpdateAuthorForm({ submit, author = null }) {
const { primaryContainer, background, onBackground } = useThemeColors();
const [birthDateVisible, setPublishDateVisible] = useState(false);
const nameRef = useRef(null);
const birthDateRef = useRef(null);
const shortBioRef = useRef(null);
const inputStyle = { ...styles.input, backgroundColor: primaryContainer };
const onSubmit = (values: any) => {
if (!authorForm.isValid) {
return;
}
submit({ ...values });
};
const authorForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
...author,
name: author?.name || '',
birthDate: (author?.birthDate && new Date(author?.birthDate)) || null,
shortBio: author?.shortBio || '',
},
onSubmit,
});
const isInvalidControl = (controlName = null) => {
if (!controlName) {
return;
}
return (
((!!authorForm.touched[controlName] && authorForm.submitCount > 0) ||
authorForm.submitCount > 0) &&
!!authorForm.errors[controlName]
);
};
const onChange = (event: any, selectedDate: any) => {
if (!selectedDate) {
return;
}
setPublishDateVisible(false);
if (event && event.type !== 'dismissed') {
authorForm.setFieldValue('birthDate', selectedDate, true);
}
};
return (
<View style={{ flex: 1, backgroundColor: background }}>
{birthDateVisible && (
<DateTimePicker
testID="birthDatePicker"
value={authorForm.values.birthDate || new Date()}
mode={'date'}
is24Hour={true}
onChange={onChange}
/>
)}
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<ScrollView keyboardShouldPersistTaps="handled">
<View style={styles.inputContainer}>
<TextInput
mode="flat"
ref={nameRef}
error={isInvalidControl('name')}
onSubmitEditing={() => birthDateRef.current.focus()}
returnKeyType="next"
onChangeText={authorForm.handleChange('name')}
onBlur={authorForm.handleBlur('name')}
value={authorForm.values.name}
autoCapitalize="none"
label={i18n.t('BookStore::Name')}
style={inputStyle}
{...props}
/>
{isInvalidControl('name') && (
<ValidationMessage>{authorForm.errors.name as string}</ValidationMessage>
)}
</View>
<View style={styles.inputContainer}>
<TextInput
ref={birthDateRef}
label={i18n.t('BookStore::BirthDate')}
onSubmitEditing={() => shortBioRef.current.focus()}
right={
<TextInput.Icon onPress={() => setPublishDateVisible(true)} icon="calendar" />
}
style={inputStyle}
editable={false}
value={authorForm.values.birthDate?.toLocaleDateString()}
{...props}
/>
{isInvalidControl('birthDate') && (
<ValidationMessage>{authorForm.errors.birthDate as string}</ValidationMessage>
)}
</View>
<Portal>
<Modal
visible={birthDateVisible}
contentContainerStyle={[styles.dateModal, { backgroundColor: background }]}>
<Text variant="titleLarge" style={styles.modalTitle}>
{i18n.t('BookStore::BirthDate')}
</Text>
<Divider style={styles.divider} />
<DateTimePicker
testID="birthDatePicker"
value={authorForm.values.birthDate || new Date()}
mode="date"
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
onChange={onChange}
maximumDate={new Date()}
textColor={onBackground}
/>
<View style={styles.modalButtons}>
<Button onPress={() => setPublishDateVisible(false)} mode="text">
{i18n.t('AbpUi::Cancel')}
</Button>
<Button onPress={() => setPublishDateVisible(false)} mode="contained">
{i18n.t('AbpUi::Ok')}
</Button>
</View>
</Modal>
</Portal>
<View style={styles.inputContainer}>
<TextInput
mode="flat"
ref={shortBioRef}
error={isInvalidControl('shortBio')}
onSubmitEditing={() => authorForm.handleSubmit()}
returnKeyType="next"
onChangeText={authorForm.handleChange('shortBio')}
onBlur={authorForm.handleBlur('shortBio')}
value={authorForm.values.shortBio}
autoCapitalize="none"
label={i18n.t('BookStore::ShortBio')}
style={inputStyle}
{...props}
/>
</View>
<FormButtons style={styles.button} submit={authorForm.handleSubmit} />
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
inputContainer: {
margin: 8,
marginLeft: 16,
marginRight: 16,
},
input: {
borderRadius: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
button: {
marginLeft: 16,
marginRight: 16,
},
divider: {
marginBottom: 16,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 20,
paddingHorizontal: 8,
},
dateModal: {
padding: 20,
margin: 20,
borderRadius: 12,
elevation: 5,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
modalTitle: {
textAlign: 'center',
marginBottom: 16,
fontWeight: '600',
},
});
CreateUpdateAuthorForm.propTypes = {
author: PropTypes.object,
submit: PropTypes.func.isRequired,
};
export default CreateUpdateAuthorForm;
将 作者 关联到书籍
更新 BookAPI 代理文件并包含 getAuthorLookup 方法
import api from "./API";
export const getList = () => api.get("/api/app/book").then(({ data }) => data);
// 添加此方法
export const getAuthorLookup = () =>
api.get("/api/app/book/author-lookup").then(({ data }) => data);
// 添加此方法
export const get = (id) =>
api.get(`/api/app/book/${id}`).then(({ data }) => data);
export const create = (input) =>
api.post("/api/app/book", input).then(({ data }) => data);
export const update = (input, id) =>
api.put(`/api/app/book/${id}`, input).then(({ data }) => data);
export const remove = (id) =>
api.delete(`/api/app/book/${id}`).then(({ data }) => data);
将 作者姓名 添加到书籍列表
打开 BooksScreen.tsx 文件(位于 ./src/screens/BookStore/Books 下)并更新以下代码。
//导入
function BooksScreen({ navigation }) {
//其他代码..
return (
//其他代码
<DataList
navigation={navigation}
fetchFn={getList}
trigger={refresh}
render={({ item }) => (
<List.Item
key={item.id}
title={item.name}
// 在此处更新
description={`${item.authorName} | ${i18n.t(
"BookStore::Enum:BookType." + item.type
)}`}
// 在此处更新
right={(props) => (
<IconButton
{...props}
icon="dots-vertical"
rippleColor={"#ccc"}
size={20}
onPress={() => openContextMenu(item)}
/>
)}
/>
)}
/>
//其他代码
);
}
item.authorName放置在书籍列表中书籍类型的旁边。
将作者列表传递给 CreateUpdateBookForm
import {
getAuthorLookup, // 添加此行
get,
create,
update,
} from "../../../../api/BookAPI";
import CreateUpdateBookForm from "./CreateUpdateBookForm";
function CreateUpdateBookScreen({
navigation,
route,
startLoading,
clearLoading,
}) {
// 添加此变量
const [authors, setAuthors] = useState([]);
// 从 author-lookup 端点获取作者
useEffect(() => {
getAuthorLookup().then(({ items } = {}) => setAuthors(items));
}, []);
// 将作者列表传递给表单
return <CreateUpdateBookForm submit={submit} book={book} authors={authors} />;
}
//其他代码..
- 我们将在
CreateUpdateBookForm组件中定义authors属性,它将用于作者下拉列表。 - 在 useEffect 函数中,我们将从服务器获取作者并设置
authors变量。
将 authorId 字段添加到书籍表单
const validations = {
authorId: Yup.string()
.nullable()
.required("AbpValidation::ThisFieldIsRequired."),
// 其他验证器
};
// 添加 `authors` 参数
function CreateUpdateBookForm({ submit, book = null, authors = [] }) {
// 为作者列表添加此变量
const [authorSelectVisible, setAuthorSelectVisible] = useState(false);
const authorIdRef = useRef(); // 添加此行
// 更新表单
const bookForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
// 添加这些
authorId: book?.authorId || "",
author: authors.find((f) => f.id === book?.authorId)?.name || "",
// 添加这些
},
onSubmit,
});
// 其他代码..
// 为作者添加 `AbpSelect` 组件和 TextInput
return (
<View style={{ flex: 1, backgroundColor: background }}>
<AbpSelect
key="authorSelect"
title={i18n.t("BookStore::Authors")}
visible={authorSelectVisible}
items={authors.map(({ id, name }) => ({ id, displayName: name }))}
hasDefualtItem={true}
hideModalFn={() => setAuthorSelectVisible(false)}
selectedItem={bookForm.values.authorId}
setSelectedItem={(id) => {
bookForm.setFieldValue("authorId", id, true);
bookForm.setFieldValue(
"author",
authors.find((f) => f.id === id)?.name || null,
false
);
}}
/>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView keyboardShouldPersistTaps="handled">
<View style={styles.input.container}>
<TextInput
ref={authorIdRef}
error={isInvalidControl("authorId")}
label={i18n.t("BookStore::Author")}
right={
<TextInput.Icon
onPress={() => setAuthorSelectVisible(true)}
icon="menu-down"
/>
}
style={inputStyle}
editable={false}
value={bookForm.values.author}
{...props}
/>
{isInvalidControl("authorId") && (
<ValidationMessage>{bookForm.errors.authorId}</ValidationMessage>
)}
</View>
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
CreateUpdateBookForm.propTypes = {
authors: PropTypes.array.isRequired, // 包含此项
};
export default CreateUpdateBookForm;
- 使用
AbpSelect组件创建作者下拉输入框。 - 在
TextInput中显示选定的作者。
以上就是全部内容。只需运行应用程序并尝试创建或编辑作者即可。
抠丁客




















