美食详情页是从美食指南列表点进去的——用户在 CuisinePage 里点击某道美食,跳转到这个页面查看详细信息。从代码看,这个页面的布局很直观:顶部导航栏、渐变 Hero 区域(大号 emoji)、信息卡片(名称 + 城市 + 描述 + 价格)、品尝建议。但更有意思的是它的参数传递方式——和城市详情页完全不同。
完整效果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参数传递:全字段传递 vs ID 查询

城市详情页的跳转方式是传 ID:

// CuisinePage → CityDetail
router.pushUrl({
  url: 'pages/CityDetail',
  params: { 'cityId': city.id } as Record<string, Object>
})

美食详情页的跳转方式是传全字段:

// CuisinePage → FoodDetail
router.pushUrl({
  url: 'pages/FoodDetail',
  params: {
    'name': f.name, 'emoji': f.emoji,
    'city': f.city, 'desc': f.desc, 'price': f.price
  } as Record<string, Object>
})

两种模式的区别:

维度 CityDetail(ID 查询) FoodDetail(全字段)
路由参数 只传 cityId 传 name/emoji/city/desc/price
数据来源 详情页从 CITIES 数组查找 详情页直接用路由参数
额外查询 需要(aboutToAppear 里遍历数组) 不需要
数据一致性 强(始终从数据源读取) 弱(依赖传参时的数据快照)

城市详情页用 ID 查询是因为城市数据量大(名称、国家、描述、景点数组、美食数组),全部通过路由传递不现实,而且城市数据可能被其他页面引用,从统一数据源读取更可靠。

美食详情页用全字段传递是因为美食数据量小(只有五个字段),而且美食只在 CuisinePage 里出现一次,不存在跨页面引用的问题。直接传字段比"传 ID → 查数组"少了一步查找,代码更简单。

参数接收

@State name: string = ''
@State emoji: string = ''
@State city: string = ''
@State desc: string = ''
@State price: string = ''

aboutToAppear(): void {
  const p = router.getParams() as Record<string, Object>
  if (p) {
    if (p['name']) this.name = p['name'] as string
    if (p['emoji']) this.emoji = p['emoji'] as string
    if (p['city']) this.city = p['city'] as string
    if (p['desc']) this.desc = p['desc'] as string
    if (p['price']) this.price = p['price'] as string
  }
}

在这里插入图片描述

五个 @State 变量分别接收五个路由参数。每个参数都做了 if (p['xxx']) 的存在性检查——如果调用方忘了传某个参数,对应变量保持空字符串,不会崩溃。

和城市详情页的 for 循环查找不同,这里只是简单的 if 判断 + 类型断言。代码量更多(五行 vs 三行),但逻辑更直白。

Hero 区域

Stack({ alignContent: Alignment.Center }) {
  Column() {}.width('100%').height(200).borderRadius(20)
    .linearGradient({ angle: 135, colors: [[A, 0], ['#FF9F43', 1]] })
  Text(this.emoji).fontSize(80)
}
.width('100%').height(200).borderRadius(20)
.margin({ left: 16, right: 16, bottom: 16 })

和城市详情页的 Hero 结构完全一样——Stack 叠加渐变背景和 emoji。区别是城市的 Hero 有 180vp 高,美食的 Hero 有 200vp 高,美食的 emoji 也更大(80px vs 64px)。

美食详情页的 Hero 没有文字标题——emoji 就是全部的视觉内容。城市的 Hero 需要显示城市名、国家、描述、评分等信息,所以需要更多空间。美食详情页把这些信息放在了下方的 Info 卡片里。

信息卡片

Column() {
  Text(this.name).fontSize(24).fontWeight(FontWeight.Bold).fontColor(T1)
    .margin({ bottom: 8 })
  Text('📍 ' + this.city).fontSize(14).fontColor(A).fontWeight(FontWeight.Medium)
    .margin({ bottom: 12 })
  Text(this.desc).fontSize(15).fontColor(T1).lineHeight(24)
    .margin({ bottom: 16 })
  Divider().strokeWidth(0.5).color('#F0EEF4').margin({ bottom: 14 })
  Text('参考价格').fontSize(13).fontWeight(FontWeight.Bold).fontColor(T1)
    .margin({ bottom: 4 })
  Text(this.price).fontSize(20).fontWeight(FontWeight.Bold).fontColor(A)
}

在这里插入图片描述

信息卡片分上下两部分,用 Divider 分隔:

上半部分:美食名称(大号黑色)、城市标签(橙色带定位图标)、描述文字(15px 行高 24px,保证长文本的可读性)。

下半部分:"参考价格"标签 + 价格数值(20px 橙色粗体)。价格和标签之间留了 4px 的间距,比其他元素的间距小,因为它们在语义上是紧密关联的。

lineHeight 的作用

Text(this.desc).fontSize(15).lineHeight(24) 设置了 24px 的行高。如果描述文字很长需要换行,24px 的行高比默认行高(大约 18-20px)更宽松,阅读起来更舒适。

在移动端 App 里,正文的行高通常是字号的 1.5-1.8 倍。这里 15 × 1.6 = 24,正好是 1.6 倍,是一个比较标准的正文行高。

品尝建议

Column() {
  Text('品尝建议').fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1)
    .width('100%').margin({ bottom: 8 })
  Text('📍 推荐在当地市场或老店品尝,味道最为正宗')
    .fontSize(13).fontColor(T2).lineHeight(20).margin({ bottom: 8 })
  Text('🍽 可作为正餐或小吃,根据个人口味选择配料')
    .fontSize(13).fontColor(T2).lineHeight(20).margin({ bottom: 8 })
  Text('📸 别忘了拍照记录,分享你的美食体验')
    .fontSize(13).fontColor(T2).lineHeight(20)
}

品尝建议是硬编码的通用建议——每道美食看到的都是同样的三条。这在原型阶段是合理的,真正的 App 会根据每道美食的特点给出个性化建议(比如寿司建议蘸酱油,拉面建议先喝汤)。

每条建议前面加了 emoji 前缀(📍🍽📸),和美食列表的城市标签风格保持一致。fontSize(13) + lineHeight(20) 的组合比信息卡片的描述文字更紧凑(信息卡片是 fontSize(15) + lineHeight(24)),因为建议文字是辅助信息,不需要那么突出。

导航栏右侧的空操作

Text('❤️').fontSize(20)

右上角的爱心图标目前没有绑定任何 onClick 事件。和运动健康 App 里的"编辑"按钮、“保存"按钮一样,这是一个 UI 占位——告诉用户"这里以后可以收藏这道美食”,但实际功能还没实现。

如果以后要实现收藏功能,需要:

  1. 点击时把美食信息存入 UserDataService
  2. 已收藏时爱心变红色(fontColor('#FF4757')
  3. 再次点击取消收藏

但在当前版本里,这个交互还没有接入。

两种参数传递模式的选择标准

什么时候用 ID 查询,什么时候用全字段传递?可以总结一个简单的判断标准:

用 ID 查询

  • 数据量大(超过 5 个字段)
  • 数据会被多个页面引用
  • 数据可能被更新(需要从统一数据源读取最新值)

用全字段传递

  • 数据量小(5 个字段以内)
  • 数据只在当前场景使用
  • 数据是静态的(不会被其他页面修改)

美食详情页完美符合"全字段传递"的条件——五个字段、只在 CuisinePage 里跳转、数据是写死的 mock。城市详情页符合"ID 查询"的条件——字段多、景点和美食数组很长、城市数据被首页和美食页共同引用。

选择哪种模式不是技术偏好的问题,而是数据特征决定的。

Logo

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

更多推荐