鸿蒙原生应用开发实战(四):复杂页面与交互体验——鱼种百科、天气详情与钓点详情

鸿蒙原生应用开发实战(四):复杂页面与交互体验------鱼种百科、天气详情与钓点详情

前言

前三篇文章我们完成了App的基础页面,本篇文章将挑战项目中最复杂的三个页面

  • 鱼种百科(FishEncyclopediaPage):分类筛选 + 关键词搜索 + 详情卡片
  • 天气详情(WeatherDetailPage):逐小时预报 + 7天预报 + 温度条可视化
  • 钓点详情(SpotDetailPage):路由参数接收 + 交互评分 + 用户评价

这些页面涉及搜索过滤、数据可视化、交互反馈等进阶能力,是鸿蒙应用从"能用"到"好用"的关键。


一、鱼种百科:搜索 + 分类 + 列表的复合过滤

鱼种百科是本项目的数据量最大的页面,包含8种鱼类信息,支持分类筛选和关键词搜索。

1.1 数据模型

typescript 复制代码
interface FishInfo {
  id: number;
  name: string;          // 中文名:鲫鱼
  englishName: string;   // 英文名:Goldfish
  category: string;      // 分类:淡水鱼/海水鱼/路亚目标鱼
  description: string;   // 描述
  bestSeason: string;    // 最佳季节
  maxWeight: string;     // 最大重量
  habitat: string;       // 栖息环境
  tips: string;          // 钓法技巧
  difficulty: number;    // 难度:1-5
}

1.2 分类筛选 + 搜索的组合过滤

这是页面最核心的交互逻辑------多条件组合过滤。我们通过一个计算属性(getter)实现:

typescript 复制代码
get filteredFish(): FishInfo[] {
  let result: FishInfo[] = [];
  for (let fish of this.fishList) {
    // 条件1:分类匹配
    let matchCategory = this.selectedCategory === 0 ||
      fish.category === this.categories[this.selectedCategory];

    // 条件2:搜索匹配
    let matchQuery = this.searchQuery.length === 0 ||
      fish.name.indexOf(this.searchQuery) >= 0 ||
      fish.englishName.toLowerCase()
        .indexOf(this.searchQuery.toLowerCase()) >= 0;

    if (matchCategory && matchQuery) {
      result.push(fish);
    }
  }
  return result;
}

