HarmonyOS APP开发---花信植物花期记录App

2 阅读7分钟

如果你想做一个植物花期记录App,需要用到Preferences日历存储

你可以在鸿蒙应用市场搜索"花信"下载体验,看看一个完整的植物花期记录App长什么样。先去下载看看效果,再回来跟着文章写代码。


写在前面

"花信"这个词,古人用来指代花开的信号。二十四番花信风,从小寒到谷雨,每一候都有一种花开放。我给这个App起名"花信",就是想做一个能帮你记录各种植物什么时候开花、什么时候结果、什么时候该采收的工具。

如果你种过花、种过菜,你就知道花期管理有多重要。错过了授粉窗口,果实就结不好;错过了采收时间,果子就过熟了。有了"花信",你可以提前记录每种植物的花期规律,到了时间系统会提醒你该做什么了。

这篇文章,我们就来聊聊怎么用鸿蒙的Preferences API来实现日历数据存储和花期计算。


这篇文章聊什么

今天要聊三个核心功能:

  1. 植物品种管理 -- 记录你种了哪些植物,每种植物的花期信息
  2. 花期追踪 -- 在日历上标记每种植物的开花、结果、采收时间
  3. 采收管理 -- 根据花期计算最佳采收时间

日历数据结构的设计是这篇文章的重点。我们需要一种高效的方式来存储和查询"某一天有哪些植物进入了什么阶段"。


第一步:植物品种和花期数据结构

先想想,一种植物的花期信息需要记录哪些东西。

// React 端:植物品种数据
const plant = {
  id: 'plant_001',
  name: '番茄',
  variety: '大番茄-粉果',
  plantDate: '2024-03-01',
  // 花期信息(基于种植日期推算)
  floweringDaysAfterPlant: 60,    // 种植后多少天开花
  fruitingDaysAfterPlant: 75,     // 种植后多少天结果
  harvestDaysAfterPlant: 90,      // 种植后多少天可以采收
  harvestDuration: 30,            // 采收持续天数
  notes: '需要充足阳光,每天浇水'
};

你看,这里的花期信息不是用具体日期存储的,而是用"种植后多少天"来表示。这样做的好处是:不管你什么时候种的,都能推算出开花和采收的日期。比如你3月1日种的番茄,开花日期就是3月1日加60天,大约4月30日。

在ArkTS里:

// ArkTS 端:植物品种接口
interface PlantInfo {
  id: string;
  name: string;
  variety: string;
  plantDate: string;               // 种植日期 YYYY-MM-DD
  floweringDays: number;            // 种植后多少天开花
  fruitingDays: number;             // 种植后多少天结果
  harvestDays: number;              // 种植后多少天可采收
  harvestDuration: number;          // 采收持续天数
  notes: string;
}

// ArkTS 端:花期阶段枚举
enum BloomStage {
  PLANTED = 'planted',             // 已种植
  GROWING = 'growing',             // 生长中
  FLOWERING = 'flowering',         // 开花中
  FRUITING = 'fruiting',           // 结果中
  HARVESTABLE = 'harvestable',     // 可采收
  HARVEST_ENDED = 'harvest_ended'  // 采收结束
}

保存植物数据:

import { preferences } from '@kit.ArkData';

// ArkTS 端:保存植物列表
async function savePlants(
  preferencesStore: preferences.Preferences,
  plants: PlantInfo[]
): Promise<void> {
  await preferencesStore.put('plant_list', JSON.stringify(plants));
  await preferencesStore.flush();
}

// ArkTS 端:读取植物列表
async function loadPlants(
  preferencesStore: preferences.Preferences
): Promise<PlantInfo[]> {
  const plantsJson = await preferencesStore.get('plant_list', '[]');
  return JSON.parse(plantsJson as string);
}

第二步:花期计算 -- 从天数推算日期

花期计算的核心逻辑是:给定种植日期和"种植后多少天"的参数,计算出具体的日期。

在React端:

// React 端:计算花期日期
function calculateBloomDates(plant) {
  const plantDate = new Date(plant.plantDate);
  const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数

  return {
    floweringDate: new Date(plantDate.getTime() + plant.floweringDays * oneDay)
      .toISOString().split('T')[0],
    fruitingDate: new Date(plantDate.getTime() + plant.fruitingDays * oneDay)
      .toISOString().split('T')[0],
    harvestStartDate: new Date(plantDate.getTime() + plant.harvestDays * oneDay)
      .toISOString().split('T')[0],
    harvestEndDate: new Date(
      plantDate.getTime() + (plant.harvestDays + plant.harvestDuration) * oneDay
    ).toISOString().split('T')[0]
  };
}

