HarmonyOS旅行探索——旅行天气页面与数据映射模式
旅行工具箱里新加了一个天气入口,点进去是旅行天气页面。这个页面的核心设计思路是"选城市 → 看天气"——顶部横向滚动的城市选择栏,中间当前天气大卡片,下方四项天气指标,底部五天预报。实现上用了一个 Record<number, Weather> 的映射表来管理 mock 数据,这种"ID → 对象"的映射模式值得单独说一下。
完整效果
页面结构
从上到下四个区域:
- 城市选择栏:横向滚动的 Tab 栏,显示所有城市的 emoji + 名称
- 当前天气卡片:大号天气图标 + 温度 + 天气描述
- 四项指标:湿度、风力、能见度、紫外线,用竖线分隔的四列
- 五天预报:每日一行,显示日期、图标、高温、低温
整体布局用 Column 纵向排列,内容区在 Scroll 里可以上下滚动。
数据结构
interface Weather {
temp: string; icon: string; desc: string
humidity: string; wind: string
}
const WEATHER_MOCK: Record<number, Weather> = {
1: { temp: '18°', icon: '⛅', desc: '多云转晴', humidity: '65%', wind: '3级' },
2: { temp: '15°', icon: '🌤️', desc: '晴朗', humidity: '55%', wind: '2级' },
3: { temp: '29°', icon: '☀️', desc: '炎热', humidity: '80%', wind: '4级' },
4: { temp: '22°', icon: '⛅', desc: '多云', humidity: '60%', wind: '3级' },
5: { temp: '20°', icon: '🌤️', desc: '晴转多云', humidity: '50%', wind: '2级' },
6: { temp: '30°', icon: '☀️', desc: '晴朗炎热', humidity: '70%', wind: '2级' },
}

Record<number, Weather> 是一个以数字为 key、Weather 对象为 value 的字典。key 对应的是城市的 id——CITIES 数组里东京 id=1,巴黎 id=2,以此类推。
这种"ID → 对象"的映射方式比直接用数组更灵活。如果以后要从服务器获取天气数据,只需要按城市 ID 查询就行,不需要关心数据在数组里的位置。而且 Record 的查找是 O(1) 的哈希查找,比数组的 O(n) 遍历更快。
天气字段全部用 string
temp、humidity、wind 都是字符串而不是数字。原因是它们都带有单位——'18°'、'65%'、'3级'。如果用数字类型,单位需要另外处理,反而增加了代码复杂度。在原型阶段,字符串是最省事的选择。
数据获取:wx() 方法
private wx(): Weather {
const def: Weather = { temp: '20°', icon: '⛅', desc: '晴朗', humidity: '60%', wind: '2级' }
const w: Weather | undefined = WEATHER_MOCK[this.selectedCity]
return w || def
}
wx() 方法根据当前选中的城市 ID 从 WEATHER_MOCK 里查找天气数据。如果找不到(比如城市 ID 不在映射表里),返回一个默认的 def 对象。
w || def 是一个防御性处理——WEATHER_MOCK[this.selectedCity] 可能返回 undefined(key 不存在时),|| 运算符会在左侧为 falsy 时返回右侧的默认值。这保证了 wx() 永远不会返回 undefined,调用方不需要做空值检查。
这个方法在 build() 里被多次调用:
Text(this.wx().icon).fontSize(64)
Text(this.wx().temp).fontSize(48)
Text(this.wx().desc).fontSize(16)
this.WDetail('💧', '湿度', this.wx().humidity)
this.WDetail('🌬️', '风力', this.wx().wind)
每次调用都会从映射表里重新查找。因为 wx() 没有缓存机制,如果 build() 被多次调用(比如切换城市时),每次都会执行查找。对于这种小数据量的场景,重复查找的性能开销可以忽略不计。但如果数据量增大或查找逻辑变复杂,可以考虑加一个 @State 变量缓存当前天气对象。
城市选择栏
Scroll() {
Row({ space: 8 }) {
ForEach(CITIES, (c: City) => {
Text(c.emoji + ' ' + c.name).fontSize(12)
.fontWeight(this.selectedCity === c.id ? FontWeight.Bold : FontWeight.Regular)
.fontColor(this.selectedCity === c.id ? '#FFFFFF' : T2)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.selectedCity === c.id ? A : '#F5F5F7')
.onClick(() => { this.selectedCity = c.id })
})
}
}
.width('100%').scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)

