在React Native中开发一个记账收入页面UI设计,你可以遵循以下步骤来构建一个用户友好的界面。这个页面通常需要包含输入收入金额、选择收入类别、记录备注和日期等功能。以下是一些基本的步骤和示例代码,帮助你开始:
- 初始化React Native项目
如果你还没有创建一个React Native项目,你可以使用以下命令来初始化:
bash
npx react-native init AccountBook
cd AccountBook
- 安装依赖
确保你已经安装了必要的依赖,例如react-native-paper和react-native-vector-icons,这些库可以帮助你快速实现一些UI组件和图标。
bash
npm install react-native-paper react-native-vector-icons
- 创建记账收入页面
在你的项目中,创建一个新的页面文件,例如IncomeScreen.js。
javascript
import React, { useState } from 'react';
import { View, Text, TextInput, Button, StyleSheet } from 'react-native';
import { IconButton } from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import DateTimePicker from '@react-native-community/datetimepicker';
const IncomeScreen = () => {
const [amount, setAmount] = useState('');
const [category, setCategory] = useState('');
const [note, setNote] = useState('');
const [date, setDate] = useState(new Date());
const [showDatePicker, setShowDatePicker] = useState(false);
const onChangeDate = (event, selectedDate) => {
const currentDate = selectedDate || date;
setShowDatePicker(Platform.OS === 'Harmony');
setDate(currentDate);
};
const addIncome = () => {
// 处理添加收入逻辑,例如发送到服务器或保存到本地存储等
console.log('Amount:', amount, 'Category:', category, 'Note:', note, 'Date:', date);
};
return (
<View style={styles.container}>
<Text style={styles.label}>金额</Text>
<TextInput
style={styles.input}
placeholder="输入金额"
keyboardType="numeric"
value={amount}
onChangeText={setAmount}
/>
<Text style={styles.label}>类别</Text>
<TextInput
style={styles.input}
placeholder="选择或输入类别"
value={category}
onChangeText={setCategory}
/>
<Text style={styles.label}>备注</Text>
<TextInput
style={styles.input}
placeholder="添加备注"
multiline
value={note}
onChangeText={setNote}
/>
<Text style={styles.label}>日期</Text>
<View style={styles.dateContainer}>
<Text>{date.toLocaleDateString()}</Text>
<IconButton icon="calendar" onPress={() => setShowDatePicker(true)} />
</View>
{showDatePicker && (
<DateTimePicker
value={date}
mode="date"
is24Hour={true}
onChange={onChangeDate}
/>
)}
<Button title="添加收入" onPress={addIncome} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: 'fff',
},
label: {
fontSize: 16,
marginBottom: 5,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
paddingHorizontal: 10,
},
dateContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderColor: 'gray',
borderWidth: 1,
paddingHorizontal: 10,
marginBottom: 10,
},
});
真实案例演示代码:
js
// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, TextInput, Modal, Alert } from 'react-native';
// Base64 图标库
const ICONS = {
add: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSAxM2gtNnY2aC0ydi02SDV2LTJoNlY1aDJ2Nmg2djJ6Ii8+PC9zdmc+',
money: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0wIDE4Yy00LjQxIDAtOC0zLjU5LTgtOHMzLjU5LTggOC04IDggMy41OSA4IDgtMy41OSA4LTggOHptLTMtOWgyLjVWN2gydjJoMnYyaC0ydiJoLTJ2LTJoLTIuNVY5eiIvPjxwYXRoIGQ9Ik0xMiAxN2MtMi43NiAwLTUtMi4yNC01LTVzMi4yNC01IDUtNSA1IDIuMjQgNSA1LTIuMjQgNS01IDV6Ii8+PC9zdmc+',
salary: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0yMCA2aC00VjRjMC0xLjEtLjktMi0yLTJoLTRjLTEuMSAwLTItLjktMi0yVjZINEMxLjggNiAwIDcuOCAwIDEwLjAxdjExLjk4QzAgMjMuMS45IDI0IDIuMDEgMjRoMTkuOThjMS4xMSAwIDItLjkgMi0ydi0xMGMwLTEuMjItLjgtMi4wMS0yLjAxLTIuMDF6TTEyIDhoMnYyaC0yeiIvPjxwYXRoIGQ9Ik0xOCAxMmgtMnYyaDJ2LTJ6bS00IDBoLTJ2Mmg0di0yaC0yeiIvPjxwYXRoIGQ9Ik0xNCAxN2gtMnYyaDJ2LTJ6bS00IDBoLTR2Mmg0di0yeiIvPjwvc3ZnPg==',
bonus: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyLjA0TDMgNi45OCAzIDEyaDE4VjYuOTdMMTIgMi4wNHpNMjEgMTVIN2MtMS4xIDAtMiAuOS0yIDJ2M2gydi0zaDE0djE0SDd2LTNoLTJ2M2MwIDEuMS45IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjE3YzAtMS4xLS45LTItMi0yeiIvPjwvc3ZnPg==',
investment: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0wIDE4Yy00LjQxIDAtOC0zLjU5LTgtOHMzLjU5LTggOC04IDggMy41OSA4IDgtMy41OSA4LTggOHptLTMtOWgyLjVWN2gydjJoMnYyaC0ydiJoLTJ2LTJoLTIuNVY5eiIvPjxwYXRoIGQ9Ik0xMiAxN2MtMi43NiAwLTUtMi4yNC01LTVzMi4yNC01IDUtNSA1IDIuMjQgNSA1LTIuMjQgNS01IDV6Ii8+PC9zdmc+',
other: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyQzYuNDggMiAyIDYuNDggMiAxMnM0LjQ4IDEwIDEwIDEwIDEwLTQuNDggMTAtMTBTMTcuNTIgMiAxMiAyem0wIDE4Yy00LjQxIDAtOC0zLjU5LTgtOHMzLjU5LTggOC04IDggMy41OSA4IDgtMy41OSA4LTggOHptLTMtOWgyLjVWN2gydjJoMnYyaC0ydiJoLTJ2LTJoLTIuNVY5eiIvPjxwYXRoIGQ9Ik0xMiAxN2MtMi43NiAwLTUtMi4yNC01LTVzMi4yNC01IDUtNSA1IDIuMjQgNSA1LTIuMjQgNS01IDV6Ii8+PC9zdmc+',
close: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSA2LjQxTDE3LjU5IDUgMTIgMTAuNTkgNi40MSA1IDUgNi40MSAxMC41OSAxMiA1IDE3LjU5IDYuNDEgMTkgMTIgMTMuNDEgMTcuNTkgMTkgMTkgMTcuNTkgMTMuNDEgMTJ6Ii8+PC9zdmc+',
calendar: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xOSA0aC0xVjJoLTJ2Mkg4VjJINlY0SDVjLTEuMSAwLTIgLjktMiAydjEzYzAgMS4xLjkgMiAyIDJoMTRjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMnptMCAxNUg1VjhoMTR2MTF6bS05LTVoMlY3aC0yeiIvPjwvc3ZnPg==',
note: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xNCAyaC00djJoLTR2NGgydjFoNlY4aDJWNmgtNHYtMnptLTggMmg0djJoLTRWNHptMTIgNEg4djJoMTJWNHptLTExIDE2aDJ2LTJoLTJ2MnptMyAwaDJ2LTJoLTJ2MnptMyAwaDJ2LTJoLTJ2MnptMyAwaDJ2LTJoLTJ2MnptMyAwaDJ2LTE0SDR2MThoMTJ2LTJ6Ii8+PC9zdmc+',
};
// 收入类型选项
const INCOME_TYPES = [
{ id: 'salary', name: '工资', icon: ICONS.salary, color: '#4361ee' },
{ id: 'bonus', name: '奖金', icon: ICONS.bonus, color: '#3a0ca3' },
{ id: 'investment', name: '投资收益', icon: ICONS.investment, color: '#7209b7' },
{ id: 'other', name: '其他', icon: ICONS.other, color: '#f72585' },
];
// 默认收入记录
const DEFAULT_RECORDS = [
{ id: '1', type: 'salary', amount: '8500', date: '2023-06-01', note: '基本工资' },
{ id: '2', type: 'bonus', amount: '2000', date: '2023-06-15', note: '季度奖金' },
{ id: '3', type: 'investment', amount: '1200', date: '2023-06-20', note: '基金收益' },
];
const IncomeRecorder: React.FC = () => {
const [records, setRecords] = useState(DEFAULT_RECORDS);
const [modalVisible, setModalVisible] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [currentRecord, setCurrentRecord] = useState({
id: '',
type: 'salary',
amount: '',
date: new Date().toISOString().split('T')[0],
note: ''
});
// 打开添加收入模态框
const openAddModal = () => {
setIsEditing(false);
setCurrentRecord({
id: '',
type: 'salary',
amount: '',
date: new Date().toISOString().split('T')[0],
note: ''
});
setModalVisible(true);
};
// 打开编辑收入模态框
const openEditModal = (record: any) => {
setIsEditing(true);
setCurrentRecord(record);
setModalVisible(true);
};
// 保存收入记录
const saveRecord = () => {
if (!currentRecord.amount || parseFloat(currentRecord.amount) <= 0) {
Alert.alert('错误', '请输入有效的金额');
return;
}
if (isEditing) {
// 编辑现有记录
setRecords(records.map(record =>
record.id === currentRecord.id ? currentRecord : record
));
} else {
// 添加新记录
const newRecord = {
...currentRecord,
id: Date.now().toString()
};
setRecords([...records, newRecord]);
}
setModalVisible(false);
};
// 删除收入记录
const deleteRecord = (id: string) => {
Alert.alert(
'确认删除',
'确定要删除这条收入记录吗?',
[
{ text: '取消', style: 'cancel' },
{ text: '删除', style: 'destructive', onPress: () => setRecords(records.filter(record => record.id !== id)) }
]
);
};
// 获取总收入
const totalIncome = records.reduce((sum, record) => sum + parseFloat(record.amount), 0);
// 获取收入类型信息
const getIncomeTypeInfo = (typeId: string) => {
return INCOME_TYPES.find(type => type.id === typeId) || INCOME_TYPES[0];
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>收入记录</Text>
<Text style={styles.subtitle}>记录您的每一笔收入</Text>
<View style={styles.summaryCard}>
<Text style={styles.summaryLabel}>本月总收入</Text>
<Text style={styles.summaryAmount}>¥ {totalIncome.toFixed(2)}</Text>
</View>
</View>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.typeSelector}>
<Text style={styles.sectionTitle}>收入类型</Text>
<View style={styles.typeOptions}>
{INCOME_TYPES.map(type => (
<TouchableOpacity
key={type.id}
style={[
styles.typeOption,
currentRecord.type === type.id && { backgroundColor: type.color + '20', borderColor: type.color }
]}
onPress={() => setCurrentRecord({...currentRecord, type: type.id})}
>
<Text style={[styles.typeIcon, { color: type.color }]}>
{decodeURIComponent(escape(atob(type.icon.split(',')[1])))}
</Text>
<Text style={[styles.typeName, currentRecord.type === type.id && { color: type.color }]}>
{type.name}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.recordList}>
<Text style={styles.sectionTitle}>收入记录</Text>
{records.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>暂无收入记录</Text>
</View>
) : (
records.map(record => {
const typeInfo = getIncomeTypeInfo(record.type);
return (
<View key={record.id} style={styles.recordCard}>
<View style={styles.recordHeader}>
<View style={styles.recordType}>
<Text style={[styles.recordTypeIcon, { color: typeInfo.color }]}>
{decodeURIComponent(escape(atob(typeInfo.icon.split(',')[1])))}
</Text>
<Text style={[styles.recordTypeName, { color: typeInfo.color }]}>
{typeInfo.name}
</Text>
</View>
<Text style={styles.recordDate}>{record.date}</Text>
</View>
<View style={styles.recordBody}>
<Text style={styles.recordAmount}>¥ {parseFloat(record.amount).toFixed(2)}</Text>
{record.note ? <Text style={styles.recordNote}>{record.note}</Text> : null}
</View>
<View style={styles.recordActions}>
<TouchableOpacity
style={styles.editButton}
onPress={() => openEditModal(record)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => deleteRecord(record.id)}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
</View>
</View>
);
})
)}
</View>
</ScrollView>
<TouchableOpacity style={styles.addButton} onPress={openAddModal}>
<Text style={styles.addIcon}>{decodeURIComponent(escape(atob(ICONS.add.split(',')[1])))}</Text>
</TouchableOpacity>
{/* 添加/编辑收入记录模态框 */}
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => setModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>{isEditing ? '编辑收入记录' : '添加收入记录'}</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Text style={styles.closeButton}>×</Text>
</TouchableOpacity>
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>收入类型</Text>
<View style={styles.modalTypeOptions}>
{INCOME_TYPES.map(type => (
<TouchableOpacity
key={type.id}
style={[
styles.modalTypeOption,
currentRecord.type === type.id && { backgroundColor: type.color }
]}
onPress={() => setCurrentRecord({...currentRecord, type: type.id})}
>
<Text style={[
styles.modalTypeText,
currentRecord.type === type.id && { color: '#ffffff' }
]}>
{type.name}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>金额</Text>
<View style={styles.amountInputContainer}>
<Text style={styles.currencySymbol}>¥</Text>
<TextInput
style={styles.amountInput}
value={currentRecord.amount}
onChangeText={(text) => setCurrentRecord({...currentRecord, amount: text})}
placeholder="0.00"
keyboardType="decimal-pad"
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>日期</Text>
<View style={styles.dateInputContainer}>
<Text style={styles.calendarIcon}>
{decodeURIComponent(escape(atob(ICONS.calendar.split(',')[1])))}
</Text>
<TextInput
style={styles.dateInput}
value={currentRecord.date}
onChangeText={(text) => setCurrentRecord({...currentRecord, date: text})}
placeholder="YYYY-MM-DD"
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>备注</Text>
<View style={styles.noteInputContainer}>
<Text style={styles.noteIcon}>
{decodeURIComponent(escape(atob(ICONS.note.split(',')[1])))}
</Text>
<TextInput
style={styles.noteInput}
value={currentRecord.note}
onChangeText={(text) => setCurrentRecord({...currentRecord, note: text})}
placeholder="添加备注..."
multiline
/>
</View>
</View>
<TouchableOpacity style={styles.saveButton} onPress={saveRecord}>
<Text style={styles.saveButtonText}>保存记录</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
},
header: {
paddingTop: 30,
paddingBottom: 25,
paddingHorizontal: 20,
backgroundColor: '#4cc9f0',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#ffffff',
textAlign: 'center',
marginBottom: 6,
},
subtitle: {
fontSize: 16,
color: '#e0f7fa',
textAlign: 'center',
marginBottom: 20,
},
summaryCard: {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 15,
padding: 20,
alignItems: 'center',
},
summaryLabel: {
fontSize: 16,
color: '#e0f7fa',
marginBottom: 8,
},
summaryAmount: {
fontSize: 32,
fontWeight: 'bold',
color: '#ffffff',
},
content: {
padding: 20,
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#212529',
marginBottom: 15,
},
typeSelector: {
marginBottom: 30,
},
typeOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
typeOption: {
width: '48%',
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 15,
marginBottom: 12,
alignItems: 'center',
borderWidth: 2,
borderColor: '#e9ecef',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
typeIcon: {
fontSize: 24,
marginBottom: 8,
},
typeName: {
fontSize: 16,
color: '#495057',
fontWeight: '600',
},
recordList: {
marginBottom: 80,
},
recordCard: {
backgroundColor: '#ffffff',
borderRadius: 15,
padding: 20,
marginBottom: 15,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
recordHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
},
recordType: {
flexDirection: 'row',
alignItems: 'center',
},
recordTypeIcon: {
fontSize: 20,
marginRight: 8,
},
recordTypeName: {
fontSize: 16,
fontWeight: '600',
},
recordDate: {
fontSize: 14,
color: '#6c757d',
},
recordBody: {
marginBottom: 15,
},
recordAmount: {
fontSize: 24,
fontWeight: 'bold',
color: '#212529',
marginBottom: 5,
},
recordNote: {
fontSize: 14,
color: '#6c757d',
},
recordActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
editButton: {
backgroundColor: '#4361ee',
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 8,
marginRight: 10,
},
editButtonText: {
color: '#ffffff',
fontWeight: '600',
},
deleteButton: {
backgroundColor: '#e63946',
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 8,
},
deleteButtonText: {
color: '#ffffff',
fontWeight: '600',
},
emptyContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
},
emptyText: {
fontSize: 16,
color: '#6c757d',
},
addButton: {
position: 'absolute',
bottom: 30,
right: 30,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#4361ee',
alignItems: 'center',
justifyContent: 'center',
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 6,
},
addIcon: {
fontSize: 28,
color: '#ffffff',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
backgroundColor: '#ffffff',
width: '85%',
borderRadius: 20,
padding: 25,
maxHeight: '80%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#212529',
},
closeButton: {
fontSize: 30,
color: '#adb5bd',
fontWeight: '200',
},
inputGroup: {
marginBottom: 20,
},
inputLabel: {
fontSize: 16,
fontWeight: '600',
color: '#495057',
marginBottom: 10,
},
modalTypeOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
},
modalTypeOption: {
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 8,
marginRight: 10,
marginBottom: 10,
backgroundColor: '#e9ecef',
},
modalTypeText: {
fontSize: 14,
color: '#495057',
fontWeight: '600',
},
amountInputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ced4da',
borderRadius: 12,
paddingHorizontal: 15,
paddingVertical: 12,
backgroundColor: '#f8f9fa',
},
currencySymbol: {
fontSize: 20,
color: '#495057',
marginRight: 10,
},
amountInput: {
flex: 1,
fontSize: 20,
color: '#212529',
},
dateInputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ced4da',
borderRadius: 12,
paddingHorizontal: 15,
paddingVertical: 12,
backgroundColor: '#f8f9fa',
},
calendarIcon: {
fontSize: 20,
color: '#495057',
marginRight: 10,
},
dateInput: {
flex: 1,
fontSize: 16,
color: '#212529',
},
noteInputContainer: {
flexDirection: 'row',
borderWidth: 1,
borderColor: '#ced4da',
borderRadius: 12,
paddingHorizontal: 15,
paddingVertical: 12,
backgroundColor: '#f8f9fa',
},
noteIcon: {
fontSize: 20,
color: '#495057',
marginRight: 10,
marginTop: 5,
},
noteInput: {
flex: 1,
fontSize: 16,
color: '#212529',
minHeight: 80,
textAlignVertical: 'top',
},
saveButton: {
backgroundColor: '#4361ee',
paddingVertical: 15,
borderRadius: 12,
alignItems: 'center',
marginTop: 10,
},
saveButtonText: {
fontSize: 18,
fontWeight: 'bold',
color: '#ffffff',
},
});
export default IncomeRecorder;
这段React Native代码实现了一个收入记录管理系统,其核心架构基于函数组件和Hooks状态管理机制。系统通过useState Hook维护收入记录数据的状态树,包含记录列表、模态框可见性、编辑状态等关键状态。UI层面采用原生组件构建列表布局,每个收入记录卡片通过内联样式动态绑定类型颜色属性,实现视觉差异化。模态框组件通过透明遮罩和滑动动画提供流畅的用户体验,数据持久化可通过集成AsyncStorage实现本地存储。
在鸿蒙生态适配方面,该系统可作为财务管理应用的基础模块。通过ArkTS桥接可实现跨平台运行,利用HarmonyOS分布式能力扩展至多设备协同场景。系统架构支持动态加载收入类型图标资源,便于后续集成鸿蒙系统的图形渲染优化特性。鸿蒙特有的Ability框架可将该管理系统封装为独立服务能力,通过Intent机制实现应用间数据传递。在UI层面,可借助鸿蒙ArkUI框架的声明式语法重构组件,利用其响应式渲染机制提升性能。系统还可接入鸿蒙生态的金融服务,实现收入记录与银行账户数据的深度融合,构建完整的财务管理解决方案。
从技术实现角度看,该系统展现了现代化前端开发的核心理念。状态管理采用单向数据流模式,通过useState Hook实现响应式更新,确保UI与数据同步。组件设计遵循单一职责原则,将展示逻辑与业务逻辑分离,提高代码可维护性。模态框的实现利用了React Native的Modal组件,通过透明背景和滑动动画增强用户体验。收入记录卡片采用Flexbox布局实现响应式列表,适配不同屏幕尺寸。
useState Hook的使用体现了React的状态管理机制,每次调用setRecords都会触发组件重新渲染,这种不可变数据更新模式确保了状态的一致性。收入记录的CRUD操作通过数组的map、filter等高阶函数实现,展示了函数式编程在React中的应用。模态框的状态控制通过布尔值切换实现,体现了简单的状态机思想。Alert组件的使用提供了用户友好的确认对话框,增强了用户体验。统计信息的计算通过reduce方法实现,展示了数据聚合的简洁性。
样式设计方面,通过StyleSheet.create定义样式对象,实现了样式的复用和性能优化。条件样式通过内联样式和样式数组结合实现,提供了灵活的视觉反馈。收入记录卡片的类型区分通过颜色动态绑定实现视觉区分,这种设计模式在移动端UI中非常常见。SafeAreaView组件的使用确保了在不同设备上的安全区域适配,体现了对不同屏幕尺寸的兼容性考虑。
数据验证逻辑通过parseFloat和条件判断实现,确保了收入金额的有效性。日期处理通过toISOString方法标准化日期格式,保证了数据的一致性。收入类型信息通过find方法动态获取,体现了数据驱动的UI设计思想。记录列表的空状态处理通过条件渲染实现,提供了良好的用户体验。
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示: