时间线 Tab 从"纯列表+筛选"升级成了"搜索+双视图+五维筛选+空状态引导"的完整信息架构。新增的搜索框支持关键词实时过滤,视图切换让卡片在列表和网格间自由切换,收藏筛选独立于原有四种状态筛选,空状态从"没有数据"变成了引导用户创建第一个胶囊。这些改动让时间线从"能用"变成了"好用"。

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

一、时间线的区块演进

版本 区块 筛选 视图
基础版 标题 + 筛选 + 列表 4 种 仅列表
优化版 标题 + 搜索 + 筛选 + 内容区 5 种 列表/网格

新增了三个能力:搜索框、第五种筛选(收藏)、列表/网格双视图。

二、搜索框的实现

搜索 UI 结构

Row() {
  Text('🔍').fontSize(FontSize.md).margin({ right: Spacing.xs })
  TextInput({ placeholder: '搜索...' })
    .layoutWeight(1).height(36).fontSize(FontSize.sm)
    .fontColor(this.getColors().text)
    .placeholderColor(this.getColors().textSecondary)
    .backgroundColor('transparent')
    .onChange((value: string) => this.searchKeyword = value)
  // 清除按钮
  if (this.searchKeyword || this.activeTag) {
    Text('✕').fontSize(FontSize.md).fontColor(this.getColors().textSecondary)
      .padding(Spacing.xs).onClick(() => this.clearFilters())
  }
  // 计数显示
  if (!this.searchKeyword && !this.activeTag) {
    Text(String(this.statsSummary.total) + '个')
      .fontSize(FontSize.xs).fontColor(this.getColors().textSecondary)
      .margin({ left: Spacing.sm })
  }
}
.width('100%').height(40)
.padding({ left: Spacing.sm, right: Spacing.sm })
.margin({ left: Spacing.md, right: Spacing.md, bottom: Spacing.sm })
.backgroundColor(this.getColors().surface)
.borderRadius(BorderRadius.lg)
.border({ width: 1, color: this.getColors().border })

在这里插入图片描述

搜索框的三种状态

状态 条件 显示内容
默认 无搜索词 + 无标签筛选 搜索图标 + “搜索…” + 总数
搜索中 有搜索词 搜索图标 + 输入内容 + ✕ 清除
标签筛选中 activeTag 非空 搜索图标 + “搜索…” + ✕ 清除

搜索框同时承载关键词搜索和标签筛选两种功能——✕ 按钮同时清除两种筛选。 这是"一框多用"的设计:TextInput 处理自由文本,activeTag 处理标签点击,两者叠加过滤。

TextInput 的透明背景

TextInput({ placeholder: '搜索...' })
  .backgroundColor('transparent')

搜索框背景透明——外层 Row 的 surface 色作为背景。 这样搜索框和外层容器视觉上融为一体,不会出现"白色输入框嵌在灰色容器里"的双层背景问题。

onChange 实时过滤

.onChange((value: string) => this.searchKeyword = value)

输入时实时更新 @State——不需要按搜索按钮。 每次输入变化都触发 getFiltered() 重新计算,列表立即更新。这是移动端搜索的标准交互——用户边输边看结果。

三、五维筛选系统

筛选标签的扩展

ForEach(['all', 'sealed', 'ready', 'opened', 'favorites'], (key: string) => {
  Text(this.getFilterLabel(key))
    .fontColor(this.activeFilter === key ? '#FFFFFF' : this.getColors().text)
    .backgroundColor(this.activeFilter === key ? this.getColors().primary : this.getColors().surface)
    .border({ width: 1, color: this.activeFilter === key ? this.getColors().primary : this.getColors().border })
    .onClick(() => this.activeFilter = key)
})

在这里插入图片描述

五种筛选条件

key 标签 筛选逻辑
all 全部 显示所有胶囊
sealed 🔒等待 未开启 && 未到期
ready ✨可开 未开启 && 已到期
opened 📖已开 已开启
favorites ❤️收藏 favorited === true

新增"收藏"筛选——独立于状态筛选。 用户可能想看"所有收藏的胶囊",不管它是等待中还是已开启。收藏是一个跨状态的维度,不能用 isOpened 和 unlockAt 判断。

getFiltered 的筛选叠加

