HarmonyOS APP开发---蜂勤录养蜂记录App日历标记

5 阅读7分钟

如果你想做一个养蜂记录App,需要用到Preferences和日历标记

欢迎大家去鸿蒙应用市场搜索下载「蜂勤录」,体验一下这款养蜂记录工具。你可以在里面标记巢脾状态、管理蜂王日历、统计产蜜量,把每个蜂箱的情况都掌握在手中。


写在前面

养蜂是一件需要耐心和细心的事情。一个蜂群的健康状况、蜂王的产卵情况、巢脾的储蜜进度,这些都需要定期检查和记录。如果你养了不止一箱蜂,那记录就更重要了 -- 哪箱蜂上脾快、哪箱蜂有分蜂迹象、哪箱蜂该取蜜了,全靠平时的记录来追踪。

蜂王的日历也是一个重要的功能。蜂王什么时候羽化、什么时候开始产卵、什么时候该更换,这些时间节点都需要记录。特别是蜂王的年龄 -- 一只蜂王一般能用1-2年,超过两年产卵力就会下降,需要及时更换。

还有产蜜量统计。每次取蜜多少斤、什么花期的蜜、品质如何,这些数据积累下来,你就能分析出一年中哪个花期产蜜最多、哪箱蜂产量最高。

这就是「蜂勤录」这个App要做的三件事:巢脾状态标记、蜂王日历、产蜜量统计。

这篇文章聊什么

这篇文章主要聊三件事:

  1. 日历标记的数据结构 -- 怎么在日历上标记有检查记录的日期,以及当天的巢脾状态。
  2. 蜂王信息的日历管理 -- 怎么记录蜂王的关键时间节点(羽化、产卵、更换),并在日历上显示。
  3. 产蜜量的统计计算 -- 怎么按月份、按蜂箱统计产蜜量,并计算年度总产量。

第一步:巢脾状态标记和日历

巢脾是蜂箱里蜜蜂用来储蜜、储粉和育虫的蜡质结构。每次开箱检查的时候,我们需要记录每个巢脾的状态:是储蜜脾、储粉脾、育虫脾还是空脾。

React版本:巢脾状态和日历

// 巢脾状态枚举
const COMB_STATUS = {
  HONEY: 'honey',       // 储蜜脾
  POLLEN: 'pollen',     // 储粉脾
  BROOD: 'brood',       // 育虫脾
  EMPTY: 'empty',       // 空脾
  CAPPED: 'capped',     // 封盖脾
};

// 日历标记数据结构
const defaultCalendarMarks = {
  '2024-03-15': { checked: true, frames: 10, honeyFrames: 4, broodFrames: 3, note: '蜂群状态良好' },
  '2024-03-22': { checked: true, frames: 10, honeyFrames: 5, broodFrames: 4, note: '储蜜增加' },
};

function CombCalendar() {
  const [marks, setMarks] = useState(() => {
    const saved = localStorage.getItem('comb_calendar');
    return saved ? JSON.parse(saved) : defaultCalendarMarks;
  });
  const [selectedDate, setSelectedDate] = useState('2024-03-15');

  const markDate = (date, data) => {
    setMarks(prev => {
      const updated = { ...prev, [date]: data };
      localStorage.setItem('comb_calendar', JSON.stringify(updated));
      return updated;
    });
  };

  // 获取当前月份的日历数据
  const getCalendarDays = (year, month) => {
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const days = [];
    for (let d = 1; d <= lastDay.getDate(); d++) {
      const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
      days.push({
        date: dateStr,
        day: d,
        isMarked: !!marks[dateStr],
        mark: marks[dateStr] || null,
      });
    }
    return days;
  };

  return (
    <div className="comb-calendar">
      <h3>巢脾检查日历</h3>
      {/* 日历网格和标记详情 */}
    </div>
  );
}

日历标记的数据结构是一个对象,key是日期字符串(YYYY-MM-DD格式),value是当天的检查记录。这种结构的好处是,查找某一天是否有记录只需要marks[dateStr],非常高效。

ArkTS版本:巢脾状态和日历

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