// React 端:获取当前阶段
function getCurrentStage(plant) {
  const now = new Date();
  const dates = calculateBloomDates(plant);
  const plantDate = new Date(plant.plantDate);

  if (now < plantDate) return { stage: 'planned', label: '计划种植' };
  if (now < new Date(dates.floweringDate)) return { stage: 'growing', label: '生长中' };
  if (now < new Date(dates.fruitingDate)) return { stage: 'flowering', label: '开花中' };
  if (now < new Date(dates.harvestStartDate)) return { stage: 'fruiting', label: '结果中' };
  if (now < new Date(dates.harvestEndDate)) return { stage: 'harvestable', label: '可采收' };
  return { stage: 'harvest_ended', label: '采收结束' };
}

在ArkTS里,逻辑完全一样,只是类型标注更严格:

// ArkTS 端:计算花期日期
function calculateBloomDates(plant: PlantInfo): {
  floweringDate: string;
  fruitingDate: string;
  harvestStartDate: string;
  harvestEndDate: string;
} {
  const plantDate = new Date(plant.plantDate);
  const oneDay = 24 * 60 * 60 * 1000;

  const floweringDate = new Date(plantDate.getTime() + plant.floweringDays * oneDay);
  const fruitingDate = new Date(plantDate.getTime() + plant.fruitingDays * oneDay);
  const harvestStartDate = new Date(plantDate.getTime() + plant.harvestDays * oneDay);
  const harvestEndDate = new Date(
    plantDate.getTime() + (plant.harvestDays + plant.harvestDuration) * oneDay
  );

  return {
    floweringDate: formatDate(floweringDate),
    fruitingDate: formatDate(fruitingDate),
    harvestStartDate: formatDate(harvestStartDate),
    harvestEndDate: formatDate(harvestEndDate)
  };
}