六个城市用 Row 横向排列,超出屏幕宽度时用 Scroll 横向滚动。选中态用橙色背景 + 白色文字,未选中态用灰色背景 + 灰色文字——和旅行短语页面的语言切换 Tab 是同一套视觉模式。
点击某个城市时,this.selectedCity = c.id 更新 @State 变量,触发 build() 重新渲染,wx() 用新的 ID 查找对应的天气数据,UI 自动更新。
scrollBar(BarState.Off) 隐藏了滚动条。横向滚动的 Tab 栏通常不需要显示滚动条——用户可以通过滑动感知到内容可以滚动,滚动条反而会干扰视觉。
天气主卡片
Column() {
Text(this.wx().icon).fontSize(64).margin({ bottom: 8 })
Text(this.wx().temp).fontSize(48).fontWeight(FontWeight.Bold).fontColor(A)
Text(this.wx().desc).fontSize(16).fontColor(T2).margin({ top: 4 })
}
.width('100%').padding(30).backgroundColor('#FFFFFF').borderRadius(18)
.margin({ left: 16, right: 16, bottom: 16 }).alignItems(HorizontalAlign.Center)
三个元素纵向居中排列:64px 的天气图标、48px 的橙色温度、16px 的灰色描述。alignItems(HorizontalAlign.Center) 让所有文字水平居中。
温度用 fontSize(48) 的超大号字体,是整个页面视觉重心最大的元素。天气图标虽然也是大号(64px),但它是一个 emoji,视觉重量比文字轻,所以温度才是真正的"视觉锚点"。
四项指标的分隔线
Row({ space: 0 }) {
this.WDetail('💧', '湿度', this.wx().humidity)
Row() {}.width(1).height(40).backgroundColor('#F0EEF4')
this.WDetail('🌬️', '风力', this.wx().wind)
Row() {}.width(1).height(40).backgroundColor('#F0EEF4')
this.WDetail('👁️', '能见度', '10km')
Row() {}.width(1).height(40).backgroundColor('#F0EEF4')
this.WDetail('☀️', '紫外线', '中等')
}

四项指标用 Row 横向排列,每两项之间放一个 1px 宽的竖线分隔。Row({ space: 0 }) 设置间距为 0,分隔线的位置完全由 Row() 元素控制。
竖线的高度是 40px,比指标卡片矮一点——指标卡片有图标 + 数值 + 标签三层内容,高度大约 60px。40px 的竖线居中对齐后,上下各留 10px 的空白,视觉上不会太挤。
能见度和紫外线两项目前是硬编码的('10km'、'中等'),没有从 Weather 接口里读取。这是因为 mock 数据里没有这两个字段,但 UI 上需要展示。如果以后接真实 API,可以在 Weather 接口里加上 visibility 和 uv 字段。
WDetail Builder
@Builder WDetail(icon: string, label: string, value: string) {
Column() {
Text(icon).fontSize(20).margin({ bottom: 4 })
Text(value).fontSize(14).fontWeight(FontWeight.Bold).fontColor(T1)
Text(label).fontSize(10).fontColor(T3)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
三个元素纵向居中:图标、数值、标签。layoutWeight(1) 让每个 WDetail 平均分配水平空间——四个 WDetail 各占 25% 的宽度。
这种"等宽多列"的布局用 layoutWeight 比用固定宽度更灵活。如果以后增加或减少指标数量,不需要手动调整每个列的宽度。
五天预报
@Builder ForecastRow(day: string, icon: string, high: string, low: string) {
Row() {
Text(day).fontSize(13).fontColor(T1).width(40)
Text(icon).fontSize(20).width(40).textAlign(TextAlign.Center)
Row() {}.layoutWeight(1)
Text(high).fontSize(14).fontWeight(FontWeight.Bold).fontColor(A).width(40).textAlign(TextAlign.End)
Text(low).fontSize(13).fontColor(T3).width(40).textAlign(TextAlign.End)
}.width('100%')
}
每行预报分五列:日期(左对齐)、天气图标(居中)、弹性空白、高温(右对齐)、低温(右对齐)。
Row() {}.layoutWeight(1) 是一个空的弹性元素——它不显示任何内容,但占据所有剩余空间,把高温和低温推到右边。这种"用空 Row 做弹性间隔"的技巧在横向布局里很常见。
日期列和温度列都设了固定宽度 width(40),保证不同日期的文字不会错位。图标列也是 width(40),加了 textAlign(TextAlign.Center) 居中显示。
硬编码的预报数据
this.ForecastRow('今天', '⛅', '18°', '12°')
this.ForecastRow('明天', '☀️', '20°', '14°')
this.ForecastRow('后天', '🌧️', '16°', '10°')
this.ForecastRow('周四', '⛅', '19°', '13°')
this.ForecastRow('周五', '☀️', '22°', '15°')
五天预报是硬编码的,没有从数据源读取。真正的天气 App 会从天气 API 获取未来几天的预报数据,但在原型阶段,写死数据足够验证 UI 布局。
和其他页面的模式对比
天气页面和旅行短语页面有一个共同点——选择 → 展示的交互模式:
| 页面 | 选择器 | 数据源 | 展示内容 |
|---|---|---|---|
| WeatherPage | 城市 Tab 栏 | WEATHER_MOCK | 当前天气 + 预报 |
| PhraseBook | 语言 Tab 栏 | PHRASES 字典 | 短语列表 |
| CurrencyPage | 货币列表 | rateMap | 换算结果 |
三个页面都用了"Tab 切换 + 数据更新"的模式,区别只在数据源和展示形式。选哪个 Tab 就显示对应的数据,切换时 @State 变更触发 build() 重新渲染。
这种模式可以抽象为:选择器改变状态 → 状态驱动数据查询 → 数据驱动 UI 渲染。理解了这个链条,大多数"选择 → 展示"类的页面都能套用。
更多推荐


所有评论(0)