react-native-calendarsReact Native库来帮助你处理日期和时间,实现鸿蒙跨平台开发日历组件

在React Native中开发一个保养日程表功能,你可以按照以下步骤进行:

  1. 初始化项目

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

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

你可能需要一些额外的库来帮助你处理日期和时间,例如react-native-calendars。使用npm或yarn来安装这些库:

bash 复制代码
npm install react-native-calendars

或者使用yarn:

bash 复制代码
yarn add react-native-calendars
  1. 创建日历组件

使用react-native-calendars库来创建一个日历组件,用户可以在上面选择日期。

javascript 复制代码
import React from 'react';
import { Calendar } from 'react-native-calendars';
import { View } from 'react-native';

const CalendarComponent = () => {
  return (
    <View>
      <Calendar
        onDayPress={day => {
          console.log('selected day', day);
        }}
        markedDates={{
          '2023-04-12': { selected: true, selectedColor: 'blue' }, // Custom marking
        }}
      />
    </View>
  );
};

export default CalendarComponent;
  1. 添加保养事项输入功能

你可以使用TextInputButton来让用户输入特定的保养事项和选择日期。

javascript 复制代码
import React, { useState } from 'react';
import { View, TextInput, Button, Text, Alert } from 'react-native';
import CalendarComponent from './CalendarComponent'; // 假设你已经创建了CalendarComponent组件文件

const MaintenanceForm = () => {
  const [task, setTask] = useState('');
  const [date, setDate] = useState('');
  const [tasks, setTasks] = useState([]); // 存储所有任务和日期

  const handleSubmit = () => {
    if (task && date) {
      setTasks([...tasks, { task, date }]); // 添加到任务列表中
      Alert.alert('Task Added', 'Task has been added successfully.');
      setTask(''); // 清空输入框内容
      setDate(''); // 清空日期选择内容(如果需要的话,你可能需要更复杂的逻辑来处理日期)
    } else {
      Alert.alert('Error', 'Please enter both task and date.');
    }
  };

  return (
    <View>
      <TextInput 
        placeholder="Enter maintenance task" 
        value={task} 
        onChangeText={setTask} 
      />
      <TextInput 
        placeholder="Select date" 
        value={date} 
        onChangeText={setDate} 
        editable={false} // 如果使用外部日历组件选择日期,则可以设置为不可编辑,并通过日历组件的回调设置此值。如果直接在TextInput选择,则需要额外的日期选择器库或自定义实现。例如使用react-native-datepicker。
      />
      <Button title="Add Task" onPress={handleSubmit} />
      <Text>Scheduled Tasks:</Text>
      {tasks.map((item, index) => (
        <Text key={index}>{item.date}: {item.task}</Text> // 显示所有任务和日期信息。根据实际需求调整显示格式。例如,你可能需要格式化日期。可以使用moment.js等库。
      ))}
    </View>
  );
};

注意:在上面的代码中,TextInput用于选择日期是不可编辑的,因为通常我们会使用一个外部的日历组件来选择日期。如果你想要在TextInput中选择日期,你可以使用react-native-datepicker或者类似的库。例如:

bash 复制代码
npm install react-native-datepicker --save // 或者 yarn add react-native-datepicker

然后在代码中使用它:

javascript 复制代码
import DatePicker from 'react-native-datepicker'; // 引入DatePicker组件。确保链接了相应的库(例如Harmony的权限)。具体可以查看react-native-datepicker的文档进行配置。例如:https://github.com/xgfe/react-native-datepickerHarmony--Harmony--windows--web-support。通常Harmony需要配置权限和一些额外的链接步骤。对于Harmony,通常只需要安装库并导入即可。对于Windows和Web平台,也需要确保正确配置。具体配置方法请参考官方

真实项目组件案例演示:

js 复制代码
// app.tsx
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Image, Alert } from 'react-native';

