如果你想做一个植物花期记录App,需要用到Preferences日历存储
你可以在鸿蒙应用市场搜索"花信"下载体验,看看一个完整的植物花期记录App长什么样。先去下载看看效果,再回来跟着文章写代码。
写在前面
"花信"这个词,古人用来指代花开的信号。二十四番花信风,从小寒到谷雨,每一候都有一种花开放。我给这个App起名"花信",就是想做一个能帮你记录各种植物什么时候开花、什么时候结果、什么时候该采收的工具。
如果你种过花、种过菜,你就知道花期管理有多重要。错过了授粉窗口,果实就结不好;错过了采收时间,果子就过熟了。有了"花信",你可以提前记录每种植物的花期规律,到了时间系统会提醒你该做什么了。
这篇文章,我们就来聊聊怎么用鸿蒙的Preferences API来实现日历数据存储和花期计算。
这篇文章聊什么
今天要聊三个核心功能:
- 植物品种管理 -- 记录你种了哪些植物,每种植物的花期信息
- 花期追踪 -- 在日历上标记每种植物的开花、结果、采收时间
- 采收管理 -- 根据花期计算最佳采收时间
日历数据结构的设计是这篇文章的重点。我们需要一种高效的方式来存储和查询"某一天有哪些植物进入了什么阶段"。
第一步:植物品种和花期数据结构
先想想,一种植物的花期信息需要记录哪些东西。
// 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 * 86400000 | getTime() + days * 86400000(一致) |
| 日期格式化 | toISOString().split('T')[0] | 手动拼接年月日 |
| 日期比较 | < > 运算符直接比较Date | < > 运算符直接比较Date(一致) |
| 级联判断 | if/else if 链 | if/else if 链(一致) |
| 日历网格 | CSS Grid | Grid 组件 |
| 事件圆点 | span + background-color | Text + borderRadius + backgroundColor |
| 月份切换 | setState 更新 | @State 自动刷新 |
总结
这篇文章我们实现了"花信"App的核心功能:
- 植物品种管理 -- 用"种植后多少天"的方式存储花期参数,而不是硬编码日期
- 花期计算 -- 根据种植日期和天数参数,推算出开花、结果、采收的具体日期
- 日历数据存储 -- 以日期为key的事件映射,方便快速查询某一天有哪些事件
- 采收管理 -- 记录每次采收的数据,支持按植物汇总统计
"种植后多少天"这种相对时间的设计是这个App的关键。它让数据具有通用性,不管你什么时候种,都能正确推算花期。如果你直接存具体日期,那换一批种植的时候,所有日期都要重新算。
如果你对种植和花期管理感兴趣,去鸿蒙应用市场搜索"花信"下载体验吧。