React Native鸿蒙跨平台开发包含输入收入金额、选择收入类别、记录备注和日期等功能,实战react-native-paper组件

在React Native中开发一个记账收入页面UI设计,你可以遵循以下步骤来构建一个用户友好的界面。这个页面通常需要包含输入收入金额、选择收入类别、记录备注和日期等功能。以下是一些基本的步骤和示例代码,帮助你开始:

  1. 初始化React Native项目

如果你还没有创建一个React Native项目,你可以使用以下命令来初始化:

bash 复制代码
npx react-native init AccountBook
cd AccountBook
  1. 安装依赖

确保你已经安装了必要的依赖,例如react-native-paperreact-native-vector-icons,这些库可以帮助你快速实现一些UI组件和图标。

bash 复制代码
npm install react-native-paper react-native-vector-icons
  1. 创建记账收入页面

在你的项目中,创建一个新的页面文件,例如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工程目录去:

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

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

相关推荐
小白学大数据7 小时前
Python 爬虫如何分析并模拟 JS 动态请求
开发语言·javascript·爬虫·python
巴拉巴拉~~7 小时前
Flutter 通用表单输入组件 CustomInputWidget:校验 + 样式 + 交互一键适配
javascript·flutter·交互
San30.7 小时前
现代前端工程化实战:从 Vite 到 Vue Router 的构建之旅
前端·javascript·vue.js
sg_knight7 小时前
模块热替换 (HMR):前端开发的“魔法”与提速秘籍
前端·javascript·vue·浏览器·web·模块化·hmr
A24207349307 小时前
js常用事件
开发语言·前端·javascript
Fighting_p7 小时前
【导出】前端 js 导出下载文件时,文件名前后带下划线问题
开发语言·前端·javascript
WYiQIU7 小时前
从今天开始备战1月中旬的前端寒假实习需要准备什么?(飞书+github+源码+题库含答案)
前端·javascript·面试·职场和发展·前端框架·github·飞书
AI分享猿7 小时前
新手跨境电商实测:Apache 搭站,雷池 WAF 零基础部署
安全·web安全·react.js·网络安全·开源·apache
摸鱼少侠梁先生7 小时前
通过接口获取字典的数据进行渲染
前端·javascript·vue.js