const App = () => {
  const [currentMonth, setCurrentMonth] = useState(new Date());
  const [selectedDate, setSelectedDate] = useState<Date | null>(null);
  const [events, setEvents] = useState<Record<string, any[]>>({
    '2023-10-15': [
      { id: 1, title: '机油更换', time: '09:00', vehicle: '丰田凯美瑞' }
    ],
    '2023-10-22': [
      { id: 2, title: '轮胎保养', time: '14:30', vehicle: '本田雅阁' }
    ],
    '2023-10-28': [
      { id: 3, title: '刹车系统检查', time: '10:00', vehicle: '大众帕萨特' },
      { id: 4, title: '空调滤芯更换', time: '15:00', vehicle: '日产轩逸' }
    ]
  });

  // Base64 icons
  const icons = {
    prev: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWxpbmUgcG9pbnRzPSIxNSAxOCA5IDEyIDE1IDYiPjwvcG9seWxpbmU+PC9zdmc+',
    next: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWxpbmUgcG9pbnRzPSI5IDYgMTUgMTIgOSAxOCI+PC9wb2x5bGluZT48L3N2Zz4=',
    calendar: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB4PSIzIiB5PSI0IiB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHJ4PSIyIiByeT0iMiI+PC9yZWN0PjxsaW5lIHgxPSI4IiB5MT0iMiIgeDI9IjgiIHkyPSI2Ij48L2xpbmU+PGxpbmUgeDE9IjE2IiB5MT0iMiIgeDI9IjE2IiB5Mj0iNiI+PC9saW5lPjxsaW5lIHgxPSIzIiB5MT0iMTAiIHgyPSIyMSIgeTI9IjEwIj48L2xpbmU+PC9zdmc+',
    event: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCI+PC9jaXJjbGU+PHBvbHlsaW5lIHBvaW50cz0iMTIgNiAxMiAxMiAxNiAxNCI+PC9wb2x5bGluZT48L3N2Zz4=',
    add: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCI+PC9jaXJjbGU+PGxpbmUgeDE9IjEyIiB5MT0iOCIgeDI9IjEyIiB5Mj0iMTYiPjwvbGluZT48bGluZSB4MT0iOCIgeTE9IjEyIiB4Mj0iMTYiIHkyPSIxMiI+PC9saW5lPjwvc3ZnPg=='
  };

  // 获取月份的天数
  const getDaysInMonth = (date: Date) => {
    const year = date.getFullYear();
    const month = date.getMonth();
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const daysInMonth = lastDay.getDate();
    const startingDayOfWeek = firstDay.getDay();

    const days = [];
    
    // 添加上个月的日期
    const prevMonthLastDay = new Date(year, month, 0).getDate();
    for (let i = startingDayOfWeek - 1; i >= 0; i--) {
      days.push({
        day: prevMonthLastDay - i,
        isCurrentMonth: false,
        date: new Date(year, month - 1, prevMonthLastDay - i)
      });
    }
    
    // 添加当前月的日期
    for (let i = 1; i <= daysInMonth; i++) {
      days.push({
        day: i,
        isCurrentMonth: true,
        date: new Date(year, month, i)
      });
    }
    
    // 添加下个月的日期
    const totalCells = 42; // 6行7列
    const remainingCells = totalCells - days.length;
    for (let i = 1; i <= remainingCells; i++) {
      days.push({
        day: i,
        isCurrentMonth: false,
        date: new Date(year, month + 1, i)
      });
    }
    
    return days;
  };

  // 格式化日期为 YYYY-MM-DD
  const formatDate = (date: Date) => {
    return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
  };

  // 切换月份
  const changeMonth = (direction: number) => {
    setCurrentMonth(prev => {
      const newMonth = new Date(prev);
      newMonth.setMonth(prev.getMonth() + direction);
      return newMonth;
    });
  };

  // 检查日期是否有事件
  const hasEvents = (date: Date) => {
    const dateStr = formatDate(date);
    return events[dateStr] && events[dateStr].length > 0;
  };

  // 获取星期标题
  const weekdays = ['日', '一', '二', '三', '四', '五', '六'];

  // 当前月份的天数数组
  const days = getDaysInMonth(currentMonth);

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>保养日程表</Text>
        <Text style={styles.subtitle}>管理您的车辆保养计划</Text>
      </View>

      {/* 日历头部 */}
      <View style={styles.calendarHeader}>
        <TouchableOpacity onPress={() => changeMonth(-1)} style={styles.navButton}>
          <Image source={{ uri: icons.prev }} style={styles.navIcon} />
        </TouchableOpacity>
        
        <View style={styles.monthYearContainer}>
          <Text style={styles.monthYearText}>
            {currentMonth.getFullYear()}年 {currentMonth.getMonth() + 1}月
          </Text>
        </View>
        
        <TouchableOpacity onPress={() => changeMonth(1)} style={styles.navButton}>
          <Image source={{ uri: icons.next }} style={styles.navIcon} />
        </TouchableOpacity>
      </View>

      {/* 星期标题 */}
      <View style={styles.weekdaysContainer}>
        {weekdays.map((day, index) => (
          <View key={index} style={styles.weekdayCell}>
            <Text style={[
              styles.weekdayText,
              index === 0 && styles.sundayText,
              index === 6 && styles.saturdayText
            ]}>
              {day}
            </Text>
          </View>
        ))}
      </View>

      {/* 日历主体 */}
      <View style={styles.calendarGrid}>
        {days.map((dayObj, index) => {
          const isSelected = selectedDate && 
            selectedDate.getDate() === dayObj.day && 
            selectedDate.getMonth() === dayObj.date.getMonth() && 
            selectedDate.getFullYear() === dayObj.date.getFullYear();
            
          const hasEvent = hasEvents(dayObj.date);
          
          return (
            <TouchableOpacity
              key={index}
              style={[
                styles.dayCell,
                !dayObj.isCurrentMonth && styles.otherMonthDay,
                isSelected && styles.selectedDay
              ]}
              onPress={() => setSelectedDate(dayObj.date)}
            >
              <View style={styles.dayContent}>
                <Text style={[
                  styles.dayText,
                  !dayObj.isCurrentMonth && styles.otherMonthDayText,
                  isSelected && styles.selectedDayText
                ]}>
                  {dayObj.day}
                </Text>
                
                {hasEvent && (
                  <View style={styles.eventIndicator}>
                    <Image source={{ uri: icons.event }} style={styles.eventIcon} />
                  </View>
                )}
              </View>
            </TouchableOpacity>
          );
        })}
      </View>

      {/* 事件详情 */}
      <View style={styles.eventsContainer}>
        <View style={styles.eventsHeader}>
          <Text style={styles.eventsTitle}>
            {selectedDate 
              ? `${selectedDate.getMonth() + 1}月${selectedDate.getDate()}日的保养安排` 
              : '请选择日期查看保养安排'}
          </Text>
          <TouchableOpacity style={styles.addButton}>
            <Image source={{ uri: icons.add }} style={styles.addIcon} />
            <Text style={styles.addButtonText}>添加</Text>
          </TouchableOpacity>
        </View>

        {selectedDate ? (
          <View style={styles.eventsList}>
            {events[formatDate(selectedDate)] ? (
              events[formatDate(selectedDate)].map(event => (
                <View key={event.id} style={styles.eventCard}>
                  <View style={styles.eventHeader}>
                    <Text style={styles.eventTitle}>{event.title}</Text>
                    <Text style={styles.eventTime}>{event.time}</Text>
                  </View>
                  <Text style={styles.eventVehicle}>{event.vehicle}</Text>
                </View>
              ))
            ) : (
              <View style={styles.emptyEvents}>
                <Text style={styles.emptyEventsText}>当天没有保养安排</Text>
              </View>
            )}
          </View>
        ) : (
          <View style={styles.emptyEvents}>
            <Text style={styles.emptyEventsText}>请选择一个日期查看保养安排</Text>
          </View>
        )}
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
    padding: 20
  },
  header: {
    alignItems: 'center',
    marginBottom: 25,
    paddingTop: 20
  },
  title: {
    fontSize: 26,
    fontWeight: 'bold',
    color: '#2c3e50'
  },
  subtitle: {
    fontSize: 15,
    color: '#7f8c8d',
    marginTop: 6
  },
  calendarHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    marginBottom: 20,
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 15,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4
  },
  navButton: {
    padding: 10
  },
  navIcon: {
    width: 24,
    height: 24
  },
  monthYearContainer: {
    alignItems: 'center'
  },
  monthYearText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#2c3e50'
  },
  weekdaysContainer: {
    flexDirection: 'row',
    backgroundColor: '#4285F4',
    borderRadius: 12,
    overflow: 'hidden',
    marginBottom: 10
  },
  weekdayCell: {
    flex: 1,
    alignItems: 'center',
    paddingVertical: 12
  },
  weekdayText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#fff'
  },
  sundayText: {
    color: '#ffebee'
  },
  saturdayText: {
    color: '#e3f2fd'
  },
  calendarGrid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    backgroundColor: '#fff',
    borderRadius: 16,
    overflow: 'hidden',
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    marginBottom: 25
  },
  dayCell: {
    width: '14.28%',
    aspectRatio: 1,
    alignItems: 'center',
    justifyContent: 'center',
    borderBottomWidth: 1,
    borderRightWidth: 1,
    borderColor: '#eee'
  },
  otherMonthDay: {
    backgroundColor: '#f8f9fa'
  },
  selectedDay: {
    backgroundColor: '#4285F4'
  },
  dayContent: {
    alignItems: 'center'
  },
  dayText: {
    fontSize: 18,
    fontWeight: '500',
    color: '#34495e'
  },
  otherMonthDayText: {
    color: '#bdc3c7'
  },
  selectedDayText: {
    color: '#fff'
  },
  eventIndicator: {
    position: 'absolute',
    bottom: 2,
    right: 2
  },
  eventIcon: {
    width: 10,
    height: 10
  },
  eventsContainer: {
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4
  },
  eventsHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 20
  },
  eventsTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#2c3e50',
    flex: 1
  },
  addButton: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#4285F4',
    borderRadius: 20,
    paddingHorizontal: 15,
    paddingVertical: 8
  },
  addIcon: {
    width: 18,
    height: 18,
    marginRight: 6
  },
  addButtonText: {
    color: '#fff',
    fontWeight: '600',
    fontSize: 15
  },
  eventsList: {
    gap: 15
  },
  eventCard: {
    backgroundColor: '#f8f9fa',
    borderRadius: 12,
    padding: 16,
    borderWidth: 1,
    borderColor: '#eef2f7'
  },
  eventHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 8
  },
  eventTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#2c3e50'
  },
  eventTime: {
    fontSize: 16,
    color: '#4285F4',
    fontWeight: '600'
  },
  eventVehicle: {
    fontSize: 15,
    color: '#7f8c8d'
  },
  emptyEvents: {
    paddingVertical: 30,
    alignItems: 'center'
  },
  emptyEventsText: {
    fontSize: 16,
    color: '#95a5a6',
    fontStyle: 'italic'
  }
});

