文章目录时间轴条目的解剖定义数据结构Builder 提取条目结构状态颜色映射ForEach 遍历渲染完整代码为什么用成员方法 Builder 而不是全局 BuilderisLast 参数的必要性小结时间轴这种组件你盯着看一秒就能发现规律每个条目长得一模一样。左边一个圆点中间一根竖线连着上下右边是标题和内容。既然结构完全一致就没理由把同样的代码写 N 遍。Builder就是干这个用的——抽出重复结构参数化差异部分一行调用搞定。时间轴条目的解剖先把一个条目拆开看看左侧是轨道一个 12vp 的实心圆 一段Divider连接线。右侧是内容区时间标签靠右标题加粗描述文字略灰。这个结构里变化的只有四个东西时间、标题、描述、圆点颜色反映事件状态。其余骨架完全固定。定义数据结构interfaceTimelineItem{time:stringtitle:stringdesc:stringstatus:done|active|pending}状态只有三种已完成、进行中、待开始。颜色由状态决定不需要外部传颜色值。Builder 提取条目结构BuildertimelineItem(item:TimelineItem,isLast:boolean){Row({space:16}){// 左侧轨道Column(){Circle({width:12,height:12}).fill(this.getStatusColor(item.status)).border({width:2,color:Color.White}).shadow({radius:4,color:#20000000})if(!isLast){Divider().vertical(true).height(48).strokeWidth(2).color(#E5E7EB)}}.width(24).alignItems(HorizontalAlign.Center)// 右侧内容Column({space:4}){Row(){Text(item.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#1F2937).layoutWeight(1)Text(item.time).fontSize(12).fontColor(#9CA3AF)}.width(100%)Text(item.desc).fontSize(13).fontColor(#6B7280).lineHeight(20).width(100%)}.layoutWeight(1).padding({bottom:24})}.alignItems(VerticalAlign.Top).width(100%)}isLast参数控制最后一个条目不渲染连接线细节但重要。状态颜色映射颜色映射单独抽成方法别把 if-else 塞进 Builder 里会很乱getStatusColor(status:done|active|pending):string{constmap:Recordstring,string{done:#10B981,// 完成绿色active:#3B82F6,// 进行中蓝色pending:#D1D5DB// 待开始灰色}returnmap[status]??#D1D5DB}ForEach 遍历渲染ForEach(this.items,(item:TimelineItem,index:number){this.timelineItem(item,indexthis.items.length-1)})注意index this.items.length - 1判断最后一项传isLast: true。完整代码enumTimelineStatus{Done,Active,Pending}classTimelineItem{time:stringtitle:stringdesc:stringstatus:TimelineStatusconstructor(time:string,title:string,desc:string,status:TimelineStatus){this.timetimethis.titletitlethis.descdescthis.statusstatus}}EntryComponentstruct PcTimelineBuilderPage{Stateitems:TimelineItem[][newTimelineItem(2024-01-05,需求评审通过,产品需求文档 v2.3 评审通过确定 HarmonyOS PC 端首个版本功能范围。,TimelineStatus.Done),newTimelineItem(2024-01-12,UI 设计稿交付,设计团队完成所有页面 UI 设计导出 Figma 标注开发侧开始接入。,TimelineStatus.Done),newTimelineItem(2024-01-20,基础框架搭建,完成页面路由、数据层、状态管理框架搭建联调接口通过。,TimelineStatus.Done),newTimelineItem(2024-01-28,核心功能开发中,文件管理、搜索、设置三个核心模块正在并行开发预计本周完成。,TimelineStatus.Active),newTimelineItem(2024-02-05,内测版本发布,向内部用户开放内测收集使用反馈修复高优先级 Bug。,TimelineStatus.Pending),newTimelineItem(2024-02-18,正式版上线,通过华为应用商店审核正式对外发布 HarmonyOS PC 端应用 v1.0。,TimelineStatus.Pending)]getStatusColor(status:TimelineStatus):string{switch(status){caseTimelineStatus.Done:return#10B981caseTimelineStatus.Active:return#3B82F6caseTimelineStatus.Pending:return#D1D5DBdefault:return#D1D5DB}}BuildertimelineItem(item:TimelineItem,isLast:boolean){Row({space:16}){Column(){Circle({width:12,height:12}).fill(this.getStatusColor(item.status)).border({width:2,color:Color.White}).shadow({radius:4,color:#20000000})if(!isLast){Divider().vertical(true).height(48).strokeWidth(2).color(#E5E7EB)}}.width(24).alignItems(HorizontalAlign.Center)Column({space:4}){Row(){Text(item.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#1F2937).layoutWeight(1)Text(item.time).fontSize(12).fontColor(#9CA3AF)}.width(100%)Text(item.desc).fontSize(13).fontColor(#6B7280).lineHeight(20).width(100%)}.layoutWeight(1).padding({bottom:24})}.alignItems(VerticalAlign.Top).width(100%)}BuilderlegendItem(color:string,label:string){Row({space:6}){Circle({width:8,height:8}).fill(color)Text(label).fontSize(12).fontColor(#6B7280)}}build(){Scroll(){Column({space:0}){// 页面标题Row(){Column({space:4}){Text(项目进度时间轴).fontSize(22).fontWeight(FontWeight.Bold).fontColor(#111827)Text(HarmonyOS PC 端应用开发里程碑).fontSize(14).fontColor(#6B7280)}.alignItems(HorizontalAlign.Start).layoutWeight(1)// 图例Row({space:16}){this.legendItem(#10B981,已完成)this.legendItem(#3B82F6,进行中)this.legendItem(#D1D5DB,待开始)}}.width(100%).padding({left:32,right:32,top:32,bottom:24}).backgroundColor(Color.White)Divider().strokeWidth(1).color(#F3F4F6)// 时间轴主体Column({space:0}){ForEach(this.items,(item:TimelineItem,index:number){this.timelineItem(item,indexthis.items.length-1)})}.padding({left:32,right:32,top:32,bottom:32}).backgroundColor(Color.White).margin({top:16})}.constraintSize({minWidth:600,maxWidth:900}).margin({left:auto,right:auto})}.width(100%).height(100%).backgroundColor(#F9FAFB)}}为什么用成员方法 Builder 而不是全局 Builder全局 Builder 没有this拿不到组件的getStatusColor方法。如果把颜色直接当参数传进去倒也可以但这样接口就变成四个参数调用侧就更繁琐。成员方法形式的 Builder 可以直接调this.xxx对于依赖组件内部数据的 Builder成员方法是更自然的选择。什么时候用全局 Builder当这段 UI 逻辑完全无状态、也不依赖任何组件数据时才考虑全局形式。比如纯展示的图标、通用的卡片外框。isLast 参数的必要性不传isLast、让每个条目都渲染连接线会怎样最后一个条目的连接线会悬空下面什么都没有视觉上很奇怪。处理方式不止一种isLast参数控制——本文用的方案最直接在 CSS 里用伪选择器——ArkUI 不支持让连接线只连接上方节点放在每个条目顶部而不是底部——逻辑上等价但计算稍麻烦方案一最简单就用这个。小结这篇的核心就一句话重复结构提进 Builder变化部分变成参数。时间轴是很好的练习场景因为它的重复性极强、差异部分也很明确。把这个思路迁移到其他场景——列表行、卡片组件、设置项——逻辑完全一样。下一篇聊时间轴的视觉设计圆点大小、竖线颜色、状态颜色系统让时间轴看起来不只是功能正确还得好看。
HarmonyOS PC 实战系列之时间轴条目用 @Builder 提取——重复结构只写一次
发布时间:2026/6/13 10:56:02
文章目录时间轴条目的解剖定义数据结构Builder 提取条目结构状态颜色映射ForEach 遍历渲染完整代码为什么用成员方法 Builder 而不是全局 BuilderisLast 参数的必要性小结时间轴这种组件你盯着看一秒就能发现规律每个条目长得一模一样。左边一个圆点中间一根竖线连着上下右边是标题和内容。既然结构完全一致就没理由把同样的代码写 N 遍。Builder就是干这个用的——抽出重复结构参数化差异部分一行调用搞定。时间轴条目的解剖先把一个条目拆开看看左侧是轨道一个 12vp 的实心圆 一段Divider连接线。右侧是内容区时间标签靠右标题加粗描述文字略灰。这个结构里变化的只有四个东西时间、标题、描述、圆点颜色反映事件状态。其余骨架完全固定。定义数据结构interfaceTimelineItem{time:stringtitle:stringdesc:stringstatus:done|active|pending}状态只有三种已完成、进行中、待开始。颜色由状态决定不需要外部传颜色值。Builder 提取条目结构BuildertimelineItem(item:TimelineItem,isLast:boolean){Row({space:16}){// 左侧轨道Column(){Circle({width:12,height:12}).fill(this.getStatusColor(item.status)).border({width:2,color:Color.White}).shadow({radius:4,color:#20000000})if(!isLast){Divider().vertical(true).height(48).strokeWidth(2).color(#E5E7EB)}}.width(24).alignItems(HorizontalAlign.Center)// 右侧内容Column({space:4}){Row(){Text(item.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#1F2937).layoutWeight(1)Text(item.time).fontSize(12).fontColor(#9CA3AF)}.width(100%)Text(item.desc).fontSize(13).fontColor(#6B7280).lineHeight(20).width(100%)}.layoutWeight(1).padding({bottom:24})}.alignItems(VerticalAlign.Top).width(100%)}isLast参数控制最后一个条目不渲染连接线细节但重要。状态颜色映射颜色映射单独抽成方法别把 if-else 塞进 Builder 里会很乱getStatusColor(status:done|active|pending):string{constmap:Recordstring,string{done:#10B981,// 完成绿色active:#3B82F6,// 进行中蓝色pending:#D1D5DB// 待开始灰色}returnmap[status]??#D1D5DB}ForEach 遍历渲染ForEach(this.items,(item:TimelineItem,index:number){this.timelineItem(item,indexthis.items.length-1)})注意index this.items.length - 1判断最后一项传isLast: true。完整代码enumTimelineStatus{Done,Active,Pending}classTimelineItem{time:stringtitle:stringdesc:stringstatus:TimelineStatusconstructor(time:string,title:string,desc:string,status:TimelineStatus){this.timetimethis.titletitlethis.descdescthis.statusstatus}}EntryComponentstruct PcTimelineBuilderPage{Stateitems:TimelineItem[][newTimelineItem(2024-01-05,需求评审通过,产品需求文档 v2.3 评审通过确定 HarmonyOS PC 端首个版本功能范围。,TimelineStatus.Done),newTimelineItem(2024-01-12,UI 设计稿交付,设计团队完成所有页面 UI 设计导出 Figma 标注开发侧开始接入。,TimelineStatus.Done),newTimelineItem(2024-01-20,基础框架搭建,完成页面路由、数据层、状态管理框架搭建联调接口通过。,TimelineStatus.Done),newTimelineItem(2024-01-28,核心功能开发中,文件管理、搜索、设置三个核心模块正在并行开发预计本周完成。,TimelineStatus.Active),newTimelineItem(2024-02-05,内测版本发布,向内部用户开放内测收集使用反馈修复高优先级 Bug。,TimelineStatus.Pending),newTimelineItem(2024-02-18,正式版上线,通过华为应用商店审核正式对外发布 HarmonyOS PC 端应用 v1.0。,TimelineStatus.Pending)]getStatusColor(status:TimelineStatus):string{switch(status){caseTimelineStatus.Done:return#10B981caseTimelineStatus.Active:return#3B82F6caseTimelineStatus.Pending:return#D1D5DBdefault:return#D1D5DB}}BuildertimelineItem(item:TimelineItem,isLast:boolean){Row({space:16}){Column(){Circle({width:12,height:12}).fill(this.getStatusColor(item.status)).border({width:2,color:Color.White}).shadow({radius:4,color:#20000000})if(!isLast){Divider().vertical(true).height(48).strokeWidth(2).color(#E5E7EB)}}.width(24).alignItems(HorizontalAlign.Center)Column({space:4}){Row(){Text(item.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor(#1F2937).layoutWeight(1)Text(item.time).fontSize(12).fontColor(#9CA3AF)}.width(100%)Text(item.desc).fontSize(13).fontColor(#6B7280).lineHeight(20).width(100%)}.layoutWeight(1).padding({bottom:24})}.alignItems(VerticalAlign.Top).width(100%)}BuilderlegendItem(color:string,label:string){Row({space:6}){Circle({width:8,height:8}).fill(color)Text(label).fontSize(12).fontColor(#6B7280)}}build(){Scroll(){Column({space:0}){// 页面标题Row(){Column({space:4}){Text(项目进度时间轴).fontSize(22).fontWeight(FontWeight.Bold).fontColor(#111827)Text(HarmonyOS PC 端应用开发里程碑).fontSize(14).fontColor(#6B7280)}.alignItems(HorizontalAlign.Start).layoutWeight(1)// 图例Row({space:16}){this.legendItem(#10B981,已完成)this.legendItem(#3B82F6,进行中)this.legendItem(#D1D5DB,待开始)}}.width(100%).padding({left:32,right:32,top:32,bottom:24}).backgroundColor(Color.White)Divider().strokeWidth(1).color(#F3F4F6)// 时间轴主体Column({space:0}){ForEach(this.items,(item:TimelineItem,index:number){this.timelineItem(item,indexthis.items.length-1)})}.padding({left:32,right:32,top:32,bottom:32}).backgroundColor(Color.White).margin({top:16})}.constraintSize({minWidth:600,maxWidth:900}).margin({left:auto,right:auto})}.width(100%).height(100%).backgroundColor(#F9FAFB)}}为什么用成员方法 Builder 而不是全局 Builder全局 Builder 没有this拿不到组件的getStatusColor方法。如果把颜色直接当参数传进去倒也可以但这样接口就变成四个参数调用侧就更繁琐。成员方法形式的 Builder 可以直接调this.xxx对于依赖组件内部数据的 Builder成员方法是更自然的选择。什么时候用全局 Builder当这段 UI 逻辑完全无状态、也不依赖任何组件数据时才考虑全局形式。比如纯展示的图标、通用的卡片外框。isLast 参数的必要性不传isLast、让每个条目都渲染连接线会怎样最后一个条目的连接线会悬空下面什么都没有视觉上很奇怪。处理方式不止一种isLast参数控制——本文用的方案最直接在 CSS 里用伪选择器——ArkUI 不支持让连接线只连接上方节点放在每个条目顶部而不是底部——逻辑上等价但计算稍麻烦方案一最简单就用这个。小结这篇的核心就一句话重复结构提进 Builder变化部分变成参数。时间轴是很好的练习场景因为它的重复性极强、差异部分也很明确。把这个思路迁移到其他场景——列表行、卡片组件、设置项——逻辑完全一样。下一篇聊时间轴的视觉设计圆点大小、竖线颜色、状态颜色系统让时间轴看起来不只是功能正确还得好看。