React Native for OpenHarmony:Calendar 日程标记与事件管理

在这里插入图片描述

🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

在这里插入图片描述


description: 深入解析在 OpenHarmony 平台上实现日历日程标记功能的技术方案,包含标记系统设计、事件管理、性能优化策略
tags:

  • react-native
  • openharmony
  • calendar
  • event-management
    category: 移动开发

React Native for OpenHarmony:Calendar 日程标记与事件管理

概述

日程标记是日历组件的核心功能之一,它通过视觉提示让用户快速识别有特殊事件的日期。本文将详细讲解如何在 OpenHarmony 平台上实现高效的日程标记系统。

标记系统设计

数据结构设计

// types/event.ts
export enum EventType {
  MEETING = 'meeting',
  TASK = 'task',
  REMINDER = 'reminder',
  HOLIDAY = 'holiday',
}

export interface CalendarEvent {
  id: string;
  date: string;        // ISO 8601 格式
  title: string;
  type: EventType;
  color: string;
  description?: string;
  completed?: boolean;
}

export interface EventMarker {
  events: CalendarEvent[];
  hasOverflow: boolean;
  displayCount: number;
}

事件管理器

// utils/eventManager.ts
import { CalendarEvent, EventType, EventMarker } from '../types/event';

export class EventManager {
  private events: Map<string, CalendarEvent[]> = new Map();
  private readonly MAX_DISPLAY_DOTS = 3;

  /**
   * 添加事件
   */
  addEvent(event: CalendarEvent): void {
    const date = event.date;
    if (!this.events.has(date)) {
      this.events.set(date, []);
    }
    this.events.get(date)!.push(event);
    this.sortEventsByType(date);
  }

  /**
   * 批量添加事件
   */
  addEvents(events: CalendarEvent[]): void {
    events.forEach(event => this.addEvent(event));
  }

  /**
   * 获取指定日期的事件
   */
  getEvents(date: string): CalendarEvent[] {
    return this.events.get(date) || [];
  }

  /**
   * 获取日期标记信息
   */
  getMarker(date: string): EventMarker {
    const events = this.getEvents(date);
    const displayCount = Math.min(events.length, this.MAX_DISPLAY_DOTS);

    return {
      events: events.slice(0, this.MAX_DISPLAY_DOTS),
      hasOverflow: events.length > this.MAX_DISPLAY_DOTS,
      displayCount,
    };
  }

  /**
   * 删除事件
   */
  removeEvent(eventId: string): boolean {
    for (const [date, events] of this.events.entries()) {
      const index = events.findIndex(e => e.id === eventId);
      if (index !== -1) {
        events.splice(index, 1);
        if (events.length === 0) {
          this.events.delete(date);
        }
        return true;
      }
    }
    return false;
  }

  /**
   * 获取有事件的日期列表
   */
  getEventDates(): string[] {
    return Array.from(this.events.keys()).sort();
  }

  /**
   * 清空所有事件
   */
  clear(): void {
    this.events.clear();
  }

  /**
   * 按类型排序事件(优先级高的在前)
   */
  private sortEventsByType(date: string): void {
    const events = this.events.get(date);
    if (!events) return;

    const typePriority: Record<EventType, number> = {
      [EventType.HOLIDAY]: 0,
      [EventType.MEETING]: 1,
      [EventType.TASK]: 2,
      [EventType.REMINDER]: 3,
    };

    events.sort((a, b) => typePriority[a.type] - typePriority[b.type]);
  }
}

颜色配置

// constants/colors.ts
import { EventType } from '../types/event';

export const EVENT_COLORS: Record<EventType, string> = {
  [EventType.MEETING]: '#9C27B0',
  [EventType.TASK]: '#2196F3',
  [EventType.REMINDER]: '#FF5722',
  [EventType.HOLIDAY]: '#4CAF50',
};