export default App;

这段React Native代码实现了一个功能完整的车辆保养日历组件,其核心原理基于React的状态管理和日期处理机制。getDaysInMonth函数通过日期运算计算当前月份的天数,通过创建上个月和下个月的日期填充日历网格,确保日历始终显示完整的6行7列布局。这种日期计算方式在鸿蒙系统的日历组件中具有重要意义,鸿蒙设备的屏幕尺寸和布局要求使得这种动态填充机制能够自适应不同设备的显示需求。

从鸿蒙系统适配的角度来看,该代码充分利用了React Native的跨平台特性,在鸿蒙设备上能够获得原生级的性能表现。鸿蒙系统的分布式数据管理能力能够与React的状态提升概念良好结合,events状态作为单一数据源确保了数据一致性。日期选择功能通过selectedDate状态管理当前选中日期,通过条件渲染实现不同日期的样式变化,这种交互设计符合鸿蒙系统的UI规范。

日历导航功能通过changeMonth函数实现月份切换,通过setMonth方法更新currentMonth状态,这种状态管理方式在鸿蒙系统的应用生命周期管理中非常重要,能够确保组件在不同使用场景下的状态一致性。日期格式化函数formatDate通过padStart方法确保日期字符串的格式统一性,这种数据处理方式在鸿蒙系统的数据同步机制中具有实际价值。