private getFiltered(): Capsule[] {
  const now = Date.now()
  let list: Capsule[] = []
  if (this.activeFilter === 'all') {
    list = this.capsules.slice()
  } else if (this.activeFilter === 'sealed') {
    list = this.capsules.filter((c: Capsule) => !c.isOpened && c.unlockAt > now)
  } else if (this.activeFilter === 'ready') {
    list = this.capsules.filter((c: Capsule) => !c.isOpened && c.unlockAt <= now)
  } else if (this.activeFilter === 'opened') {
    list = this.capsules.filter((c: Capsule) => c.isOpened)
  } else if (this.activeFilter === 'favorites') {
    list = this.capsules.filter((c: Capsule) => c.favorited)
  }
  // 关键词搜索叠加
  if (this.searchKeyword) {
    const kw = this.searchKeyword.toLowerCase()
    list = list.filter((c: Capsule) =>
      c.content.toLowerCase().includes(kw) ||
      c.tags.some((t: string) => t.toLowerCase().includes(kw))
    )
  }
  // 标签筛选叠加
  if (this.activeTag) {
    list = list.filter((c: Capsule) =>
      c.tags.some((t: string) => t === this.activeTag)
    )
  }
  list.sort((a: Capsule, b: Capsule) => b.createdAt - a.createdAt)
  return list
}

三层筛选叠加:状态筛选 → 关键词搜索 → 标签筛选。 每一层都在上一层的结果上进一步过滤。用户可以同时使用"收藏+搜索关键词"——先筛选收藏,再在收藏中搜索。

clearFilters 的统一清除

private clearFilters(): void {
  this.searchKeyword = ''
  this.activeTag = ''
  this.activeFilter = 'all'
}

✕ 按钮一键清除所有筛选——恢复到默认状态。 不需要用户分别清除搜索词和标签。

四、双视图切换

视图切换按钮

Text(this.viewMode === 'list' ? '⊞' : '☰')
  .fontSize(20).width(40).height(40).textAlign(TextAlign.Center)
  .onClick(() => this.viewMode = this.viewMode === 'list' ? 'grid' : 'list')

用文字符号作为图标——⊞ 代表网格,☰ 代表列表。 不需要加载图片资源,纯文本渲染。点击在两种视图间切换。

列表视图 vs 网格视图

特性 列表视图 网格视图
布局 Column 垂直排列 Flex 2×2 网格
卡片宽度 100% 48%
信息密度 高(完整内容预览) 低(仅图标+短文字)
适用场景 阅读内容详情 快速浏览状态

网格视图的卡片设计

Column() {
  if (getCapsuleState(capsule) === CapsuleState.SEALED) {
    Text('🔒').fontSize(36)
  } else {
    Text(capsule.mood).fontSize(36)
  }
  if (getCapsuleState(capsule) === CapsuleState.SEALED) {
    Text(this.countdownText(capsule.unlockAt))
      .fontSize(11).fontColor(this.getColors().primary)
      .fontWeight(FontWeight.Bold).margin({ top: 4 })
  } else if (getCapsuleState(capsule) === CapsuleState.READY) {
    Text('✨可开启').fontSize(11)
      .fontColor('#F59E0B').fontWeight(FontWeight.Bold)
      .margin({ top: 4 })
  } else {
    Text(capsule.content.length > 15
      ? capsule.content.substring(0, 15) + '…'
      : capsule.content)
      .fontSize(10).fontColor(this.getColors().textSecondary)
      .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
      .margin({ top: 4 })
  }
}
.width('48%').height(100)
.justifyContent(FlexAlign.Center)
.backgroundColor(getCapsuleState(capsule) === CapsuleState.READY
  ? '#FFF8E1' : this.getColors().surface)
.borderRadius(BorderRadius.md)

在这里插入图片描述

网格卡片的三种状态显示

状态 图标 下方文字 背景色
SEALED 🔒 倒计时(primary色) surface
READY capsule.mood ✨可开启(黄色) #FFF8E1(暖黄)
OPENED capsule.mood 内容前15字+… surface

READY 状态的卡片有特殊背景色——暖黄色 #FFF8E1。 用户扫一眼网格就能看到哪些胶囊可以开启——黄色卡片在一堆白色卡片里非常醒目。

getCapsuleState 函数

// 从 model/Capsule.ets 导入
function getCapsuleState(capsule: Capsule): CapsuleState {
  if (capsule.isOpened) return CapsuleState.OPENED
  if (capsule.unlockAt <= Date.now()) return CapsuleState.READY
  return CapsuleState.SEALED
}

状态计算逻辑和首页 getFiltered 一致——但封装成了独立函数。 网格视图和详情页都需要判断状态,复用这个函数避免重复代码。

五、空状态的引导设计

基础版 vs 优化版

// 基础版:简单文字
Column() {
  Text('🔮').fontSize(64)
  Text('还没有胶囊').fontSize(FontSize.lg)
  Text('切换到「封印」标签创建第一个').fontSize(FontSize.sm)
}

// 优化版:完整引导
Column() {
  Text('🔮').fontSize(72)
  Text('封印你的第一个胶囊')
    .fontSize(FontSize.lg).fontWeight(FontWeight.Bold)
  Text('写下想对未来自己说的话\n设定一个未来的时间\n等待时光让它成熟')
    .fontSize(FontSize.sm).textAlign(TextAlign.Center).lineHeight(22)
  Row() {
    Text('✍️ 开始封印').fontSize(FontSize.md)
      .fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
  }
  .backgroundColor(this.getColors().primary).borderRadius(BorderRadius.full)
  .onClick(() => this.currentTab = 'create')
}

