
旅行工具箱里新加了一个天气入口点进去是旅行天气页面。这个页面的核心设计思路是选城市 → 看天气——顶部横向滚动的城市选择栏中间当前天气大卡片下方四项天气指标底部五天预报。实现上用了一个Recordnumber, Weather的映射表来管理 mock 数据这种ID → 对象的映射模式值得单独说一下。完整效果页面结构从上到下四个区域城市选择栏横向滚动的 Tab 栏显示所有城市的 emoji 名称当前天气卡片大号天气图标 温度 天气描述四项指标湿度、风力、能见度、紫外线用竖线分隔的四列五天预报每日一行显示日期、图标、高温、低温整体布局用Column纵向排列内容区在Scroll里可以上下滚动。数据结构interfaceWeather{temp:string;icon:string;desc:stringhumidity:string;wind:string}constWEATHER_MOCK:Recordnumber,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级},}Recordnumber, Weather是一个以数字为 key、Weather 对象为 value 的字典。key 对应的是城市的id——CITIES数组里东京 id1巴黎 id2以此类推。这种ID → 对象的映射方式比直接用数组更灵活。如果以后要从服务器获取天气数据只需要按城市 ID 查询就行不需要关心数据在数组里的位置。而且Record的查找是 O(1) 的哈希查找比数组的 O(n) 遍历更快。天气字段全部用 stringtemp、humidity、wind都是字符串而不是数字。原因是它们都带有单位——18°、65%、3级。如果用数字类型单位需要另外处理反而增加了代码复杂度。在原型阶段字符串是最省事的选择。数据获取wx() 方法privatewx():Weather{constdef:Weather{temp:20°,icon:⛅,desc:晴朗,humidity:60%,wind:2级}constw:Weather|undefinedWEATHER_MOCK[this.selectedCity]returnw||def}wx()方法根据当前选中的城市 ID 从WEATHER_MOCK里查找天气数据。如果找不到比如城市 ID 不在映射表里返回一个默认的def对象。w || def是一个防御性处理——WEATHER_MOCK[this.selectedCity]可能返回undefinedkey 不存在时||运算符会在左侧为 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.selectedCityc.id?FontWeight.Bold:FontWeight.Regular).fontColor(this.selectedCityc.id?#FFFFFF:T2).padding({left:12,right:12,top:6,bottom:6}).borderRadius(16).backgroundColor(this.selectedCityc.id?A:#F5F5F7).onClick((){this.selectedCityc.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 BuilderBuilderWDetail(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比用固定宽度更灵活。如果以后增加或减少指标数量不需要手动调整每个列的宽度。五天预报BuilderForecastRow(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 渲染。理解了这个链条大多数选择 → 展示类的页面都能套用。