UI布局采用ScrollView作为根容器,确保内容在不同屏幕尺寸设备上的可滚动性。日历头部通过TouchableOpacity组件实现月份切换按钮,通过条件渲染显示当前月份和年份,这种交互设计符合鸿蒙系统的用户界面规范。星期标题区域通过map方法遍历weekdays数组生成7个标签,通过条件渲染实现周末标签的特殊样式,这种信息架构在鸿蒙系统的健康数据展示中非常常见。

日历主体区域通过map方法遍历days数组生成42个日期单元格,每个单元格根据isCurrentMonth状态应用不同样式,通过hasEvents函数检查日期是否有事件并显示指示器图标。这种条件渲染模式在鸿蒙系统的动态UI构建中具有优势,能够根据数据变化实时更新界面。日期选择功能通过onPress回调函数更新selectedDate状态,通过条件渲染实现选中日期的高亮显示,这种交互设计符合鸿蒙系统的UI规范。

事件详情区域通过条件渲染显示选中日期的保养安排,通过map方法遍历events数组生成事件列表。这种信息架构在鸿蒙系统的健康数据展示中非常常见,能够帮助用户快速获取关键信息。事件指示器图标通过Image组件加载远程图片资源,这种远程图片加载方式在鸿蒙系统的网络图片缓存机制中能够获得良好的性能表现。

从鸿蒙系统的技术特性来看,该代码通过React Native的声明式编程范式,将复杂的日期逻辑抽象为简单的状态转换。鸿蒙系统的ArkUI框架同样强调声明式UI开发,这种设计思想的一致性使得应用在鸿蒙设备上能够获得接近原生的性能表现。组件的生命周期管理与鸿蒙系统的应用管理机制保持一致,能够在应用前后台切换时正确处理状态更新。


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

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

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

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

相关推荐
赵财猫._.8 小时前
React Native鸿蒙开发实战(九):复杂业务场景实战与架构设计
react native·react.js·harmonyos
ifeng09188 小时前
uniapp开发鸿蒙:跨端兼容与条件编译实战
华为·uni-app·harmonyos
ifeng09188 小时前
uniapp开发鸿蒙:常见问题与踩坑指南
华为·uni-app·harmonyos
2401_860494708 小时前
如何在React Native中实现鸿蒙跨平台开发任务列表页面在很多应用中都是一个常见的需求,比如待办事项列表、购物车列表等
react native·react.js·harmonyos
XHW___0018 小时前
鸿蒙webrtc编译
华为·webrtc·harmonyos
零Suger8 小时前
React Router v7数据模式使用指南
javascript·笔记·react.js
狮子也疯狂8 小时前
【智能编程助手】| 鸿蒙系统下的AI辅助编程实战
人工智能·华为·harmonyos
_Kayo_8 小时前
React 动态显示icon
前端·react.js·react