空状态的四个层次

层次 内容 作用
图标 🔮 72px 吸引注意力
标题 “封印你的第一个胶囊” 告诉用户该做什么
描述 三行引导文字 解释产品价值
按钮 “✍️ 开始封印” 直接跳转到封印 Tab

从"告知"变成"引导"——不只是说"没有数据",而是告诉用户"你可以做什么"。 按钮直接跳转到封印 Tab(this.currentTab = 'create'),减少用户操作路径。

描述文字的三行结构

写下想对未来自己说的话    ← 功能价值
设定一个未来的时间         ← 操作步骤
等待时光让它成熟           ← 情感共鸣

三行文字分别回答"是什么"“怎么做”“为什么”。 这是产品文案的经典结构——功能+步骤+情感。最后一行"等待时光让它成熟"把"等待"这件事变得浪漫,而不是枯燥的倒计时。

六、标题栏的两个按钮

Row() {
  Text('时光胶囊').fontSize(FontSize.title)
    .fontColor(this.getColors().text).fontWeight(FontWeight.Bold)
  Blank()
  Text(this.viewMode === 'list' ? '⊞' : '☰')
    .fontSize(20).width(40).height(40)
    .onClick(() => this.viewMode = this.viewMode === 'list' ? 'grid' : 'list')
  Text('🎲')
    .fontSize(20).width(40).height(40)
    .onClick(() => this.goToShuffle())
}

两个按钮的分工

按钮 图标 功能
视图切换 ⊞ / ☰ 列表/网格切换
记忆洗牌 🎲 跳转 MemoryShuffle

标题栏承载了两个不常用但需要快速访问的功能。 视图切换和洗牌不是核心操作(核心是筛选和查看),但放在标题栏比放在筛选栏或底部更合适——不占用筛选空间,不影响主流程。

七、搜索与筛选的交互优先级

用户操作路径:
  1. 打开时间线 Tab → 显示所有胶囊(按时间降序)
  2. 点击筛选标签 → 按状态/收藏筛选
  3. 输入搜索关键词 → 在筛选结果中进一步过滤
  4. 点击标签 → 在搜索结果中再过滤
  5. 点击 ✕ → 清除所有筛选,回到步骤 1

筛选和搜索是"且"的关系——同时生效。 用户可以"收藏中的+搜索’旅行’"——先筛选收藏,再在收藏中搜索包含"旅行"的胶囊。

搜索的范围

list.filter((c: Capsule) =>
  c.content.toLowerCase().includes(kw) ||
  c.tags.some((t: string) => t.toLowerCase().includes(kw))
)

搜索覆盖两个字段:内容和标签。 不搜索心情——因为心情是 emoji 不是文字。不搜索 ID——因为用户不会搜索 ID。toLowerCase() 保证大小写不敏感。

八、数据流的完整链路

用户输入搜索词
  → searchKeyword 更新
  → getFiltered() 重新计算
  → 三层层叠筛选(状态→关键词→标签)
  → 返回过滤后的列表
  → ForEach 重新渲染

五个 @State 变量协同

变量 作用 触发筛选
activeFilter 状态/收藏筛选
searchKeyword 关键词搜索
activeTag 标签筛选
viewMode 视图切换 否(只影响渲染方式)
capsules 原始数据 是(数据源)

五个变量中三个参与筛选,一个影响渲染方式,一个是数据源。 筛选逻辑和视图逻辑完全分离——改筛选不影响视图,改视图不影响筛选。

九、整体嵌套结构

Column                              buildTimeline 容器
  ├→ Row (标题)                     标题 + 视图切换 + 洗牌
  ├→ Row (搜索框)                   搜索图标 + TextInput + 清除/计数
  ├→ Scroll + Row (筛选标签)         五种筛选的横向滚动
  └→ 内容区(三选一)
       ├→ 空状态 Column             无胶囊时的引导
       ├→ Scroll + Flex (网格视图)  viewMode === 'grid'
       └→ List + ForEach (列表视图) viewMode === 'list'

内容区用 if-else 三选一——空状态、网格、列表互斥。 空状态优先判断(capsules.length === 0),有数据时再判断视图模式。

嵌套深度 5 层

Column → Scroll → Row → ForEach → Text (筛选标签)
Column → Scroll → Flex → Column → Text (网格卡片)

5 层嵌套——比首页更深。 因为时间线增加了搜索和双视图,嵌套层级增加了。但在 ArkTS 里 5 层是可接受的——超过 7 层才需要考虑拆分。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