export const EVENT_LABELS: Record<EventType, { zh: string; en: string }> = {
  [EventType.MEETING]: { zh: '会议', en: 'Meeting' },
  [EventType.TASK]: { zh: '任务', en: 'Task' },
  [EventType.REMINDER]: { zh: '提醒', en: 'Reminder' },
  [EventType.HOLIDAY]: { zh: '假期', en: 'Holiday' },
};

组件实现

日历组件

// components/EventCalendar.tsx
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  Alert,
} from 'react-native';
import { CalendarEvent, EventType } from '../types/event';
import { EventManager } from '../utils/eventManager';
import { EVENT_COLORS, EVENT_LABELS } from '../constants/colors';
import { DateUtils } from '../utils/dateUtils';

interface EventCalendarProps {
  initialEvents?: CalendarEvent[];
  onEventPress?: (event: CalendarEvent) => void;
  onDateSelect?: (date: string) => void;
}

export const EventCalendar: React.FC<EventCalendarProps> = ({
  initialEvents = [],
  onEventPress,
  onDateSelect,
}) => {
  const [currentDate, setCurrentDate] = useState(new Date());
  const [selectedDate, setSelectedDate] = useState<string>('');
  const [eventManager] = useState(() => new EventManager());

  // 初始化事件
  useEffect(() => {
    if (initialEvents.length > 0) {
      eventManager.addEvents(initialEvents);
    }
  }, [initialEvents, eventManager]);

  // 获取月份数据
  const monthData = useMemo(() => {
    return DateUtils.getMonthData(currentDate);
  }, [currentDate]);

  // 切换月份
  const changeMonth = useCallback((offset: number) => {
    setCurrentDate((prev) => {
      const newDate = new Date(prev);
      newDate.setMonth(newDate.getMonth() + offset);
      return newDate;
    });
  }, []);

  // 选择日期
  const selectDate = useCallback((dateString: string) => {
    setSelectedDate(dateString);
    onDateSelect?.(dateString);
  }, [onDateSelect]);

  // 渲染标记点
  const renderEventDots = useCallback((dateString: string) => {
    const marker = eventManager.getMarker(dateString);

    if (marker.displayCount === 0) return null;

    return (
      <View style={styles.dotsContainer}>
        {marker.events.map((event, index) => (
          <View
            key={event.id}
            style={[
              styles.eventDot,
              { backgroundColor: event.color }
            ]}
          />
        ))}
        {marker.hasOverflow && (
          <View style={styles.moreDot}>
            <Text style={styles.moreDotText}>+</Text>
          </View>
        )}
      </View>
    );
  }, [eventManager]);

  // 获取选中日期的事件
  const selectedEvents = useMemo(() => {
    return selectedDate ? eventManager.getEvents(selectedDate) : [];
  }, [selectedDate, eventManager]);

  // 渲染日期网格
  const calendarGrid = useMemo(() => {
    const rows: React.ReactNode[] = [];

    for (let i = 0; i < 6; i++) {
      const rowDays = monthData.days.slice(i * 7, (i + 1) * 7);

      rows.push(
        <View key={i} style={styles.weekRow}>
          {rowDays.map((day, index) => {
            const isSelected = selectedDate === day.dateString;
            const hasEvents = eventManager.getMarker(day.dateString).displayCount > 0;

            return (
              <TouchableOpacity
                key={index}
                style={[
                  styles.dayCell,
                  !day.isCurrentMonth && styles.dayCellDisabled,
                  day.isToday && styles.dayCellToday,
                  isSelected && styles.dayCellSelected,
                  hasEvents && styles.dayCellWithEvents,
                ]}
                onPress={() => day.isCurrentMonth && selectDate(day.dateString)}
                disabled={!day.isCurrentMonth}
                activeOpacity={0.7}
              >
                <Text
                  style={[
                    styles.dayText,
                    !day.isCurrentMonth && styles.dayTextDisabled,
                    day.isToday && styles.dayTextToday,
                    isSelected && styles.dayTextSelected,
                  ]}
                >
                  {day.day}
                </Text>
                {day.isCurrentMonth && renderEventDots(day.dateString)}
              </TouchableOpacity>
            );
          })}
        </View>
      );
    }

    return rows;
  }, [monthData.days, selectedDate, eventManager, selectDate, renderEventDots]);

  return (
    <ScrollView style={styles.container}>
      {/* 日历卡片 */}
      <View style={styles.calendarCard}>
        {/* 月份导航 */}
        <View style={styles.monthNavigation}>
          <TouchableOpacity
            style={styles.navButton}
            onPress={() => changeMonth(-1)}
          >
            <Text style={styles.navButtonText}></Text>
          </TouchableOpacity>
          <Text style={styles.monthText}>
            {monthData.year}{DateUtils.getMonthName(monthData.month)}
          </Text>
          <TouchableOpacity
            style={styles.navButton}
            onPress={() => changeMonth(1)}
          >
            <Text style={styles.navButtonText}></Text>
          </TouchableOpacity>
        </View>

        {/* 星期标题 */}
        <View style={styles.weekHeader}>
          {DateUtils.getWeekDays().map((day, index) => (
            <View key={index} style={styles.weekDayCell}>
              <Text style={styles.weekDayText}>{day}</Text>
            </View>
          ))}
        </View>

        {/* 日期网格 */}
        <View style={styles.daysContainer}>{calendarGrid}</View>
      </View>

      {/* 选中日期的事件列表 */}
      {selectedDate && (
        <View style={styles.eventListCard}>
          <Text style={styles.eventListTitle}>
            {selectedDate} 的日程
          </Text>
          {selectedEvents.length > 0 ? (
            selectedEvents.map((event) => (
              <TouchableOpacity
                key={event.id}
                style={styles.eventItem}
                onPress={() => onEventPress?.(event)}
              >
                <View
                  style={[
                    styles.eventTypeDot,
                    { backgroundColor: event.color }
                  ]}
                />
                <View style={styles.eventContent}>
                  <Text style={styles.eventTitle}>{event.title}</Text>
                  <Text
                    style={[
                      styles.eventType,
                      { color: event.color }
                    ]}
                  >
                    {EVENT_LABELS[event.type].zh}
                  </Text>
                </View>
              </TouchableOpacity>
            ))
          ) : (
            <Text style={styles.noEventText}>暂无日程安排</Text>
          )}
        </View>
      )}

      {/* 事件类型说明 */}
      <View style={styles.legendCard}>
        <Text style={styles.legendTitle}>事件类型</Text>
        {Object.entries(EventType).map(([_, type]) => (
          <View key={type} style={styles.legendItem}>
            <View
              style={[
                styles.legendDot,
                { backgroundColor: EVENT_COLORS[type] }
              ]}
            />
            <Text style={styles.legendLabel}>
              {EVENT_LABELS[type].zh}
            </Text>
          </View>
        ))}
      </View>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  calendarCard: {
    backgroundColor: '#fff',
    borderRadius: 16,
    margin: 16,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  monthNavigation: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 16,
  },
  monthText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#1a1a1a',
  },
  navButton: {
    width: 36,
    height: 36,
    borderRadius: 18,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  navButtonText: {
    fontSize: 22,
    color: '#007AFF',
    fontWeight: '600',
  },
  weekHeader: {
    flexDirection: 'row',
    marginBottom: 8,
  },
  weekDayCell: {
    flex: 1,
    height: 32,
    justifyContent: 'center',
    alignItems: 'center',
  },
  weekDayText: {
    fontSize: 13,
    fontWeight: '600',
    color: '#666',
  },
  daysContainer: {
    marginTop: 4,
  },
  weekRow: {
    flexDirection: 'row',
    marginBottom: 4,
  },
  dayCell: {
    flex: 1,
    minHeight: 52,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 8,
    margin: 1,
    position: 'relative',
  },
  dayCellDisabled: {
    opacity: 0.3,
  },
  dayCellToday: {
    borderWidth: 2,
    borderColor: '#007AFF',
  },
  dayCellSelected: {
    backgroundColor: '#F0F8FF',
    borderWidth: 1,
    borderColor: '#007AFF',
  },
  dayCellWithEvents: {
    paddingBottom: 4,
  },
  dayText: {
    fontSize: 16,
    color: '#1a1a1a',
    fontWeight: '500',
    marginBottom: 2,
  },
  dayTextDisabled: {
    color: '#999',
  },
  dayTextToday: {
    color: '#007AFF',
    fontWeight: '700',
  },
  dayTextSelected: {
    color: '#007AFF',
    fontWeight: '700',
  },
  dotsContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    gap: 2,
  },
  eventDot: {
    width: 5,
    height: 5,
    borderRadius: 2.5,
  },
  moreDot: {
    width: 14,
    height: 14,
    borderRadius: 7,
    backgroundColor: '#E0E0E0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  moreDotText: {
    fontSize: 10,
    color: '#666',
    fontWeight: '700',
  },
  eventListCard: {
    backgroundColor: '#fff',
    borderRadius: 12,
    margin: 16,
    marginTop: 8,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 2,
  },
  eventListTitle: {
    fontSize: 16,
    fontWeight: '700',
    color: '#1a1a1a',
    marginBottom: 12,
  },
  eventItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  eventTypeDot: {
    width: 12,
    height: 12,
    borderRadius: 6,
    marginRight: 12,
  },
  eventContent: {
    flex: 1,
  },
  eventTitle: {
    fontSize: 15,
    fontWeight: '600',
    color: '#1a1a1a',
    marginBottom: 2,
  },
  eventType: {
    fontSize: 12,
  },
  noEventText: {
    fontSize: 14,
    color: '#999',
    textAlign: 'center',
    paddingVertical: 20,
  },
  legendCard: {
    backgroundColor: '#fff',
    borderRadius: 12,
    margin: 16,
    marginTop: 8,
    padding: 16,
  },
  legendTitle: {
    fontSize: 16,
    fontWeight: '700',
    color: '#1a1a1a',
    marginBottom: 12,
  },
  legendItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 10,
  },
  legendDot: {
    width: 12,
    height: 12,
    borderRadius: 6,
    marginRight: 10,
  },
  legendLabel: {
    fontSize: 14,
    color: '#333',
  },
});