设计要点

  • selectedCategory 为0时表示"全部",不做分类过滤
  • 搜索支持中文名和英文名模糊匹配
  • 英文搜索忽略大小写(.toLowerCase()
  • 使用 for 循环而非 filter,在严格模式下更稳定

1.3 分类标签栏

分类标签使用横向排列的胶囊按钮,选中的使用主题色填充:

typescript 复制代码
@State selectedCategory: number = 0;

private categories: string[] = ['全部', '淡水鱼', '海水鱼', '路亚目标鱼'];

Row() {
  ForEach(this.categories, (cat: string, index: number) => {
    Text(cat)
      .fontSize($r('app.float.small_font_size'))
      .fontColor(this.selectedCategory === index ? Color.White : $r('app.color.text_primary'))
      .backgroundColor(this.selectedCategory === index ? $r('app.color.primary') : $r('app.color.background'))
      .padding({ left: 14, right: 14, top: 6, bottom: 6 })
      .borderRadius(16)
      .margin({ right: 4, top: 8 })
      .onClick(() => {
        this.selectedCategory = index;  // 更新选中状态,触发重新渲染
      })
  }, (cat: string) => cat)
}
.width('90%')

交互细节

  • 未选中:白色文字 + 灰色背景(背景色)
  • 选中:白色文字 + 主题绿色背景
  • borderRadius(16) 圆角效果
  • onClick 修改 selectedCategory 状态,触发 filteredFish 重新计算和UI更新

1.4 搜索输入框

typescript 复制代码
@State searchQuery: string = '';

TextInput({ placeholder: '搜索鱼种名称', text: this.searchQuery })
  .width('90%')
  .height(40)
  .backgroundColor(Color.White)
  .borderRadius(20)
  .padding({ left: 16 })
  .onChange((value: string) => {
    this.searchQuery = value;  // 实时搜索
  })

TextInput 组件要点

  • placeholder:占位提示文字
  • text:绑定状态变量
  • onChange:输入变化时触发,实现实时过滤
  • 圆角+白色背景,与分类标签风格一致

1.5 鱼种信息卡片

每张卡片展示丰富的信息:名称、英文名、分类标签、描述、难度星级、最大重量、旺季和钓法技巧:

typescript 复制代码
ListItem() {
  Column() {
    // 标题行:中文名 + 英文名 + 分类标签
    Row() {
      Text(fish.name).fontSize(18).fontWeight(FontWeight.Bold)
      Text(fish.englishName).fontSize($r('app.float.small_font_size'))
        .fontColor($r('app.color.text_hint')).margin({ left: 8 })
      Blank()
      Text(fish.category)  // 分类标签
    }

    // 描述
    Text(fish.description).fontSize($r('app.float.small_font_size'))
      .fontColor($r('app.color.text_secondary'))
      .margin({ top: 8 }).lineHeight(20)

    // 难度 + 最大重量
    Row() {
      Text('🎯难度: ' + this.getDifficultyStars(fish.difficulty))
      Blank()
      Text('🏆最大: ' + fish.maxWeight)
    }
    .margin({ top: 8 })

    // 旺季
    Row() {
      Text('📅旺季: ' + fish.bestSeason)
    }
    .margin({ top: 4 })

    Divider().margin({ top: 8, bottom: 4 })

    // 钓法技巧
    Text('💡 ' + fish.tips)
      .fontSize($r('app.float.small_font_size'))
      .fontColor($r('app.color.primary'))
  }
  .padding(16)
  .backgroundColor($r('app.color.card_bg'))
  .borderRadius($r('app.float.card_corner_radius'))
}

1.6 难度星级生成器

我们用工具方法生成星级字符串:

typescript 复制代码
getDifficultyStars(difficulty: number): string {
  let stars = '';
  for (let i = 0; i < 5; i++) {
    stars += i < difficulty ? '★' : '☆';
  }
  return stars;
}

为什么用方法而不是计算属性?

  • 星级生成依赖参数 difficulty
  • 方法可以传递参数,getter无法传参
  • 方法返回值可以直接在模板中渲染

1.7 空搜索结果

typescript 复制代码
if (this.filteredFish.length === 0) {
  Column() {
    Text('🐟').fontSize(60)
    Text('没有找到匹配的鱼种')
  }
  .width('100%')
  .height('50%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

二、天气详情:数据可视化实战

天气详情页将展示逐小时预报和7天预报,包含温度条这种可视化元素。

2.1 数据模型

typescript 复制代码
// 逐小时
interface HourlyWeather {
  time: string;    // 06:00
  temp: number;    // 温度
  icon: string;    // 图标
  wind: string;    // 风力
}

// 每日预报
interface DailyForecast {
  date: string;
  weekDay: string;
  high: number;    // 最高温
  low: number;     // 最低温
  icon: string;
  desc: string;
  windLevel: string;
  humidity: string;
}

2.2 当前天气大卡片

typescript 复制代码
Column() {
  Row() {
    Text('☀️').fontSize(48)
    Column() {
      Text('18°C').fontSize(36).fontWeight(FontWeight.Bold)
      Text('晴 · 体感舒适')
        .fontSize($r('app.float.body_font_size'))
        .fontColor($r('app.color.text_secondary'))
        .margin({ top: 4 })
    }
    .margin({ left: 16 })
  }
  .alignItems(VerticalAlign.Center)
}

2.3 逐小时横向滚动

使用 Row 实现水平排列的逐小时预报:

typescript 复制代码
Row() {
  ForEach(this.hours, (hour: HourlyWeather) => {
    Column() {
      Text(hour.time).fontSize(12).fontColor($r('app.color.text_hint'))
      Text(hour.icon).fontSize(24).margin({ top: 6 })
      Text(hour.temp + '°').fontSize($r('app.float.body_font_size'))
        .fontWeight(FontWeight.Medium).margin({ top: 4 })
      Text(hour.wind).fontSize(11).fontColor($r('app.color.text_hint'))
        .margin({ top: 2 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
  }, (hour: HourlyWeather) => hour.time)
}

每个时段通过 layoutWeight(1) 平分宽度。

2.4 7天预报 + 温度条

这是天气页最核心的可视化设计。我们用两个叠加的 Column 实现温度条:

typescript 复制代码
ForEach(this.daily, (day: DailyForecast) => {
  Row() {
    // 星期 + 日期
    Column() {
      Text(day.weekDay).fontSize($r('app.float.small_font_size'))
        .fontWeight(FontWeight.Medium)
      Text(day.date).fontSize(11).fontColor($r('app.color.text_hint'))
        .margin({ top: 2 })
    }
    .width(50)

    // 天气描述
    Text(day.icon + ' ' + day.desc)
      .fontSize($r('app.float.small_font_size'))
      .width(90)

    Blank()

    // 最低温
    Text(day.low + '°').fontSize($r('app.float.small_font_size'))
      .fontColor($r('app.color.text_hint'))

    // 温度条
    Stack().width(60).height(6).margin({ left: 6, right: 6 }) {
      // 背景条
      Column().width('100%').height(6)
        .backgroundColor('#FFE0E0E0').borderRadius(3)
      // 前景条(表示温度范围)
      Column()
        .width(((day.high - day.low) / 14) * 60)
        .height(6)
        .backgroundColor($r('app.color.primary'))
        .borderRadius(3)
        .margin({ left: ((day.low - 6) / 14) * 60 })
    }

    // 最高温
    Text(day.high + '°').fontSize($r('app.float.small_font_size'))
      .fontColor($r('app.color.text_primary')).fontWeight(FontWeight.Medium)
  }
  .width('100%')
  .margin({ bottom: 10 })
}, (day: DailyForecast) => day.date)

温度条实现原理

复制代码
假设温度范围 6°C ~ 20°C (跨度14°C)
某天 low=8°C, high=17°C

前景条宽度 = (17-8)/14 * 60 ≈ 38.6vp
左边距 = (8-6)/14 * 60 ≈ 8.6vp

Stack容器宽度固定60vp
背景条100%填充
前景条按比例定位和缩放

这种方案不需要第三方图表库,纯ArkUI组件实现,轻量且高效。

2.5 钓鱼建议

天气详情页的特色功能------基于天气的钓鱼建议:

typescript 复制代码
private fishingAdvice: string = '今明两天天气适宜钓鱼,建议选择晴天时段出钓。周六有小雨,鱼口可能较好但不建议冒雨出行。';

get bestHours(): string {
  return '上午9:00 - 下午15:00';
}

Column() {
  Text('🎣 钓鱼建议')
  Text(this.fishingAdvice).lineHeight(22)

  Row() {
    Text('最佳出钓时段: ')
    Text(this.bestHours).fontColor($r('app.color.primary'))
  }

  Row() {
    Text('风力风向: ')
    Text('南风2-3级,适宜钓鱼')
  }

  Row() {
    Text('气压: ')
    Text('1023hPa 稳定')
  }
}

三、钓点详情:动态路由与交互

钓点详情页接收首页传递的钓点数据,展示详细信息,并支持用户评分和查看评价。

3.1 路由参数接收

这是页面数据流的重点------从首页接收参数

typescript 复制代码
interface SpotDetailParams {
  spotData: FishingSpot;
}

@State spot: FishingSpot = {
  id: 0, name: '', location: '', waterDepth: '',
  fishTypes: [], rating: 0, distance: ''
};

aboutToAppear(): void {
  const params = router.getParams() as SpotDetailParams;
  if (params && params.spotData) {
    this.spot = params.spotData;
  }
}

生命周期时序

  1. aboutToAppear() --- 页面即将显示,最先执行
  2. build() --- 构建UI
  3. aboutToDisappear() --- 页面即将消失

aboutToAppearbuild() 之前执行,保证了数据在UI渲染前已准备好。

3.2 用户评分交互

用户评分是典型的交互反馈场景:

typescript 复制代码
@State userRating: number = 0;

Row() {
  ForEach([1, 2, 3, 4, 5], (star: number) => {
    Text(star <= this.userRating ? '★' : '☆')
      .fontSize(36)
      .fontColor($r('app.color.rating_star'))
      .onClick(() => {
        this.userRating = star;  // 点击更新评分
      })
      .margin({ right: 4 })
  }, (star: number) => star.toString())
}

交互流程

  1. 用户点击第3颗星 → this.userRating = 3
  2. @State 变化 → UI重新渲染
  3. 前3颗星显示实心★,后2颗显示空心☆

3.3 评价列表

typescript 复制代码
Column() {
  Text('钓友评价').fontSize($r('app.float.body_font_size'))
    .fontWeight(FontWeight.Medium).margin({ bottom: 12 })

  // 评价1
  Column() {
    Row() {
      Circle().width(32).height(32).fill('#FFE91E63')
      Text(' 钓友A').fontSize($r('app.float.body_font_size'))
      Blank()
      Text('★★★★☆').fontSize(14).fontColor($r('app.color.rating_star'))
    }
    Text('水质很好,鱼口不错,推荐早上来。')
      .fontSize($r('app.float.small_font_size'))
      .fontColor($r('app.color.text_secondary'))
      .margin({ top: 4 })
  }

  Divider().margin({ top: 12, bottom: 12 })

  // 评价2
  Column() {
    Row() {
      Circle().width(32).height(32).fill('#FF2196F3')
      Text(' 钓友B')
      Blank()
      Text('★★★★★')
    }
    Text('第二次来了,渔获满满,停车也方便。')
  }
}

细节 :使用 Circle() 组件作为用户头像占位,不同的颜色区分不同用户。

3.4 常见鱼种展示

typescript 复制代码
Row() {
  ForEach(this.spot.fishTypes, (fish: string) => {
    Text('🐟 ' + fish)
      .backgroundColor('#FFE8F5E9')
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .borderRadius(16)
      .margin({ right: 8 })
  }, (fish: string) => fish)
}

四、ArkTS 交互模式总结

通过这三个页面,我们总结了ArkTS中几种常见的交互模式:

4.1 过滤模式(Filter Pattern)

复制代码
用户操作 → 状态变化 → 计算属性重新计算 → UI自动更新

适用于搜索、分类筛选等场景。

4.2 交互模式(Interactive Pattern)

复制代码
用户点击 → @State变量更新 → UI重新渲染 → 用户看到反馈

适用于评分、开关、计数器等场景。

4.3 参数模式(Route Pattern)

复制代码
首页 pushUrl(params) → 详情页 aboutToAppear 获取参数 → 页面渲染

适用于页面间数据传递场景。


五、性能优化:getter 计算属性的妙用

在鱼种百科中,我们使用了 get filteredFish() 计算属性:

typescript 复制代码
get filteredFish(): FishInfo[] {
  // 组合过滤逻辑
}

为什么要用 getter?

方式 特点 适用场景
getter 属性 每次访问时计算,自动依赖 @State 过滤、派生数据
普通方法 需要显式调用 有参数的转换逻辑
@State 手动维护 需要手动更新 有异步逻辑时

getter 的自动依赖追踪 意味着:当 selectedCategorysearchQuery 变化时,框架会自动重新计算 filteredFish 并更新UI。


六、UI 细节与视觉一致性

6.1 卡片圆角统一

所有卡片使用统一的圆角值:

typescript 复制代码
.borderRadius($r('app.float.card_corner_radius'))  // 12vp

6.2 颜色统一

  • 主色调:#FF2E7D32(森林绿)
  • 背景色:#FFF5F5F5(浅灰)
  • 卡片背景:#FFFFFF(白色)
  • 正文字色:#FF333333
  • 辅助文字:#FF666666
  • 提示文字:#FF999999

6.3 间距栅格

使用 $r('app.float.padding_medium')(16vp)作为标准间距,保持页面呼吸感。


七、小结

本篇我们完成了项目中最复杂的三个页面:

页面 核心挑战 技术点
鱼种百科 FishEncyclopediaPage 组合过滤 + 搜索 + 大列表 getter过滤、TextInput、分类标签
天气详情 WeatherDetailPage 数据可视化 温度条Stack叠加、逐小时/日布局
钓点详情 SpotDetailPage 动态路由 + 交互 router.getParams、评分交互、aboutToAppear

至此,App的7个页面已经开发了6个。下一篇将迎来最终章 ------钓点地图的模拟实现、全局构建配置和性能优化总结!


项目源码 :基于 HarmonyOS API 23 + Stage模型 + ArkTS

系列目录

  • 第一篇:项目初始化与环境配置
  • 第二篇:首页与钓点列表开发
  • 第三篇:数据管理与多页面交互
  • 第四篇:复杂页面与交互体验(本篇)
  • 第五篇:地图可视化与性能优化
相关推荐
lqj_本人2 小时前
鸿蒙pc:Hoppscotch-hoppscotch-ohos适配全记录
华为·harmonyos
xcLeigh2 小时前
鸿蒙PC平台 imv 图片查看器适配实战:极简主义设计的 Electron 迁移
华为·electron·harmonyos·鸿蒙·imv·图片操作·web_engine
不羁的木木2 小时前
《HarmonyOS 6.1 新能力实战之智感握姿》第四篇:进阶应用——横屏游戏手柄模式
游戏·华为·harmonyos
风华圆舞3 小时前
在 Flutter 鸿蒙项目里接入语音识别的完整思路
flutter·语音识别·harmonyos
Swift社区3 小时前
鸿蒙游戏Runtime解析:Store如何驱动整个游戏世界?
游戏·华为·harmonyos
YM52e4 小时前
手写模型集合书籍鸿蒙PC ArkTS 对象字面量类型问题约束深度解析
学习·华为·harmonyos·鸿蒙
狼哥16864 小时前
《新闻资讯》四、视频模块实现指南
ui·华为·音视频·harmonyos
风华圆舞5 小时前
鸿蒙 + Flutter 下如何让 HarmonyOS 能力真正服务于 AI 体验
人工智能·flutter·harmonyos