HarmonyOS今天吃什么——历史记录的时间轴与标签筛选

每次转盘抽签后,结果会自动写入历史记录。历史页就是这些记录的展示窗口——按时间倒序排列,显示被选中的食物名称、emoji、分类和时间。页面顶部有分类标签用于筛选,底部有清空按钮。这个页面的逻辑不复杂,但有一个细节值得拆解:历史记录里冗余存了食物名称和 emoji——不存食物 ID 引用。这个设计决策让历史记录在食物被删除后仍然有效。

完整效果
在这里插入图片描述

在这里插入图片描述

一、历史页的整体结构

从代码看,历史页分四个区块:

Column
  ├→ Text ("历史记录")              标题
  ├→ 分类标签 (ForEach CATEGORIES)  筛选
  ├→ List + ForEach                 记录列表
  └→ 清空按钮                       底部操作

四个区块各司其职——标题告诉用户这是什么,标签筛选范围,列表展示内容,按钮提供操作。 和管理页一样的三段式布局。

二、分类标签的筛选逻辑

标签结构

Row() {
  ForEach(CATEGORIES.slice(0, 6), (c: FoodCategory) => {
    Text(c.emoji + ' ' + c.name)
      .fontSize(FontSize.xs).height(28)
      .fontColor(this.historyCat === c.key ? '#FFFFFF' : this.gc().text)
      .backgroundColor(this.historyCat === c.key ? this.gc().primary : this.gc().surface)
      .borderRadius(BorderRadius.full)
      .onClick(() => this.historyCat = c.key)
  })
}

在这里插入图片描述

和转盘 Tab、管理 Tab 的标签完全一致——选中态白字+主色背景,未选中深色字+surface 背景。 三套标签用同一个样式,但变量不同(pickCat / newCat / historyCat)。

筛选逻辑

private getFilteredHistory(): PickRecord[] {
  let list = this.history;
  if (this.historyCat !== 'all') {
    list = list.filter((r: PickRecord) => r.category === this.historyCat);
  }
  return list;
}

和转盘 Tab 的时间线筛选逻辑一样——先赋值原数组,有筛选条件就 filter。 但这里只有一层筛选(分类),比转盘 Tab 的三层筛选简单。

CATEGORIES.slice(0, 6) 的含义

CATEGORIES.slice(0, 6)  // 含"全部",共 6 项

历史页需要"全部"标签——因为用户可能想看所有记录,不限分类。 这和转盘 Tab 一样(slice(0, 6)),但和管理 Tab 不同(slice(1) 跳过"全部")。

三个 Tab 的标签对比

Tab数据源含"全部"变量作用
转盘CATEGORIES.slice(0, 6)pickCat筛选转盘范围
管理CATEGORIES.slice(1)newCat选择新增分类
历史CATEGORIES.slice(0, 6)historyCat筛选记录范围

同一个 CATEGORIES 常量,三个 Tab 用不同 slice 和变量。 转盘和历史需要"全部",管理不需要。

三、记录列表的渲染

列表结构

List() {
  ForEach(this.getFilteredHistory(), (r: PickRecord) => {
    ListItem() {
      Row() {
        Column() {
          Text(r.emoji).fontSize(28)
          Text(r.foodName).fontSize(FontSize.sm)
            .fontColor(this.gc().text).margin({ top: Spacing.xs })
        }
        Blank()
        Column() {
          Text(r.pickedAt).fontSize(FontSize.xs)
            .fontColor(this.gc().textSecondary)
          Text(CATEGORIES.find((c: FoodCategory) => c.key === r.category)?.name ?? '')
            .fontSize(FontSize.xs)
            .fontColor(this.gc().primary)
            .margin({ top: Spacing.xs })
        }
      }
      .width('100%').padding(Spacing.md)
      .backgroundColor(this.gc().surface)
      .borderRadius(BorderRadius.md)
      .margin({ bottom: Spacing.sm, left: Spacing.md, right: Spacing.md })
    }
  })
}

每行的元素排列

[emoji] [食物名称]          ── Blank ──          [时间] [分类名]
  左侧:被选中的食物信息                          右侧:元数据

左侧是"发生了什么"——emoji + 名称。右侧是"什么时候、什么分类"——时间 + 分类。 信息层次清晰:先看到是什么食物,再看到时间和分类。

分类名的查找

Text(CATEGORIES.find((c: FoodCategory) => c.key === r.category)?.name ?? '')

history 里存的是 category key(如 ‘chinese’),不是中文名(如 ‘中餐’)。 显示时需要从 CATEGORIES 里查找对应的 name。?.name ?? '' 防止查找失败时崩溃。

时间格式化

private fmtTime(ts: number): string {
  const d = new Date(ts);
  return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}

输出格式如 “7/17 14:30”——月/日 时:分。 不显示年份——历史记录通常在同一年内,省略年份更简洁。padStart(2, ‘0’) 保证小时和分钟两位数显示。

四、空状态的设计

if (this.history.length === 0) {
  Column() {
    Text('📜').fontSize(48)
    Text('还没有历史记录')
      .fontSize(FontSize.md).fontColor(this.gc().textSecondary)
    Text('去转盘抽签吧')
      .fontSize(FontSize.sm).fontColor(this.gc().textTertiary)
      .margin({ top: Spacing.xs })
  }
  .width('100%').layoutWeight(1)
  .justifyContent(FlexAlign.Center)
}