// ArkTS 端:日期格式化辅助函数
function formatDate(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

// ArkTS 端:获取植物当前阶段
function getCurrentStage(plant: PlantInfo): { stage: string; label: string } {
  const now = new Date();
  const dates = calculateBloomDates(plant);
  const plantDate = new Date(plant.plantDate);

  if (now < plantDate) {
    return { stage: 'planned', label: '计划种植' };
  }
  if (now < new Date(dates.floweringDate)) {
    return { stage: 'growing', label: '生长中' };
  }
  if (now < new Date(dates.fruitingDate)) {
    return { stage: 'flowering', label: '开花中' };
  }
  if (now < new Date(dates.harvestStartDate)) {
    return { stage: 'fruiting', label: '结果中' };
  }
  if (now < new Date(dates.harvestEndDate)) {
    return { stage: 'harvestable', label: '可采收' };
  }
  return { stage: 'harvest_ended', label: '采收结束' };
}

getCurrentStage函数的逻辑是一系列日期比较。从最早的时间开始判断,如果当前时间还没到开花日期,那就是"生长中";如果到了开花日期但还没到结果日期,那就是"开花中",以此类推。这种级联判断的方式简单直观,虽然不是最优美的写法,但可读性很好。


第三步:日历数据结构设计

日历数据是这个App的核心。我们需要一种方式,能够快速查询"某一天有哪些植物进入了什么阶段"。

最直观的方式是:以日期为key,存储当天有事件的植物列表。

// React 端:日历事件数据结构
// key 是日期 'YYYY-MM-DD',value 是当天的事件列表
const calendarEvents = {
  '2024-04-30': [
    { plantId: 'plant_001', plantName: '番茄', event: '开花' },
    { plantId: 'plant_002', plantName: '黄瓜', event: '可采收' }
  ],
  '2024-05-15': [
    { plantId: 'plant_001', plantName: '番茄', event: '结果' }
  ]
};

在ArkTS里:

// ArkTS 端:日历事件接口
interface CalendarEvent {
  plantId: string;
  plantName: string;
  event: string;     // '开花' / '结果' / '可采收' / '采收结束'
  color: string;    // 事件颜色标识
}

// ArkTS 端:日历数据类型(日期到事件列表的映射)
type CalendarData = Record<string, CalendarEvent[]>;

生成日历数据:

// ArkTS 端:根据植物列表生成日历数据
function generateCalendarData(plants: PlantInfo[]): CalendarData {
  const calendar: CalendarData = {};

  plants.forEach((plant: PlantInfo) => {
    const dates = calculateBloomDates(plant);

    // 添加开花事件
    addCalendarEvent(calendar, dates.floweringDate, {
      plantId: plant.id,
      plantName: plant.name,
      event: '开花',
      color: '#e91e63'
    });

    // 添加结果事件
    addCalendarEvent(calendar, dates.fruitingDate, {
      plantId: plant.id,
      plantName: plant.name,
      event: '结果',
      color: '#ff9800'
    });

    // 添加可采收事件
    addCalendarEvent(calendar, dates.harvestStartDate, {
      plantId: plant.id,
      plantName: plant.name,
      event: '可采收',
      color: '#4caf50'
    });

    // 添加采收结束事件
    addCalendarEvent(calendar, dates.harvestEndDate, {
      plantId: plant.id,
      plantName: plant.name,
      event: '采收结束',
      color: '#9e9e9e'
    });
  });

  return calendar;
}

// ArkTS 端:向日历添加事件
function addCalendarEvent(
  calendar: CalendarData,
  date: string,
  event: CalendarEvent
): void {
  if (calendar[date]) {
    calendar[date].push(event);
  } else {
    calendar[date] = [event];
  }
}

保存和读取日历数据:

// ArkTS 端:保存日历数据
async function saveCalendarData(
  preferencesStore: preferences.Preferences,
  calendar: CalendarData
): Promise<void> {
  await preferencesStore.put('calendar_data', JSON.stringify(calendar));
  await preferencesStore.flush();
}

// ArkTS 端:读取日历数据
async function loadCalendarData(
  preferencesStore: preferences.Preferences
): Promise<CalendarData> {
  const calendarJson = await preferencesStore.get('calendar_data', '{}');
  return JSON.parse(calendarJson as string);
}

第四步:日历UI展示

有了日历数据,我们需要在UI上展示。当某一天有事件的时候,在日期数字下方显示一个彩色小圆点。

// ArkTS 端:花期日历组件
@Component
struct BloomCalendar {
  @State calendarData: CalendarData = {};
  @State currentYear: number = 2024;
  @State currentMonth: number = 6;
  @State selectedDate: string = '';
  @State selectedEvents: CalendarEvent[] = [];

  // 获取某个月的天数
  private getDaysInMonth(year: number, month: number): number {
    return new Date(year, month, 0).getDate();
  }

  // 获取某天的事件
  private getEventsForDate(date: string): CalendarEvent[] {
    return this.calendarData[date] || [];
  }

  // 获取某天的事件颜色点
  private getEventDots(date: string): string[] {
    const events = this.getEventsForDate(date);
    return events.map((e: CalendarEvent) => e.color);
  }

  // 选中某天
  private selectDate(date: string) {
    this.selectedDate = date;
    this.selectedEvents = this.getEventsForDate(date);
  }

  build() {
    Column() {
      // 月份导航
      Row() {
        Button('<')
          .width(40)
          .height(40)
          .onClick(() => {
            if (this.currentMonth === 1) {
              this.currentMonth = 12;
              this.currentYear -= 1;
            } else {
              this.currentMonth -= 1;
            }
          })
        Text(`${this.currentYear}${this.currentMonth}月`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        Button('>')
          .width(40)
          .height(40)
          .onClick(() => {
            if (this.currentMonth === 12) {
              this.currentMonth = 1;
              this.currentYear += 1;
            } else {
              this.currentMonth += 1;
            }
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .margin({ bottom: 12 })

      // 星期标题
      Row() {
        ForEach(['日', '一', '二', '三', '四', '五', '六'], (day: string) => {
          Text(day)
            .fontSize(14)
            .fontColor('#999')
            .width('14.28%')
            .textAlign(TextAlign.Center)
        })
      }
      .width('100%')
      .margin({ bottom: 8 })

      // 日历网格
      Grid() {
        ForEach(
          Array.from({ length: this.getDaysInMonth(this.currentYear, this.currentMonth) }),
          (_: Object, index: number) => {
            const day = index + 1;
            const dateStr = `${this.currentYear}-${String(this.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
            const dots = this.getEventDots(dateStr);
            const isSelected = this.selectedDate === dateStr;

            GridItem() {
              Column() {
                // 日期数字
                Text(day.toString())
                  .fontSize(14)
                  .fontColor(isSelected ? '#ffffff' : '#333')
                  .width(32)
                  .height(32)
                  .textAlign(TextAlign.Center)
                  .borderRadius(16)
                  .backgroundColor(isSelected ? '#4caf50' : 'transparent')

                // 事件圆点
                if (dots.length > 0) {
                  Row() {
                    ForEach(dots.slice(0, 3), (color: string) => {
                      Text('')
                        .width(6)
                        .height(6)
                        .borderRadius(3)
                        .backgroundColor(color)
                        .margin({ right: 2 })
                    })
                  }
                  .margin({ top: 2 })
                }
              }
              .alignItems(HorizontalAlign.Center)
              .onClick(() => {
                this.selectDate(dateStr);
              })
            }
          }
        )
      }
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
      .columnsGap(4)
      .rowsGap(4)
      .width('100%')
      .margin({ bottom: 16 })

      // 选中日期的事件列表
      if (this.selectedEvents.length > 0) {
        Column() {
          Text(`${this.selectedDate} 事件`)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 8 })

          ForEach(this.selectedEvents, (event: CalendarEvent) => {
            Row() {
              Text('')
                .width(10)
                .height(10)
                .borderRadius(5)
                .backgroundColor(event.color)
              Text(`${event.plantName} - ${event.event}`)
                .fontSize(14)
                .margin({ left: 8 })
            }
            .width('100%')
            .padding(8)
            .margin({ bottom: 4 })
            .backgroundColor('#f5f5f5')
            .borderRadius(6)
          })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#ffffff')
        .borderRadius(8)
      }
    }
    .padding(16)
  }
}

这个日历组件有几个值得聊的地方。

事件圆点最多显示3个(dots.slice(0, 3)),因为如果某一天有太多事件,圆点会挤在一起不好看。如果超过3个,后面的就不显示了,用户可以点击日期查看完整列表。

选中日期后,下方会显示该日期的所有事件。每个事件前面有一个彩色圆点,颜色和日历上的圆点对应,这样用户能直观地知道每个事件代表什么。

月份导航用了两个按钮,点击左箭头显示上个月,点击右箭头显示下个月。跨年的时候自动处理年份的增减。


第五步:采收管理

采收管理是花期记录的最终目的。当植物进入采收期后,我们需要记录每次采收的数量和品质。

// ArkTS 端:采收记录接口
interface HarvestRecord {
  id: string;
  plantId: string;
  plantName: string;
  harvestDate: string;
  quantity: number;       // 采收数量
  unit: string;           // 单位:个、斤、克等
  quality: string;        // 品质:优、良、中、差
  notes: string;
}

// ArkTS 端:保存采收记录
async function saveHarvestRecord(
  preferencesStore: preferences.Preferences,
  record: HarvestRecord
): Promise<void> {
  const recordsJson = await preferencesStore.get('harvest_records', '[]');
  const records: HarvestRecord[] = JSON.parse(recordsJson as string);
  records.push(record);
  await preferencesStore.put('harvest_records', JSON.stringify(records));
  await preferencesStore.flush();
}

// ArkTS 端:计算某植物的总采收量
function getTotalHarvest(
  records: HarvestRecord[],
  plantId: string
): number {
  return records
    .filter((r: HarvestRecord) => r.plantId === plantId)
    .reduce((sum: number, r: HarvestRecord) => sum + r.quantity, 0);
}

流程图

flowchart TD
    A[用户添加植物品种] --> B[填写种植日期和花期参数]
    B --> C[保存到 Preferences]
    C --> D[自动计算开花/结果/采收日期]
    D --> E[生成日历事件数据]
    E --> F[在日历上标记关键日期]

    G[时间推进] --> H{到达开花日期?}
    H -- 是 --> I[标记为开花中]
    I --> J{到达结果日期?}
    J -- 是 --> K[标记为结果中]
    K --> L{到达采收日期?}
    L -- 是 --> M[提醒用户采收]
    M --> N[记录采收数据]
    N --> O{采收期结束?}
    O -- 否 --> N
    O -- 是 --> P[标记采收结束]

React vs ArkTS 对比表

功能React (Web)ArkTS (鸿蒙)
日期加减天数getTime() + days * 86400000getTime() + days * 86400000(一致)
日期格式化toISOString().split('T')[0]手动拼接年月日
日期比较< > 运算符直接比较Date< > 运算符直接比较Date(一致)
级联判断if/else if 链if/else if 链(一致)
日历网格CSS GridGrid 组件
事件圆点span + background-colorText + borderRadius + backgroundColor
月份切换setState 更新@State 自动刷新

总结

这篇文章我们实现了"花信"App的核心功能:

  1. 植物品种管理 -- 用"种植后多少天"的方式存储花期参数,而不是硬编码日期
  2. 花期计算 -- 根据种植日期和天数参数,推算出开花、结果、采收的具体日期
  3. 日历数据存储 -- 以日期为key的事件映射,方便快速查询某一天有哪些事件
  4. 采收管理 -- 记录每次采收的数据,支持按植物汇总统计

"种植后多少天"这种相对时间的设计是这个App的关键。它让数据具有通用性,不管你什么时候种,都能正确推算花期。如果你直接存具体日期,那换一批种植的时候,所有日期都要重新算。

如果你对种植和花期管理感兴趣,去鸿蒙应用市场搜索"花信"下载体验吧。