// 巢脾检查记录
interface CombCheckRecord {
  checked: boolean;
  totalFrames: number;    // 总巢框数
  honeyFrames: number;    // 储蜜框数
  pollenFrames: number;   // 储粉框数
  broodFrames: number;    // 育虫框数
  emptyFrames: number;    // 空框数
  note: string;
}

// 日历标记类型
type CalendarMarks = Record<string, CombCheckRecord>;

let dataStore: preferences.Preferences | null = null;

@Entry
@Component
struct CombCalendarPage {
  @State marks: CalendarMarks = {};
  @State currentYear: number = 2024;
  @State currentMonth: number = 2;  // 0-indexed, 2 = 三月
  @State selectedDate: string = '';
  @State editTotal: number = 10;
  @State editHoney: number = 4;
  @State editBrood: number = 3;
  @State editPollen: number = 1;
  @State editNote: string = '';

  async aboutToAppear() {
    const now = new Date();
    this.currentYear = now.getFullYear();
    this.currentMonth = now.getMonth();

    try {
      dataStore = await preferences.getPreferences(getContext(this), 'fengqinlu_store');
      const json = await dataStore.get('comb_calendar', '{}') as string;
      if (json !== '{}') {
        this.marks = JSON.parse(json) as CalendarMarks;
      }
    } catch (err) {
      console.error('加载巢脾数据失败', JSON.stringify(err));
    }
  }