在这里插入图片描述

三层次引导——48px 卷轴 emoji + “还没有历史记录” 标题 + “去转盘抽签吧” 描述。 和转盘 Tab 的空状态结构一致。

四个 Tab 空状态对比

Tabemoji标题操作引导
转盘🎰 80px“还没有美食”有按钮
管理🍽️ 48px“还没有美食”无按钮
历史📜 48px“还没有历史记录”有描述文字
设置无(始终有内容)

转盘空状态有按钮是因为"开始"按钮在页面底部,用户可能看不到。 历史空状态用描述文字引导——不需要按钮,因为操作在转盘 Tab。

五、清空历史的功能

按钮结构

if (this.history.length > 0) {
  Button('清空历史记录')
    .width('90%').height(44)
    .fontSize(FontSize.sm)
    .fontColor('#FFFFFF')
    .backgroundColor('#EF4444')
    .borderRadius(BorderRadius.md)
    .margin({ top: Spacing.md, bottom: Spacing.lg })
    .onClick(() => this.showClear = true)
}

红色按钮——和删除食物一样的警告色。 this.history.length > 0 控制显示——没有历史时不需要清空按钮。

清空操作的两步确认

// 点击按钮 → 弹出确认弹窗
.onClick(() => this.showClear = true)

// 确认弹窗
AlertDialog({
  title: '确认清空',
  message: '清空后无法恢复,确定要清空所有历史记录吗?',
  primaryButton: {
    value: '取消',
    action: () => this.showClear = false
  },
  secondaryButton: {
    value: '清空',
    fontColor: '#EF4444',
    action: () => this.clr()
  }
})

先弹窗确认,再执行清空。 清空是不可逆操作——删除食物可以重新添加,但历史记录清空后无法恢复。

clr 方法

private async clr(): Promise<void> {
  if (this.db) {
    await this.db.clearHistory();
    this.showClear = false;
    this.load();
  }
}

清空后立刻关闭弹窗、刷新列表。 三步操作顺序不能乱——先清数据,再关弹窗,最后刷新 UI。

六、清空按钮的显示控制

if (this.history.length > 0) {
  Button('清空历史记录')...
}

有历史才显示按钮——空状态时没有清空按钮。 这是防御性设计——防止用户在空页面看到一个无意义的清空按钮。

和管理页删除的对比

特性历史清空食物删除
确认弹窗
不可逆可重新添加
按钮颜色红色🗑️ emoji
操作风险

风险越高,确认越严格。 清空所有历史是高风险操作,需要弹窗确认。删除单个食物是低风险操作,直接执行。

七、@State 变量的分工

变量作用影响范围
history历史记录数组列表渲染
historyCat分类筛选筛选结果
showClear弹窗控制AlertDialog 显示

三个变量各管一件事——数据、筛选、弹窗。 比转盘 Tab 的 8 个 @State 少很多——因为历史页功能简单,不需要动画和轮播。

八、fmtTime 方法的位置

private fmtTime(ts: number): string {
  const d = new Date(ts);
  return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}

fmtTime 是 Index 的私有方法——不是数据库层的方法。 时间格式化是 UI 逻辑,不是数据逻辑。数据库层只存时间戳,格式化由显示层决定。

时间戳存储的好处

存储方式优点缺点
时间戳 (Date.now())精确、可排序、可计算不直观,需要格式化
格式化字符串直观不可排序、不可计算

存时间戳,显示时格式化——存储和显示分离。 这是标准做法。

九、历史记录的写入时机

写入位置

// Database.ets 的 pick 方法
async pick(cat?: string): Promise<FoodItem | null> {
  const pool = this.getFoods(cat);
  if (pool.length === 0) return null;
  const p = pool[Math.floor(Math.random() * pool.length)];
  p.pickCount++; p.lastPicked = Date.now();
  await this.saveFoods();
  this.history.push({ id: 'h_' + Date.now(), foodName: p.name, emoji: p.emoji, category: p.category, pickedAt: Date.now() } as PickRecord);
  await this.saveHistory();
  return p;
}

每次抽签自动写入历史——不需要用户操作。 history 的写入和 pickCount 的更新在同一个方法里——一次抽签同时更新两个数据。

写入的数据结构

{
  id: 'h_' + Date.now(),      // 唯一 ID
  foodName: p.name,            // 食物名称(冗余)
  emoji: p.emoji,              // 表情(冗余)
  category: p.category,        // 分类(冗余)
  pickedAt: Date.now()         // 时间戳
}

冗余存储 foodName 和 emoji——不存 foodId 引用。 如果存 foodId,删除食物后历史记录就断链了。冗余存储让历史记录独立于食物数据——即使食物被删除,历史仍然完整。

十、整体嵌套结构

Column                              HistoryTab 容器
  ├→ Text ("历史记录")              标题
  ├→ Scroll + Row (分类标签)        横向滚动的分类选择
  ├→ 条件渲染
  │    ├→ 空状态 Column             无历史时
  │    └→ List + ForEach           有历史时
  │         └→ ListItem + Row      每行:emoji+名称 ── 时间+分类
  └→ 清空按钮                       有历史时才显示

嵌套深度 4 层——List → ListItem → Row → Column/Text。 历史页是四个 Tab 中结构最简单的——没有动画、没有轮播、没有弹窗输入,只有列表和筛选。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值