性能优化策略

渲染优化

  1. 限制标记数量

    • 单日最多显示 3 个标记点
    • 超出部分显示 “+” 号
  2. 虚拟化处理

    • 使用 useMemo 缓存计算结果
    • 避免在渲染中创建新对象
  3. 事件数据分页

    // 分页加载事件数据
    const loadEventsForMonth = async (year: number, month: number) => {
      const startDate = `${year}-${String(month + 1).padStart(2, '0')}-01`;
      const endDate = `${year}-${String(month + 1).padStart(2, '0')}-31`;
    
      const events = await api.getEvents({ startDate, endDate });
      eventManager.addEvents(events);
    };
    

内存优化

  1. 及时清理

    // 组件卸载时清理
    useEffect(() => {
      return () => {
        eventManager.clear();
      };
    }, []);
    
  2. 懒加载事件

    • 只加载当前月份的事件
    • 切换月份时才加载新数据

OpenHarmony 适配要点

  1. 使用绝对定位

    • 标记点使用绝对定位避免布局抖动
  2. 避免过度嵌套

    • 减少组件层级提升性能
  3. 简化样式

    • 避免使用复杂阴影和渐变

总结

本文介绍了在 OpenHarmony 平台上实现日历日程标记系统的完整方案,包括数据结构设计、事件管理、性能优化等关键技术点。


相关资源

📕个人领域 :Linux/C++/java/AI
🚀 个人主页有点流鼻涕 · CSDN
💬 座右铭“向光而行,沐光而生。”

在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