  // 生成日历天数
  getCalendarDays(): { date: string; day: number; isMarked: boolean }[] {
    const firstDay = new Date(this.currentYear, this.currentMonth, 1);
    const lastDay = new Date(this.currentYear, this.currentMonth + 1, 0);
    const days: { date: string; day: number; isMarked: boolean }[] = [];

    // 前面的空白填充
    for (let i = 0; i < firstDay.getDay(); i++) {
      days.push({ date: '', day: 0, isMarked: false });
    }

    for (let d = 1; d <= lastDay.getDate(); d++) {
      const dateStr = `${this.currentYear}-${String(this.currentMonth + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
      days.push({
        date: dateStr,
        day: d,
        isMarked: !!this.marks[dateStr],
      });
    }

    return days;
  }

  // 保存检查记录
  async saveCheck() {
    if (this.selectedDate === '') return;

    const record: CombCheckRecord = {
      checked: true,
      totalFrames: this.editTotal,
      honeyFrames: this.editHoney,
      pollenFrames: this.editPollen,
      broodFrames: this.editBrood,
      emptyFrames: this.editTotal - this.editHoney - this.editPollen - this.editBrood,
      note: this.editNote,
    };

    this.marks = { ...this.marks, [this.selectedDate]: record };

    if (dataStore) {
      await dataStore.put('comb_calendar', JSON.stringify(this.marks));
      await dataStore.flush();
    }

    this.editNote = '';
  }

  // 选择日期
  selectDate(dateStr: string) {
    this.selectedDate = dateStr;
    if (this.marks[dateStr]) {
      const record = this.marks[dateStr];
      this.editTotal = record.totalFrames;
      this.editHoney = record.honeyFrames;
      this.editBrood = record.broodFrames;
      this.editPollen = record.pollenFrames;
      this.editNote = record.note;
    } else {
      this.editTotal = 10;
      this.editHoney = 4;
      this.editBrood = 3;
      this.editPollen = 1;
      this.editNote = '';
    }
  }

  // 切换月份
  prevMonth() {
    if (this.currentMonth === 0) {
      this.currentMonth = 11;
      this.currentYear--;
    } else {
      this.currentMonth--;
    }
  }

  nextMonth() {
    if (this.currentMonth === 11) {
      this.currentMonth = 0;
      this.currentYear++;
    } else {
      this.currentMonth++;
    }
  }

  build() {
    Scroll() {
      Column() {
        Text('巢脾检查日历')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })

        // 月份导航
        Row() {
          Button('<')
            .width(50)
            .onClick(() => { this.prevMonth(); })
          Text(`${this.currentYear}${this.currentMonth + 1}月`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
          Button('>')
            .width(50)
            .onClick(() => { this.nextMonth(); })
        }
        .width('90%')
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ bottom: 12 })

        // 星期标题
        Row() {
          ForEach(['日', '一', '二', '三', '四', '五', '六'], (day: string) => {
            Text(day)
              .fontSize(13)
              .fontColor('#999999')
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
          }, (day: string) => day)
        }
        .width('90%')
        .margin({ bottom: 4 })

        // 日历网格
        Grid() {
          ForEach(this.getCalendarDays(), (item: { date: string; day: number; isMarked: boolean }) => {
            if (item.date === '') {
              GridItem() {
                Text('')
                  .fontSize(14)
                  .width('100%')
                  .height(40)
              }
            } else {
              GridItem() {
                Text(`${item.day}`)
                  .fontSize(14)
                  .width('100%')
                  .height(40)
                  .textAlign(TextAlign.Center)
                  .fontColor(this.selectedDate === item.date ? '#FFFFFF' : '#333333')
                  .backgroundColor(
                    this.selectedDate === item.date ? '#2196F3' :
                    item.isMarked ? '#E8F5E9' : '#FFFFFF'
                  )
                  .borderRadius(20)
                  .onClick(() => { this.selectDate(item.date); })
              }
            }
          }, (item: { date: string; day: number }, index: number) => `${index}`)
        }
        .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
        .columnsGap(4)
        .rowsGap(4)
        .width('90%')
        .margin({ bottom: 20 })

        // 选中日期的编辑区域
        if (this.selectedDate !== '') {
          Column() {
            Text(`${this.selectedDate} 巢脾检查`)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .margin({ bottom: 12 })

            // 总巢框数
            Row() {
              Text('总巢框')
                .fontSize(14)
                .width(80)
              Text(`${this.editTotal} 框`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .margin({ left: 8, right: 8 })
              Slider({ min: 1, max: 20, step: 1, value: this.editTotal, style: SliderStyle.OutSet })
                .layoutWeight(1)
                .onChange((value: number) => { this.editTotal = value; })
            }
            .width('100%')
            .margin({ bottom: 8 })

            // 储蜜框
            Row() {
              Text('储蜜框')
                .fontSize(14)
                .width(80)
              Text(`${this.editHoney} 框`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF9800')
                .margin({ left: 8, right: 8 })
              Slider({ min: 0, max: this.editTotal, step: 1, value: this.editHoney, style: SliderStyle.OutSet })
                .layoutWeight(1)
                .onChange((value: number) => { this.editHoney = value; })
            }
            .width('100%')
            .margin({ bottom: 8 })

            // 育虫框
            Row() {
              Text('育虫框')
                .fontSize(14)
                .width(80)
              Text(`${this.editBrood} 框`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#4CAF50')
                .margin({ left: 8, right: 8 })
              Slider({ min: 0, max: this.editTotal, step: 1, value: this.editBrood, style: SliderStyle.OutSet })
                .layoutWeight(1)
                .onChange((value: number) => { this.editBrood = value; })
            }
            .width('100%')
            .margin({ bottom: 8 })

            // 储粉框
            Row() {
              Text('储粉框')
                .fontSize(14)
                .width(80)
              Text(`${this.editPollen} 框`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFD700')
                .margin({ left: 8, right: 8 })
              Slider({ min: 0, max: this.editTotal, step: 1, value: this.editPollen, style: SliderStyle.OutSet })
                .layoutWeight(1)
                .onChange((value: number) => { this.editPollen = value; })
            }
            .width('100%')
            .margin({ bottom: 8 })

            // 备注
            TextArea({ placeholder: '检查备注...', text: this.editNote })
              .width('100%')
              .height(60)
              .onChange((value: string) => { this.editNote = value; })
              .margin({ bottom: 12 })

            Button('保存检查记录')
              .width('100%')
              .backgroundColor('#4CAF50')
              .fontColor('#FFFFFF')
              .onClick(() => { this.saveCheck(); })
          }
          .width('90%')
          .padding(16)
          .backgroundColor('#F5F5F5')
          .borderRadius(12)
        }
      }
      .width('100%')
      .padding(20)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

日历网格用了Grid组件,columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')定义了7等分的列布局,正好对应一周7天。getCalendarDays()方法生成当月的天数数组,包括前面的空白填充(让第一天对齐到正确的星期位置)。

有标记的日期用浅绿色背景显示,选中的日期用蓝色背景 + 白色文字显示。点击某个日期后,下方会显示编辑区域,可以填写当天的巢脾检查数据。


第二步:蜂王日历

蜂王是蜂群的核心。记录蜂王的关键时间节点,对于蜂群管理非常重要。

React版本:蜂王日历

const defaultQueens = [
  {
    id: '1',
    hiveName: '1号箱',
    queenBreed: '意蜂',
    emergenceDate: '2024-03-01',    // 羽化日期
    firstEggDate: '2024-03-15',     // 首次产卵日期
    marked: true,                     // 是否标记
    markColor: '#FF0000',            // 标记颜色
    notes: '产卵积极',
  },
];

function QueenCalendar() {
  const [queens, setQueens] = useState(() => {
    const saved = localStorage.getItem('queen_calendar');
    return saved ? JSON.parse(saved) : defaultQueens;
  });

  const updateQueen = (id, field, value) => {
    setQueens(prev => {
      const updated = prev.map(q => q.id === id ? { ...q, [field]: value } : q);
      localStorage.setItem('queen_calendar', JSON.stringify(updated));
      return updated;
    });
  };

  // 计算蜂王年龄(天数)
  const getQueenAge = (emergenceDate) => {
    return Math.floor((Date.now() - new Date(emergenceDate).getTime()) / (1000 * 60 * 60 * 24));
  };

  return (
    <div className="queen-calendar">
      <h3>蜂王日历</h3>
      {queens.map(queen => (
        <div key={queen.id} className="queen-card">
          <h4>{queen.hiveName} - {queen.queenBreed}</h4>
          <p>蜂王年龄:{getQueenAge(queen.emergenceDate)}天</p>
          <p>羽化日期:{queen.emergenceDate}</p>
          <p>首次产卵:{queen.firstEggDate}</p>
        </div>
      ))}
    </div>
  );
}

ArkTS版本:蜂王日历

interface QueenInfo {
  id: string;
  hiveName: string;
  breed: string;
  emergenceDate: string;
  firstEggDate: string;
  marked: boolean;
  markColor: string;
  notes: string;
}

@Entry
@Component
struct QueenCalendarPage {
  @State queens: QueenInfo[] = [];
  @State selectedQueenId: string = '';
  @State editHiveName: string = '';
  @State editBreed: number = 0;
  @State editEmergence: string = '';
  @State editFirstEgg: string = '';
  @State editNotes: string = '';

  private breedOptions: string[] = ['意蜂', '中蜂', '卡蜂', '东北黑蜂'];

  async aboutToAppear() {
    if (!dataStore) return;
    try {
      const json = await dataStore.get('queen_calendar', '') as string;
      if (json !== '') {
        this.queens = JSON.parse(json) as QueenInfo[];
      }
    } catch (err) {
      console.error('加载蜂王数据失败', JSON.stringify(err));
    }
  }

  // 计算蜂王年龄
  getQueenAge(emergenceDate: string): number {
    return Math.floor((Date.now() - new Date(emergenceDate).getTime()) / (1000 * 60 * 60 * 24));
  }

  // 获取蜂王状态评估
  getQueenStatus(queen: QueenInfo): { text: string; color: string } {
    const age = this.getQueenAge(queen.emergenceDate);
    if (age < 30) return { text: '新王', color: '#4CAF50' };
    if (age < 365) return { text: '壮年', color: '#2196F3' };
    if (age < 730) return { text: '老龄', color: '#FF9800' };
    return { text: '建议更换', color: '#F44336' };
  }

  get selectedQueen(): QueenInfo | undefined {
    return this.queens.find((q: QueenInfo) => q.id === this.selectedQueenId);
  }

  // 添加蜂王
  async addQueen() {
    const queen: QueenInfo = {
      id: Date.now().toString(),
      hiveName: this.editHiveName,
      breed: this.breedOptions[this.editBreed],
      emergenceDate: this.editEmergence,
      firstEggDate: this.editFirstEgg,
      marked: false,
      markColor: '#FF0000',
      notes: this.editNotes,
    };

    this.queens = [...this.queens, queen];

    if (dataStore) {
      await dataStore.put('queen_calendar', JSON.stringify(this.queens));
      await dataStore.flush();
    }

    this.editHiveName = '';
    this.editEmergence = '';
    this.editFirstEgg = '';
    this.editNotes = '';
  }

  build() {
    Scroll() {
      Column() {
        Text('蜂王日历')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })

        // 添加蜂王表单
        Column() {
          Text('添加蜂王')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 12 })

          TextInput({ placeholder: '蜂箱名称', text: this.editHiveName })
            .width('100%')
            .onChange((value: string) => { this.editHiveName = value; })
            .margin({ bottom: 8 })

          Row() {
            Text('蜂种')
              .fontSize(14)
              .width(60)
            ForEach(this.breedOptions, (breed: string, index: number) => {
              Text(breed)
                .fontSize(13)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .backgroundColor(this.editBreed === index ? '#FF9800' : '#E0E0E0')
                .fontColor(this.editBreed === index ? '#FFFFFF' : '#333333')
                .borderRadius(14)
                .onClick(() => { this.editBreed = index; })
            }, (_breed: string, index: number) => `${index}`)
          }
          .width('100%')
          .margin({ bottom: 8 })

          TextInput({ placeholder: '羽化日期 YYYY-MM-DD', text: this.editEmergence })
            .width('100%')
            .onChange((value: string) => { this.editEmergence = value; })
            .margin({ bottom: 8 })

          TextInput({ placeholder: '首次产卵日期 YYYY-MM-DD', text: this.editFirstEgg })
            .width('100%')
            .onChange((value: string) => { this.editFirstEgg = value; })
            .margin({ bottom: 8 })

          TextArea({ placeholder: '备注...', text: this.editNotes })
            .width('100%')
            .height(50)
            .onChange((value: string) => { this.editNotes = value; })
            .margin({ bottom: 12 })

          Button('添加蜂王')
            .width('100%')
            .backgroundColor('#FF9800')
            .fontColor('#FFFFFF')
            .onClick(() => { this.addQueen(); })
        }
        .width('90%')
        .padding(16)
        .backgroundColor('#FFF3E0')
        .borderRadius(12)
        .margin({ bottom: 20 })

        // 蜂王列表
        Text('蜂王列表')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .width('90%')
          .margin({ bottom: 12 })

        ForEach(this.queens, (queen: QueenInfo) => {
          Column() {
            Row() {
              Text(queen.hiveName)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .layoutWeight(1)
              Text(this.getQueenStatus(queen).text)
                .fontSize(13)
                .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                .backgroundColor(this.getQueenStatus(queen).color)
                .fontColor('#FFFFFF')
                .borderRadius(10)
            }
            .width('100%')
            .margin({ bottom: 8 })

            Row() {
              Text(`蜂种:${queen.breed}`)
                .fontSize(13)
                .fontColor('#666666')
                .layoutWeight(1)
              Text(`年龄:${this.getQueenAge(queen.emergenceDate)}天`)
                .fontSize(13)
                .fontColor('#999999')
            }
            .width('100%')
            .margin({ bottom: 4 })

            if (queen.notes) {
              Text(queen.notes)
                .fontSize(13)
                .fontColor('#333333')
                .width('100%')
            }
          }
          .width('90%')
          .padding(12)
          .backgroundColor('#FAFAFA')
          .borderRadius(8)
          .margin({ bottom: 8 })
          .alignItems(HorizontalAlign.Start)
        }, (queen: QueenInfo) => queen.id)
      }
      .width('100%')
      .padding(20)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

蜂王状态评估是根据年龄来判断的:30天以内是"新王",1年以内是"壮年",1-2年是"老龄",超过2年建议更换。这个评估标准是养蜂界的通用经验值。


第三步:产蜜量统计

React版本

function HoneyStats() {
  const [records, setRecords] = useState(() => {
    const saved = localStorage.getItem('honey_records');
    return saved ? JSON.parse(saved) : [];
  });

  const addRecord = (record) => {
    const updated = [{ ...record, id: Date.now().toString() }, ...records];
    setRecords(updated);
    localStorage.setItem('honey_records', JSON.stringify(updated));
  };

  // 按月统计
  const monthlyStats = records.reduce((acc, r) => {
    const month = r.date.substring(0, 7);
    if (!acc[month]) acc[month] = 0;
    acc[month] += r.amount;
    return acc;
  }, {});

  const totalHoney = records.reduce((sum, r) => sum + r.amount, 0);

  return (
    <div className="honey-stats">
      <h3>产蜜量统计</h3>
      <p>年度总产量:{totalHoney} 斤</p>
      {/* 月度统计和记录列表 */}
    </div>
  );
}

ArkTS版本:产蜜量统计

interface HoneyRecord {
  id: string;
  date: string;
  hiveName: string;
  amount: number;       // 产量,斤
  flowerSource: string;  // 蜜源
  quality: string;       // 品质:优、良、中
}

@Entry
@Component
struct HoneyStatsPage {
  @State records: HoneyRecord[] = [];
  @State recordHive: string = '';
  @State recordAmount: number = 5;
  @State recordFlower: string = '';
  @State recordQuality: number = 0;

  private qualityOptions: string[] = ['优', '良', '中'];

  async aboutToAppear() {
    if (!dataStore) return;
    try {
      const json = await dataStore.get('honey_records', '[]') as string;
      this.records = JSON.parse(json) as HoneyRecord[];
    } catch (err) {
      console.error('加载产蜜记录失败', JSON.stringify(err));
    }
  }

  // 按月统计
  getMonthlyStats(): { month: string; total: number }[] {
    const stats: Record<string, number> = {};
    this.records.forEach((r: HoneyRecord) => {
      const month = r.date.substring(0, 7);
      if (!stats[month]) stats[month] = 0;
      stats[month] += r.amount;
    });
    return Object.entries(stats)
      .map(([month, total]) => ({ month, total }))
      .sort((a, b) => a.month.localeCompare(b.month));
  }

  // 年度总产量
  getTotalHoney(): number {
    return this.records.reduce((sum: number, r: HoneyRecord) => sum + r.amount, 0);
  }

  async addRecord() {
    const record: HoneyRecord = {
      id: Date.now().toString(),
      date: new Date().toISOString().split('T')[0],
      hiveName: this.recordHive,
      amount: this.recordAmount,
      flowerSource: this.recordFlower,
      quality: this.qualityOptions[this.recordQuality],
    };

    this.records = [record, ...this.records];

    if (dataStore) {
      await dataStore.put('honey_records', JSON.stringify(this.records));
      await dataStore.flush();
    }

    this.recordHive = '';
    this.recordFlower = '';
  }

  build() {
    Scroll() {
      Column() {
        Text('产蜜量统计')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })

        // 年度总产量
        Row() {
          Column() {
            Text(`${this.getTotalHoney()}`)
              .fontSize(36)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF9800')
            Text('年度总产量(斤)')
              .fontSize(13)
              .fontColor('#999999')
          }
          .layoutWeight(1)
          .padding(16)
          .backgroundColor('#FFF3E0')
          .borderRadius(12)

          Column() {
            Text(`${this.records.length}`)
              .fontSize(36)
              .fontWeight(FontWeight.Bold)
              .fontColor('#4CAF50')
            Text('取蜜次数')
              .fontSize(13)
              .fontColor('#999999')
          }
          .layoutWeight(1)
          .padding(16)
          .backgroundColor('#E8F5E9')
          .borderRadius(12)
        }
        .width('90%')
        .space(12)
        .margin({ bottom: 20 })

        // 月度统计
        if (this.getMonthlyStats().length > 0) {
          Text('月度统计')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .width('90%')
            .margin({ bottom: 12 })

          ForEach(this.getMonthlyStats(), (stat: { month: string; total: number }) => {
            Row() {
              Text(stat.month)
                .fontSize(14)
                .fontColor('#666666')
                .layoutWeight(1)
              Text(`${stat.total} 斤`)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF9800')
            }
            .width('90%')
            .padding(10)
            .backgroundColor('#FAFAFA')
            .borderRadius(6)
            .margin({ bottom: 4 })
          }, (stat: { month: string }) => stat.month)
        }

        // 添加记录
        Column() {
          Text('记录取蜜')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 12 })

          TextInput({ placeholder: '蜂箱名称', text: this.recordHive })
            .width('100%')
            .onChange((value: string) => { this.recordHive = value; })
            .margin({ bottom: 8 })

          Row() {
            Text('产量')
              .fontSize(14)
              .width(60)
            Text(`${this.recordAmount} 斤`)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .margin({ left: 8, right: 8 })
            Slider({ min: 0.5, max: 50, step: 0.5, value: this.recordAmount, style: SliderStyle.OutSet })
              .layoutWeight(1)
              .onChange((value: number) => { this.recordAmount = value; })
          }
          .width('100%')
          .margin({ bottom: 8 })

          TextInput({ placeholder: '蜜源(如:油菜花)', text: this.recordFlower })
            .width('100%')
            .onChange((value: string) => { this.recordFlower = value; })
            .margin({ bottom: 8 })

          Row() {
            Text('品质')
              .fontSize(14)
              .width(60)
            ForEach(this.qualityOptions, (option: string, index: number) => {
              Text(option)
                .fontSize(14)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .backgroundColor(this.recordQuality === index ? '#FF9800' : '#E0E0E0')
                .fontColor(this.recordQuality === index ? '#FFFFFF' : '#333333')
                .borderRadius(16)
                .onClick(() => { this.recordQuality = index; })
            }, (_option: string, index: number) => `${index}`)
          }
          .width('100%')
          .margin({ bottom: 12 })

          Button('记录取蜜')
            .width('100%')
            .backgroundColor('#FF9800')
            .fontColor('#FFFFFF')
            .onClick(() => { this.addRecord(); })
        }
        .width('90%')
        .padding(16)
        .backgroundColor('#F5F5F5')
        .borderRadius(12)
        .margin({ top: 16, bottom: 16 })
      }
      .width('100%')
      .padding(20)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

月度统计的实现用了reduce来按月份分组求和,然后用Object.entries把对象转成数组,最后按月份排序。这样用户就能看到每个月的产蜜量变化趋势。


流程图

flowchart TD
    A[用户打开App] --> B[加载日历标记数据]
    B --> C[显示当月日历]
    C --> D[用户点击某一天]
    D --> E{该天是否有记录?}
    E -->|有| F[加载已有记录到表单]
    E -->|没有| G[显示空白表单]
    F --> H[用户编辑巢脾数据]
    G --> H
    H --> I[保存到Preferences]
    I --> J[日历上标记该天]

    A --> K[用户进入蜂王日历]
    K --> L[显示蜂王列表]
    L --> M[用户添加新蜂王]
    M --> N[填写蜂王信息]
    N --> O[保存到Preferences]
    O --> P[计算蜂王年龄和状态]

    A --> Q[用户进入产蜜统计]
    Q --> R[显示年度总产量]
    R --> S[显示月度统计]
    S --> T[用户记录取蜜]
    T --> U[保存到Preferences]
    U --> V[更新统计数据]

React vs ArkTS 对比表

功能点React (Web)ArkTS (鸿蒙)
日历网格CSS GridGrid组件 + columnsTemplate
日期对象操作Date原生APIDate原生API(一致)
Record类型普通对象Record<string, T> 类型
月份切换setState更新直接修改@State变量
分组统计reduce + Object.entriesreduce + Object.entries(一致)
状态标签条件className条件backgroundColor

总结

这篇文章我们用「蜂勤录」这个养蜂记录App,学习了三个功能的实现:

  1. 日历标记:用Grid组件构建7列日历网格,用Record<string, CombCheckRecord>存储每天的检查数据,有标记的日期用不同背景色显示。

  2. 蜂王日历:记录蜂王的关键信息(品种、羽化日期、首次产卵日期),根据年龄自动评估蜂王状态,用不同颜色的标签显示。

  3. 产蜜量统计:用reduce按月分组求和,用卡片式布局显示年度总产量和取蜜次数,月度统计按时间排序展示。

如果你对养蜂感兴趣,欢迎去鸿蒙应用市场搜索「蜂勤录」下载体验。