HarmonyOS 6.1 实战:@Builder装饰器支持参数传递、条件渲染、循环迭代等多种语法,与TypeScript函数的使用方式高度一致

在移动应用开发的演进历程中,声明式UI范式已经成为业界公认的主流方向。HarmonyOS ArkUI框架以其声明式的开发范式、组件化的架构设计以及高效的渲染管线,为开发者提供了一套完整的端到端解决方案。本文将以"鸽语·信鸽竞翔与赛鸽俱乐部"应用为载体,深入剖析基于HarmonyOS API 24(即HarmonyOS 6.1.1版本所对应的ArkTS API等级)的声明式UI开发实践,从颜色系统设计、数据模型定义、状态管理机制,到布局组件编排、特效层实现、弹框交互模式,逐一展开深度技术分析。
声明式UI的核心思想在于"描述UI应该是什么样子,而不是命令UI如何变化"。在传统的命令式开发中,开发者需要手动操作DOM节点或视图对象,通过setter方法逐一修改属性,当状态变化时还需手动触发界面刷新。这种方式在复杂交互场景下极易产生状态不一致的问题,代码维护成本也随功能迭代呈指数级增长。而声明式UI则将UI描述为状态的函数——给定一个状态,就有唯一确定的UI与之对应,当状态发生变化时,框架自动计算差异并更新视图。这种范式从根本上简化了状态与视图的同步逻辑,让开发者能够将更多精力聚焦于业务逻辑本身。
HarmonyOS ArkUI框架在声明式UI的基础之上,进一步引入了@Observed、@ObjectLink、@Track等高级状态管理装饰器,形成了一套层次分明、粒度可控的响应式系统。对于复杂的数据结构,@Observed装饰器能够标记类为可观察对象,使得其内部属性的变化能够被框架精准捕获,从而触发细粒度的UI刷新。这种机制相较于整体替换对象的方式,在性能上有着显著的优势,尤其适合赛鸽俱乐部这类包含大量列表数据和动态内容的应用场景。
@Builder装饰器是ArkUI框架中另一个极具特色的语法特性。它允许开发者将可复用的UI片段封装为独立的构建函数,在需要的位置通过函数调用的方式进行引用。@Builder装饰器支持参数传递、条件渲染、循环迭代等多种语法,与TypeScript函数的使用方式高度一致,但在底层却享受着框架级别的编译优化。在鸽语应用中,@Builder装饰器被广泛应用于header头部、subNav子导航、各页面内容区、弹框组件等多个UI模块,形成了清晰的组件分层结构。
Stack/Column/Row三大布局容器构成了ArkUI声明式布局系统的基石。Stack布局采用堆叠式排列,子组件沿Z轴方向叠加,适合实现弹框、遮罩层、特效层等层级化界面;Column布局沿垂直方向排列子组件,是页面纵向滚动内容的主要承载方式;Row布局沿水平方向排列子组件,常用于横向功能分区和行内元素排列。这三种布局容器通过嵌套组合,可以构建出任意复杂的界面结构,且配合layoutWeight属性能够实现灵活的自适应布局。在鸽语应用中,Stack作为根容器承载了背景层、特效层、内容层和弹框层四个层级,Column则负责页面主体内容的纵向排列,Row广泛应用于导航栏、卡片头部、列表项等水平布局场景。
ArkUI渲染管线是支撑声明式UI高效运行的底层引擎。从组件声明到界面呈现,整个过程经历了组件树构建、差异比较、布局计算、绘制指令生成、GPU渲染等多个阶段。框架通过虚拟DOM(Virtual DOM)技术维护组件的内存表示,当状态变化时,先在虚拟DOM上计算最小变更集,再将变更应用到真实渲染树,从而避免不必要的重绘和重排。鸽语应用中的鸽子剪影浮动特效和气流光环扩散特效,正是基于这套渲染管线实现的——通过setInterval定时更新tick状态,触发特效层的重新计算和渲染,在保证动画流畅的同时,不会对上层交互组件的性能产生影响。
灰蓝赛鸽色彩设计系统是鸽语应用在视觉层面的核心表达。以灰蓝色#5C6B7A为主色调,鸽红色#A63D40为强调色,辅以wing羽色#8494A5作为特殊点缀色,构成了一个层次分明、语义丰富的色彩体系。灰蓝色象征着赛鸽在蓝天翱翔的自由意象,鸽红色则代表着赛鸽竞翔的激情与活力,两种颜色的搭配既保持了专业稳重的基调,又注入了运动竞技的热血元素。整个色彩系统通过ColorPalette接口进行类型化定义,确保在代码层面的类型安全和使用一致性。
信鸽竞翔社区交互模式是鸽语应用的产品灵魂。围绕赛鸽市集、训放托管、秋赛报名、鸽舍诊断四大核心玩法,应用构建了从赛鸽交易、训放管理、赛事参与到知识学习的完整生态闭环。鸽友圈动态、人气赛鸽橱窗、养训课堂等功能模块,为鸽友提供了交流互动、学习成长的社区空间。底部4主tab加首页7内容tab的双层导航结构,使得信息组织清晰有序,用户能够快速定位到所需功能。而新增、编辑、删除三类弹框交互,则覆盖了用户内容生产和数据管理的核心操作路径。
一、ColorPalette接口设计与灰蓝鸽红颜色系统深度剖析
1.1 接口定义与设计理念

颜色系统是一个应用视觉识别体系的基石。在鸽语应用中,颜色系统通过TypeScript接口ColorPalette进行类型化定义,这种设计方式带来了多重优势:首先,接口约束了颜色字段的名称和类型,确保所有颜色值都以字符串形式存储,符合CSS颜色值的通用规范;其次,通过接口类型注解,IDE能够提供智能补全和类型检查,有效减少颜色引用错误;最后,接口化的颜色系统便于后续扩展和维护,当需要新增颜色字段时,只需在接口中添加声明即可。
interface ColorPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
wing: string;
}
从接口的字段命名可以看出,这套颜色系统采用了分层语义化的设计思路。primary系列代表主色调及其明暗变体,accent系列代表强调色及其浅色变体,text系列定义了三级文字色阶,bg和cardBg分别对应页面背景和卡片背景,success/warning/danger构成了状态色三元组,white提供了通用白色值,而wing则是赛鸽主题特有的羽色点缀。
1.2 主色调:灰蓝#5C6B7A的语义解析

primary: '#5C6B7A'是整个应用的核心主色调。这是一种中等明度的灰蓝色,RGB值约为(92, 107, 122),在色相环上位于蓝灰色区域。灰蓝色的选择蕴含着深刻的产品语义:它既有着天空的辽阔感,呼应了赛鸽翱翔天际的产品意象;又带着金属般的沉稳质感,传递出专业可信赖的品牌调性。在界面中,灰蓝色主要应用于导航栏选中态、按钮背景、重要文字标题、进度条主色等关键视觉位置,起到了视觉锚点的作用。
primaryLight: '#DCE1E7'是主色调的浅色变体,明度提升到了约88%。这种浅灰蓝色在界面中主要用作选中项的背景填充,例如子导航栏当前选中tab的背景色。浅灰蓝与主色调形成了和谐的同色系对比,既能够标识选中状态,又不会产生过于强烈的视觉冲击,保持了整体界面的柔和统一。
primaryDark: '#3B4754'是主色调的深色变体,明度降低到了约29%。深灰蓝色在界面中主要用于需要突出但又不适合使用纯黑的场景,例如训放进度卡上的"顶风逆飞"标签文字、"查看雷达"按钮的文字颜色等。深灰蓝与主色调形成了深浅对比,增强了视觉层次感。
1.3 强调色:鸽红#A63D40的情感表达
accent: '#A63D40'是应用的强调色,我们称之为"鸽红"。这是一种偏暗的红棕色,RGB值约为(166, 61, 64),饱和度适中,明度约为45%。鸽红色的选择颇具匠心:它不是鲜艳的正红,而是带有褐色调的暗红,这种颜色既有着赛鸽竞翔的热血激情,又不失稳重内敛的气质。在界面中,鸽红色主要应用于报名按钮、退课按钮、危险操作、重要标签等高亮强调场景,与灰蓝主色形成冷暖对比,有效引导用户视觉焦点。
accentLight: '#F0DCDA'是鸽红色的浅色变体,这是一种非常浅的粉红色,明度高达95%以上。浅鸽红色在界面中主要用作状态标签的背景填充,例如赛鸽等级标签、训放状态标签等。浅色背景配合深色文字,既能够清晰标识状态信息,又保持了界面的清爽通透。
1.4 背景与文字色阶:信息层级的视觉表达

bg: '#F1F3F5'是页面的全局背景色,这是一种极浅的灰蓝色,明度约为95%。浅灰蓝背景为整个应用奠定了冷色调的基调,与白色卡片形成微妙的冷暖对比,避免了纯白背景可能带来的刺眼感,同时也与灰蓝主色形成了色彩呼应。
cardBg: '#FFFFFF'是卡片组件的背景色,使用标准的纯白色。在浅灰蓝背景之上,白色卡片能够清晰地勾勒出内容区块的边界,形成明确的视觉分隔。这种"浅灰底+白卡片"的组合是现代UI设计中的经典搭配,既保证了内容的可读性,又营造出层次分明的视觉效果。
文字系统定义了三个层级:textPrimary: '#2B333D'是一级文字色,这是一种偏蓝的深灰色,用于标题、正文等主要文字内容,确保最佳的可读性;textSecondary: '#5F6C79'是二级文字色,明度约为40%,用于辅助说明、次要信息等;textHint: '#A6B0BC'是三级文字色,明度约为70%,用于提示文字、时间戳、占位符等最弱信息层级。三级文字色阶构成了清晰的信息视觉层级,用户可以通过颜色深浅快速判断信息的重要程度。
border: '#E2E7EC'是边框分割线颜色,这是一种极浅的灰蓝色,明度约为90%。边框色与背景色接近但略有区分,能够在不破坏整体视觉统一性的前提下,提供微妙的视觉分隔。
1.5 状态色与特殊色:功能语义的色彩编码
success: '#7BA884'是成功状态色,一种柔和的灰绿色。绿色在UI设计中通常代表成功、通过、健康等正面语义。在鸽语应用中,成功色用于"已归巢"状态标识、最佳分速数据、天气晴好等积极信息的展示。
warning: '#D9A05B'是警告状态色,一种温暖的赭黄色。黄色代表提醒、注意、进行中等语义。在应用中,警告色用于"验鸽中"状态、鸽币余额、赛线信息等需要引起注意但非紧急的场景。
danger: '#C76B5A'是危险状态色,一种偏红的棕褐色。红色代表危险、删除、错误等负面语义。在应用中,危险色用于取消训放按钮、退课按钮、删除确认等破坏性操作的标识,以及购物车角标等需要强烈警示的场景。
white: '#FFFFFF'是纯白色,作为通用颜色值提供。虽然cardBg也使用白色,但单独定义white字段使得代码语义更加清晰——当需要表达"白色"这个颜色概念时(如文字颜色、图标颜色),使用white;当需要表达"卡片背景"这个语义时,使用cardBg。
wing: '#8494A5'是整个颜色系统中最具特色的字段——羽色。这是一种介于主色primary和主色浅变体primaryLight之间的灰蓝色,RGB值约为(132, 148, 165)。wing羽色的命名灵感来源于赛鸽的翅膀羽毛颜色,它在界面中扮演着"特殊点缀色"的角色,常用于训放进度卡上的标签背景、"查看雷达"按钮背景、气流光环特效等位置。wing色的存在使得整个颜色系统在标准的UI色彩体系之外,增添了一份赛鸽主题的专属韵味。
二、@Observed数据模型深度解析

2.1 @Observed装饰器的工作原理
在HarmonyOS ArkUI的状态管理体系中,@Observed装饰器扮演着至关重要的角色。它用于装饰类,使得该类的实例成为可观察对象。当被@Observed装饰的类的实例属性发生变化时,框架能够自动检测到这些变化,并触发依赖这些属性的UI组件进行重新渲染。这种细粒度的状态追踪机制,是ArkUI高性能声明式渲染的核心保障之一。
与@State装饰器只能追踪基本类型和对象引用变化不同,@Observed装饰器能够深入到对象内部,追踪对象属性的变化。这意味着即使对象引用没有改变,只要对象内部的某个属性发生了变化,使用到该属性的UI组件也会自动更新。这种机制对于复杂数据模型的管理尤为重要——开发者无需在每次修改数据时都创建新的对象实例,直接修改属性即可触发界面刷新,大大简化了代码逻辑。
在鸽语应用中,共定义了四个@Observed数据模型类:PostItem(鸽友动态)、PigeonItem(赛鸽)、RaceItem(训放订单)、GearItem(鸽具器材)。这四个模型分别对应了社区内容、赛鸽商品、训放服务、器材物资四大核心业务实体,构成了应用数据层的骨架。
2.2 PostItem:鸽友动态数据模型
@Observed
export class PostItem {
id: number = 0
nick: string = ''
avatar: string = ''
text: string = ''
time: string = ''
likes: number = 0
constructor(id: number, nick: string, avatar: string, text: string, time: string, likes: number) {
this.id = id; this.nick = nick; this.avatar = avatar; this.text = text
this.time = time; this.likes = likes
}
}
PostItem是鸽友圈动态的数据模型,包含六个字段:
id: number:动态的唯一标识符,数值类型。在ForEach循环渲染中作为key使用,确保列表项的稳定标识,提升渲染性能。nick: string:发布者昵称,字符串类型。展示鸽友的用户名,是社区互动中的身份标识。avatar: string:发布者头像,字符串类型。使用emoji表情作为头像图标,这种设计既简化了图片资源管理,又增添了趣味性。text: string:动态正文内容,字符串类型。鸽友发布的文字内容,是动态信息的核心载体。time: string:发布时间,字符串类型。使用相对时间描述,如"6分钟前"、“2小时前”、"昨天"等,符合社区产品的时间展示习惯。likes: number:点赞数,数值类型。表示该动态获得的互动量,反映内容的受欢迎程度。
PostItem的构造函数接收六个参数并依次赋值给对应的实例属性。构造函数采用了紧凑的写法,将多个赋值语句用分号分隔在同一行,体现了函数式编程的简洁风格。
2.3 PigeonItem:赛鸽数据模型

@Observed
export class PigeonItem {
id: number = 0
name: string = ''
craft: string = ''
price: string = ''
level: string = ''
hot: number = 0
constructor(id: number, name: string, craft: string, price: string, level: string, hot: number) {
this.id = id; this.name = name; this.craft = craft; this.price = price
this.level = level; this.hot = hot
}
}
PigeonItem是赛鸽橱窗的数据模型,包含六个字段:
id: number:赛鸽的唯一标识符,数值类型。name: string:赛鸽名称,字符串类型。名称中包含了羽色信息和足环编号,如"灰壳·24-088821",格式为"羽色·年份-编号",这是赛鸽行业的标准命名方式。craft: string:血统品系,字符串类型。描述赛鸽的血统来源,如"詹森直孙"、“胡本血脉”、"红绛老血统"等,这是赛鸽价值评估的重要依据。price: string:价格,字符串类型。格式如"¥6800/羽",包含货币符号和单位,直接用于界面展示。level: string:等级标签,字符串类型。如"精英"、“主力”、“潜力”、“冠军血”、"幼鸽"等,用于标识赛鸽的品质等级。hot: number:关注度,数值类型。表示该赛鸽的人气热度,数值越高代表越受欢迎。在界面中,hot值既用于展示关注度数据,也用于驱动进度条的宽度计算。
2.4 RaceItem:训放订单数据模型
@Observed
export class RaceItem {
id: number = 0
name: string = ''
item: string = ''
status: string = ''
progress: number = 0
constructor(id: number, name: string, item: string, status: string, progress: number) {
this.id = id; this.name = name; this.item = item; this.status = status
this.progress = progress
}
}
RaceItem是训放订单的数据模型,包含五个字段:
id: number:订单的唯一标识符,数值类型。name: string:订单名称,字符串类型。格式如"雨点号·230km 训放",包含了赛鸽名称和训放距离,清晰描述订单内容。item: string:训放项目详情,字符串类型。描述赛线方向、风力条件、参赛羽数等信息,如"西北线·顶风"、“竞翔·1320羽”。status: string:订单状态,字符串类型。枚举值包括"已归巢"、“在翔”、“集鸽中”、“验鸽中”、"待放飞"等,不同状态对应不同的颜色和进度值。progress: number:进度百分比,数值类型。表示训放任务的完成进度,与status字段联动,用于驱动进度条的渲染。
2.5 GearItem:鸽具器材数据模型
@Observed
export class GearItem {
id: number = 0
name: string = ''
desc: string = ''
icon: string = ''
constructor(id: number, name: string, desc: string, icon: string) {
this.id = id; this.name = name; this.desc = desc; this.icon = icon
}
}
GearItem是鸽具器材的数据模型,包含四个字段:
id: number:器材的唯一标识符,数值类型。name: string:器材名称,字符串类型。如"电子足环"、“赛鸽GPS追踪器”、"清除饲料·清淡"等。desc: string:器材描述,字符串类型。简短说明器材的用途或特点,如"扫码验鸽"、“实时定位”、"集鸽前投喂"等。icon: string:器材图标,字符串类型。使用emoji表情作为图标,如🔖、📡、🌾等,与PostItem的avatar字段设计思路一致。
2.6 辅助接口定义
除了四个核心@Observed模型类之外,代码中还定义了四个普通interface用于辅助数据描述:
WeekChartItem——周训放柱状图数据项:
interface WeekChartItem {
label: string;
value: number;
}
包含label(星期标签)和value(训放次数)两个字段,用于驱动本周训放放飞次数柱状图的渲染。
RaceChartItem——赛程构成图数据项:
interface RaceChartItem {
label: string;
value: number;
color: string;
}
在WeekChartItem的基础上增加了color字段,每个赛程类别有对应的颜色值,用于横向条形图的着色。
CourseItem——课程数据项:
interface CourseItem {
title: string;
week: string;
progress: number;
color: string;
}
课程模型包含标题、周次、进度和颜色四个字段,每门课程有独特的主题色,增强了视觉区分度。
LoftTypeItem——鸽舍玩法类型项:
interface LoftTypeItem {
name: string;
desc: string;
icon: string;
hot: boolean;
}
玩法类型模型包含名称、描述、图标和热门标记四个字段,hot布尔值用于标识是否为热门玩法,在界面上显示"热门"标签。
NavItem——导航项:
interface NavItem {
icon: string;
label: string;
}
底部导航栏的数据模型,包含图标和标签两个字段。
这些普通接口与@Observed类的区别在于:普通接口仅用于数据结构的类型描述,不具备响应式能力,适合静态数据或不需要细粒度追踪的场景;而@Observed类的实例属性变化能够触发UI刷新,适合需要动态更新的业务数据。
三、静态数据数组全景解析

3.1 POST_LIST:鸽友动态列表
POST_LIST是鸽友圈动态的静态数据源,包含8条模拟数据。每条数据生动刻画了不同类型鸽友的日常状态:
- 第1条:"雨点号主人"发布了230公里归巢的动态,获得129个点赞,发布于6分钟前,是最新鲜的内容。
- 第2条:"灰壳血统党"分享了灰壳鸽子阴天定向稳定的经验,获得107个点赞。
- 第3条:"秋赛冲奖人"记录了集鸽前换清淡饲料的备战细节,获得98个点赞。
- 第4条:"鸽语主理人"发布了鸽舍开放日的活动通知,获得90个点赞。
- 第5条:"新手鸽友"分享了第一次放飞30公里的紧张经历,获得82个点赞。
- 第6条:"公棚参赛户"报告了交鸽公棚的情况,获得73个点赞。
- 第7条:"脚环收藏控"展示了十年老足环的收藏成果,获得65个点赞。
- 第8条:"夜放观察者"分享了清晨放飞归巢率高的观察心得,发布于昨天,获得59个点赞。
这8条数据覆盖了赛鸽爱好者的多种用户画像——从新手到老手、从血统党到实战派、从收藏爱好者到科学观察者,构建了一个丰富多元的社区生态。数据按时间倒序排列,点赞数呈现梯度递减,符合社交产品中"越新的内容互动量越高"的一般规律。
3.2 PIGEON_LIST:赛鸽橱窗列表

PIGEON_LIST是赛鸽市集的商品数据源,包含8羽不同品级的赛鸽:
| 序号 | 名称 | 血统 | 价格 | 等级 | 关注度 |
|---|---|---|---|---|---|
| 1 | 灰壳·24-088821 | 詹森直孙 | ¥6800/羽 | 精英 | 96 |
| 2 | 雨点·24-066052 | 胡本血脉 | ¥4500/羽 | 精英 | 92 |
| 3 | 绛鸽·24-112347 | 红绛老血统 | ¥2800/羽 | 主力 | 88 |
| 4 | 石板灰·24-090813 | 幕利门系 | ¥3800/羽 | 主力 | 84 |
| 5 | 麒麟花·24-133905 | 花鸽观赏 | ¥1200/羽 | 潜力 | 78 |
| 6 | 白条·23-201764 | 老国血 | ¥860/羽 | 潜力 | 72 |
| 7 | 深雨点·23-188420 | 500公里冠军直子 | ¥9800/羽 | 冠军血 | 69 |
| 8 | 幼鸽·25-010865 | 春季作育 | ¥600/羽 | 幼鸽 | 61 |
赛鸽数据呈现出清晰的价格梯度和等级体系:从600元的幼鸽到9800元的冠军血系,覆盖了不同预算层次的消费者。等级标签包括幼鸽、潜力、主力、精英、冠军血五个层级,关注度数值也与等级基本正相关。羽色涵盖了灰壳、雨点、绛鸽、石板灰、麒麟花、白条、深雨点等多个品种,充分展示了赛鸽的多样性。
3.3 RACE_LIST:训放订单列表
RACE_LIST是训放管理的订单数据源,包含8条不同状态的训放记录:
- “雨点号·230km 训放”——状态"已归巢",进度100%,西北线顶风
- “灰壳二号·320km”——状态"在翔",进度74%,北线侧风
- “秋赛第一关·500km”——状态"集鸽中",进度52%,竞翔1320羽
- “幼鸽路训·80km”——状态"验鸽中",进度38%,分组12羽
- “公棚热身赛·200km”——状态"待放飞",进度26%,交鸽3羽
- “绛鸽·160km 复放”——状态"在翔",进度18%,东南线
- “特比环资格赛”——状态"集鸽中",进度12%,环号登记
- “年度总决赛·550km”——状态"待放飞",进度6%,报名审核
训放订单覆盖了从短距离(80km)到长距离(550km)的多种赛程,状态涵盖了已归巢、在翔、集鸽中、验鸽中、待放飞等全生命周期阶段。进度值按从高到低排列,直观展示了不同订单的推进程度。
3.4 GEAR_LIST:鸽具器材列表
GEAR_LIST是器材库的商品数据源,包含8类鸽具用品:
- 电子足环(🔖)——扫码验鸽
- 赛鸽GPS追踪器(📡)——实时定位
- 清除饲料·清淡(🌾)——集鸽前投喂
- 营养土·矿物质(🪨)——日常保健
- 栖架·木质(🪵)——鸽舍改造
- 巢盆·草编(🪺)——繁殖季备用
- 鸽舍消毒剂(🧴)——每周喷洒
- 训放笼·便携(🎒)——20羽装
器材种类涵盖了身份识别(足环、GPS)、饲料营养(清除饲料、营养土)、鸽舍设施(栖架、巢盆)、卫生防疫(消毒剂)、运输工具(训放笼)等多个品类,覆盖了赛鸽饲养的全方位需求。
3.5 WEEK_CHART与RACE_CHART:图表数据
WEEK_CHART是本周训放放飞次数的柱状图数据,包含周一到周日共7天的数据:周一3次、周二5次、周三4次、周四6次、周五8次、周六13次、周日10次。数据呈现出明显的"周末高峰"特征——周六周日的训放次数显著高于工作日,这符合鸽友利用业余时间进行训放的实际生活规律。周末两天的训放次数合计23次,占全周49次的约47%(界面中展示的"53%"可能采用了不同的统计口径)。
RACE_CHART是本季参赛赛程构成的横向条形图数据,包含5个类别:
- 短程100-300:34%,主色调灰蓝
- 中程300-500:28%,强调色鸽红
- 长程500-700:18%,成功色绿
- 超远程1000+:12%,警告色黄
- 公棚赛:8%,危险色红棕
赛程构成以短程和中程为主(合计62%),长程和超远程为辅,公棚赛占比最小。这反映了普通鸽友的参赛结构——短中距离训放是日常基础,长距离和超远程是进阶挑战,公棚赛则是少数人的选择。
3.6 TYPE_LIST与COURSE_LIST:玩法与课程
TYPE_LIST是首页"在鸽语能玩什么"四格功能的数据源,包含4个核心玩法:赛鸽市集(🕊️,热门)、训放托管(📍,热门)、秋赛报名(🏆)、鸽舍诊断(🏠)。前两个标注为热门,反映了产品的核心功能定位。
COURSE_LIST是养训课堂的课程数据源,包含5门递进式课程:
- 信鸽品种与羽色识别——第1周,进度100%,灰蓝色
- 幼鸽作育与开家——第2周,进度86%,鸽红色
- 路训节奏与体能调控——第3周,进度54%,绿色
- 竞翔定向原理——第4周,进度29%,黄色
- 鸽舍通风与疾病预防——第5周,进度5%,红棕色
课程按周次递进,进度依次递减,模拟了用户从入门到进阶的学习路径。每门课程有独特的主题色,形成了彩虹般的视觉序列。
3.7 导航配置数据
NAV_LIST是底部主导航的配置数据,包含4个主tab:
- 🕊️ 首页:应用的核心信息聚合页
- 🏠 鸽舍:赛鸽橱窗与鸽舍管理
- 📚 课堂:养训知识学习
- 👤 我的:个人中心
底部4tab的结构遵循了经典的移动应用导航模式——首页聚合信息、中间页承载核心功能、末页放置个人中心。
SUB_NAV_LIST是首页的子导航配置,包含7个内容tab:精选、赛鸽橱窗、我的鸽舍、竞翔赛事、养训课堂、鸽友圈、器材库。这7个子tab覆盖了应用的全部主要功能模块,用户在首页即可通过横向滑动快速切换不同内容区域,无需跳转到底部其他tab,大大提升了操作效率。
四、纯函数工具库深度解析
4.1 barH:柱状图高度计算函数
function barH(v: number, max: number): number {
return Math.round(118 * v / max);
}
barH函数用于计算柱状图中柱子的高度。它接收两个参数:v表示当前柱子的数值,max表示所有柱子中的最大值。函数返回按比例计算后的像素高度,基准高度为118像素。Math.round方法确保返回值为整数像素,避免亚像素渲染导致的模糊问题。这个函数体现了数据可视化中的等比缩放思想——将数据值线性映射到像素高度,最大值对应满高度,其余值按比例缩小。
4.2 pigeonColor:柱状图颜色映射函数
function pigeonColor(v: number): string {
if (v > 9) {
return '#5C6B7A';
} else if (v > 6) {
return '#A63D40';
}
return '#D9A05B';
}
pigeonColor函数根据数值大小返回不同的颜色,用于柱状图的分级着色。数值大于9时返回灰蓝色(主色调),大于6时返回鸽红色(强调色),其余情况返回赭黄色(警告色)。这种分级着色方式使得用户能够通过颜色快速判断数值的大小等级——高值冷色调、中值暖色调、低值黄调,形成了直观的视觉编码体系。
4.3 鸽子剪影浮动特效函数组
鸽子剪影浮动特效由三个函数协同实现:doveX、doveY、doveA。
function doveX(tick: number, i: number): number {
return 40 + ((tick * 6 + i * 211) % 600);
}
doveX函数计算鸽子剪影的水平位置。tick是时间帧计数器,i是鸽子的索引编号。公式中tick * 6表示随时间推移的水平位移速度,i * 211为每只鸽子设置不同的起始偏移,确保它们不会重叠在一起。% 600将位置限制在600像素范围内,形成循环往复的飞行效果。最终加上40像素的左边距,确保鸽子从页面左侧一定距离处开始出现。
function doveY(tick: number, i: number): number {
return 70 + i * 170 + ((tick + i) % 5) * 6;
}
doveY函数计算鸽子剪影的垂直位置。i * 170为每只鸽子分配不同的垂直高度区间,4只鸽子分别位于70、240、410、580像素的高度附近,形成错落有致的分布。((tick + i) % 5) * 6则产生了上下浮动的效果——每5个tick周期完成一次上下浮动,浮动幅度为6像素。这种小幅上下波动模拟了鸽子飞行时翅膀扇动带来的身体起伏,增强了动画的真实感。
function doveA(tick: number, i: number): number {
return 0.25 + ((tick + i) % 4) * 0.08;
}
doveA函数计算鸽子剪影的透明度。基础透明度为0.25(即25%不透明),保证鸽子剪影始终处于半透明状态,不会对上层内容造成干扰。((tick + i) % 4) * 0.08产生了透明度的周期变化,变化范围为0到0.24,即透明度在0.25到0.49之间波动。这种若隐若现的效果增强了鸽子剪影的灵动感,同时也模拟了鸽子在远处飞行时的视觉模糊感。
4.4 气流光环扩散特效函数组
气流光环扩散特效由四个函数实现:ringR、ringA、ringX、ringY。
function ringR(tick: number, i: number): number {
return 16 + ((tick + i) % 5) * 9;
}
ringR函数计算气流光环的半径。基础半径为16像素,((tick + i) % 5) * 9使半径在16到52像素之间周期性变化,每5个tick完成一次扩张收缩循环。这种半径变化模拟了气流从中心向外扩散然后消散的视觉效果。
function ringA(tick: number, i: number): number {
return 0.04 + ((tick + i) % 5) * 0.03;
}
ringA函数计算气流光环的透明度。基础透明度仅为0.04(4%不透明),非常微弱。((tick + i) % 5) * 0.03使透明度在0.04到0.16之间波动。这种极低的透明度设计确保了气流光环作为背景装饰的定位,不会喧宾夺主。
function ringX(tick: number, i: number): number {
return 70 + ((tick * 4 + i * 197) % 530);
}
function ringY(tick: number, i: number): number {
return 90 + ((tick * 5 + i * 181) % 560);
}
ringX和ringY函数计算气流光环的位置。水平方向速度为tick * 4,垂直方向速度为tick * 5,垂直方向移动略快于水平方向,形成斜向漂移的效果。i * 197和i * 181为每个光环设置不同的起始位置。水平移动范围530像素,垂直移动范围560像素,加上起始偏移后,光环在整个页面区域内缓慢漂移。
4.5 statusColor与statusProgress:训放状态映射
function statusColor(s: string): string {
if (s === '已归巢') {
return '#7BA884';
} else if (s === '在翔') {
return '#A63D40';
} else if (s === '集鸽中') {
return '#5C6B7A';
} else if (s === '验鸽中') {
return '#D9A05B';
}
return '#A6B0BC';
}
statusColor函数根据训放状态返回对应的颜色值。每种状态都有明确的色彩语义:已归巢对应绿色(成功完成)、在翔对应鸽红色(正在进行中,最受关注)、集鸽中对应灰蓝色(准备阶段)、验鸽中对应赭黄色(进行中但关注度较低)、其他状态对应灰蓝色的浅色变体(默认状态)。这种颜色编码使得用户只需扫一眼状态标签的颜色,就能快速了解训放的当前阶段。
function statusProgress(s: string): number {
if (s === '已归巢') {
return 100;
} else if (s === '在翔') {
return 72;
} else if (s === '集鸽中') {
return 50;
} else if (s === '验鸽中') {
return 32;
}
return 10;
}
statusProgress函数根据训放状态返回对应的进度百分比。已归巢100%(完成)、在翔72%(飞行途中)、集鸽中50%(准备阶段过半)、验鸽中32%(检验阶段)、其他10%(初始阶段)。进度值与状态的对应关系反映了训放任务的生命周期阶段,进度条的可视化展示让用户能够直观感知任务的推进程度。
4.6 pigeonTagColor:羽色标签色函数
function pigeonTagColor(g: string): string {
if (g.indexOf('雨点') >= 0) {
return '#5C6B7A';
} else if (g.indexOf('绛') >= 0 || g.indexOf('麒麟花') >= 0) {
return '#A63D40';
} else if (g.indexOf('灰') >= 0 || g.indexOf('石板') >= 0) {
return '#7BA884';
}
return '#D9A05B';
}
pigeonTagColor函数根据赛鸽名称中的羽色关键词返回对应的颜色标识。雨点系鸽子对应灰蓝色(主色调),绛鸽和麒麟花对应鸽红色(强调色),灰系和石板灰对应绿色(成功色),其他羽色对应赭黄色(警告色)。这种将羽色与UI颜色关联的设计,增强了赛鸽个体的视觉辨识度,也使得颜色系统与赛鸽主题更加贴合。
4.7 levelColor:等级标签色函数
function levelColor(s: string): string {
if (s === '幼鸽') {
return '#7BA884';
} else if (s === '潜力') {
return '#D9A05B';
} else if (s === '主力') {
return '#5C6B7A';
} else if (s === '精英') {
return '#A63D40';
}
return '#C76B5A';
}
levelColor函数根据赛鸽等级返回对应的颜色。幼鸽对应绿色(新生、成长)、潜力对应黄色(有潜力、需关注)、主力对应灰蓝色(中坚力量、稳定)、精英对应鸽红色(顶级、突出)、其他(如冠军血)对应红棕色(珍稀、高端)。等级颜色从绿到黄到蓝到红到红棕,形成了一条价值递增的色彩光谱,用户可以通过颜色快速判断赛鸽的品级高低。
五、组件状态管理与生命周期
5.1 组件定义与状态变量
@Entry
@Component
struct PagePigeonRacing {
@State mainTab: number = 0
@State subTab: number = 0
@State tick: number = 0
@State glow: number = 0
@State addOpen: boolean = false
@State editOpen: boolean = false
@State delOpen: boolean = false
@State editIdx: number = 0
@State delTarget: string = 'race'
@State editName: string = ''
@State editStyle: string = ''
@State editNote: string = ''
@State addTitle: string = ''
@State addContent: string = ''
@State postList: PostItem[] = POST_LIST
@State raceList: RaceItem[] = RACE_LIST
@State fxTimer: number = -1
PagePigeonRacing是应用的主页面组件,使用@Entry和@Component装饰器标记。@Entry表示这是一个页面入口组件,@Component表示这是一个自定义组件。
组件内部定义了16个@State状态变量,可以分为以下几类:
导航状态类:
mainTab:底部主导航的当前选中索引,默认0(首页)subTab:首页子导航的当前选中索引,默认0(精选)
特效状态类:
tick:特效时间帧计数器,驱动鸽子剪影和气流光环的动画glow:光晕闪烁计数器,用于控制呼吸灯效果
弹框状态类:
addOpen:新增弹框(发鸽语)的开关状态editOpen:编辑弹框(编辑训放单)的开关状态delOpen:删除弹框(取消训放/退课)的开关状态
弹框数据类:
editIdx:当前编辑的训放单索引delTarget:删除操作的目标类型(‘race’或’course’)editName:编辑弹框中的名称输入值editStyle:编辑弹框中的状态输入值editNote:编辑弹框中的备注输入值addTitle:新增弹框中的标题输入值addContent:新增弹框中的内容输入值
列表数据类:
postList:鸽友动态列表,初始值为POST_LIST静态数据raceList:训放订单列表,初始值为RACE_LIST静态数据
计时器引用类:
fxTimer:特效定时器的ID,用于在页面销毁时清除定时器
每个@State变量都有明确的职责边界,遵循了单一职责原则。状态变量的数量虽然较多,但分类清晰,便于维护和理解。
5.2 生命周期方法
aboutToAppear(): void {
this.fxTimer = setInterval(() => {
this.tick = this.tick + 1;
this.glow = (this.glow + 1) % 2;
}, 90);
}
aboutToAppear是组件的生命周期方法,在组件即将出现时调用。在这个方法中,启动了一个特效定时器,每90毫秒执行一次回调函数。回调函数中执行两个操作:将tick加1(驱动特效帧动画)、将glow在0和1之间切换(实现闪烁效果)。90毫秒的间隔约合每秒11帧,这个帧率对于装饰性特效来说足够流畅,同时不会消耗过多性能。
aboutToDisappear(): void {
if (this.fxTimer > 0) {
clearInterval(this.fxTimer);
this.fxTimer = -1;
}
}
aboutToDisappear是组件的另一个生命周期方法,在组件即将消失时调用。在这个方法中,清除了特效定时器,并将fxTimer重置为-1。及时清除定时器是防止内存泄漏的重要措施,如果定时器未被清除,即使组件已经销毁,回调函数仍然会持续执行,导致内存无法释放。
5.3 交互方法
组件定义了7个交互方法,分别处理不同的用户操作:
openAdd():打开发鸽语弹框。方法中先将addTitle和addContent清空,确保每次打开弹框都是空白状态,然后将addOpen设为true。这种"打开前重置"的模式确保了弹框内容的一致性,避免上次输入的残留数据影响下次使用。
openEdit(index: number):打开编辑训放单弹框。方法接收一个索引参数,先进行边界检查(确保索引在有效范围内),然后将对应训放单的数据填充到编辑表单字段中(editName、editStyle、editNote),最后将editOpen设为true。边界检查是防御式编程的体现,防止无效索引导致运行时错误。
openDelA():打开取消训放的删除确认弹框。将delTarget设为’race’,然后打开del弹框。
openDelB():打开退出课程的删除确认弹框。将delTarget设为’course’,然后打开del弹框。
这两个方法复用了同一个删除弹框组件,通过delTarget状态来区分不同的删除场景。这种复用设计减少了代码冗余,同时保持了交互的一致性。
doAdd():执行发布鸽语操作。先检查addTitle是否有内容(非空校验),如果有内容则在postList头部插入一条新的PostItem,然后关闭弹框。使用unshift方法将新动态添加到列表开头,符合社交产品"最新内容在最前面"的展示习惯。新动态的id设为999,昵称设为"我的鸽语",头像设为🕊️,时间设为"刚刚",点赞数设为0。
doEdit():执行保存编辑操作。先检查editIdx的有效性,然后使用splice方法替换raceList中对应索引位置的元素,最后关闭弹框。splice方法能够精确替换数组中的指定位置元素,确保数据的原位更新。
doDel():执行删除操作。根据delTarget的值判断删除目标,如果是’race’则删除raceList的第一条记录,如果是’course’则删除postList的第一条记录,然后关闭弹框。删除操作采用了"删首条"的简化设计,在实际产品中可能需要根据用户选择的具体项进行删除。
六、@Builder组件方法深度解析
6.1 fxLayer:特效层构建器
@Builder
fxLayer() {
Stack({ alignContent: Alignment.TopStart }) {
ForEach([0, 1, 2, 3], (i: number) => {
Text('🕊️')
.fontSize(16)
.opacity(doveA(this.tick, i))
.rotate({ angle: -8 })
.translate({ x: doveX(this.tick, i), y: doveY(this.tick, i) })
}, (i: number) => i.toString())
ForEach([0, 1, 2, 3], (i: number) => {
Column()
.width(ringR(this.tick, i) * 2)
.height(ringR(this.tick, i) * 2)
.borderRadius(ringR(this.tick, i))
.backgroundColor(COLORS.wing)
.opacity(ringA(this.tick, i))
.translate({ x: ringX(this.tick, i), y: ringY(this.tick, i) })
}, (i: number) => i.toString())
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
}
fxLayer是特效层的构建器方法,它使用Stack布局堆叠了两组ForEach循环:鸽子剪影和气流光环。每组各有4个元素(索引0-3),总共8个特效元素。
鸽子剪影使用Text组件渲染emoji🕊️,通过opacity设置透明度、rotate设置旋转角度(-8度,模拟鸽子向左飞行的姿态)、translate设置位移。每帧tick变化时,doveX、doveY、doveA三个函数会计算出新的位置和透明度,从而产生飞行和浮动的动画效果。
气流光环使用Column组件(设置宽高相等且borderRadius等于半径,从而形成圆形),背景色使用wing羽色。通过width和height的动态变化产生扩张收缩效果,opacity的变化产生明暗呼吸效果,translate的变化产生漂移效果。
特效层最关键的设置是.hitTestBehavior(HitTestMode.None),这行代码将特效层的点击测试模式设为None,意味着所有点击事件都会穿透特效层,不会被特效元素拦截。这是实现"装饰性特效不影响交互"的核心技术手段——如果没有这个设置,鸽子剪影和气流光环所在的位置将会阻挡用户点击下层的按钮和列表项,严重影响用户体验。
6.2 header:头部区域构建器
header方法构建了页面顶部的头部区域,包含四个部分:搜索条+购物车、训放进度卡、秋赛报名横幅、数据四格。
搜索条+购物车是最顶部的功能入口。左侧是一个鸽子emoji图标,中间是搜索框(显示"搜一搜「詹森」「特比环」"的提示文字),右侧是购物车图标带红色角标(显示数字1)。搜索框使用Row布局包裹图标和提示文字,设置了白色背景和圆角,模拟了真实搜索框的视觉效果。购物车使用Stack布局实现了图标+角标的叠加效果,角标通过translate偏移到右上角。
训放进度卡是头部最醒目的元素,使用灰蓝色(COLORS.primary)作为背景色。卡片左侧显示训放信息:标题"雨点号·230km 在翔"、副标题"预计14:20归巢·分速1280米/分"、以及"顶风逆飞"标签和"同期在翔4羽"说明。右侧是"查看雷达"按钮,使用wing羽色作为背景色。点击"查看雷达"按钮会触发openEdit方法,打开编辑训放单弹框。
秋赛报名横幅使用白色卡片背景,左侧是奖杯emoji图标,中间是报名信息(标题"秋赛特比环报名·剩2天",副标题"500km争霸赛·冠军奖金¥20000"),右侧是鸽红色的"报名"按钮。点击报名按钮会触发openAdd方法,打开发鸽语弹框(在这个原型中,报名按钮和发鸽语复用了同一个弹框)。
数据四格是四个并排的统计卡片,分别展示"在翔羽数"(4羽,灰蓝色)、“累计训放”(186次,鸽红色)、“最佳分速”(1420米,绿色)、“鸽币”(¥120,赭黄色)。四个卡片使用相同的布局结构——标题文字(9号字、提示色)和数值文字(13号字、粗体、主题色),通过layoutWeight(1)平均分配宽度。每个统计项使用不同的主题色,形成了彩虹般的视觉效果,同时也强化了数据的语义区分。
6.3 subNav:子导航构建器
@Builder
subNav() {
Scroll() {
Row({ space: 6 }) {
ForEach(SUB_NAV_LIST, (item: string, idx: number) => {
Column({ space: 5 }) {
Row()
.width('100%')
.height(3)
.borderRadius(2)
.backgroundColor(this.subTab === idx ? COLORS.accent : COLORS.border)
Text(item)
.fontSize(this.subTab === idx ? 13 : 11)
.fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.subTab === idx ? COLORS.primaryDark : COLORS.textSecondary)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(this.subTab === idx ? COLORS.primaryLight : COLORS.bg)
.borderRadius(8)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.subTab = idx;
})
}, (item: string) => item)
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.padding({ top: 6, bottom: 10 })
}
subNav方法构建了首页的子导航栏,这是一个"结构顶条式"的横向滚动导航。
导航使用Scroll组件包裹Row布局,设置为横向滚动且隐藏滚动条。ForEach遍历SUB_NAV_LIST数组生成7个导航项。
每个导航项使用Column布局,顶部是一个3像素高的指示条(选中时为鸽红色accent,未选中时为边框色border),底部是文字标签。选中态和未选中态在多个维度上形成差异:
- 指示条颜色:accent vs border
- 文字大小:13px vs 11px
- 文字粗细:Bold vs Normal
- 文字颜色:primaryDark vs textSecondary
- 背景颜色:primaryLight vs bg
这种多维度的选中态反馈设计,确保了用户能够清晰地感知当前所处的导航位置。点击导航项时,通过onClick回调将subTab状态更新为对应索引,从而触发UI刷新,切换选中状态和下方内容区域。
"结构顶条式"导航的设计特点是:导航项顶部有指示条、整体呈胶囊状(圆角背景)、选中态有背景填充。这种设计风格介于传统标签页(Tab)和按钮组(ButtonGroup)之间,既有标签页的清晰分类感,又有按钮组的明确点击反馈。
6.4 pageFeatured:精选页面构建器
pageFeatured是首页精选tab的内容构建器,包含五个主要模块:玩法四格、周训放柱状图、赛程构成、人气赛鸽橱窗、鸽友动态。
玩法四格模块展示了应用的四大核心玩法:赛鸽市集、训放托管、秋赛报名、鸽舍诊断。每个玩法使用Column布局,包含图标、名称、描述、热门标签四个元素。四个卡片通过layoutWeight(1)平均分配宽度,形成四列等分布局。"热门"标签使用红棕色(danger)文字,只有hot为true的项才显示红色文字,否则显示边框色(相当于隐藏)。
周训放柱状图模块展示了本周每天的训放次数统计。模块头部是标题和单位说明,主体部分使用Row布局横向排列7个柱子,每个柱子使用Column布局,包含柱体(高度由barH函数计算、颜色由pigeonColor函数计算)、数值标签、星期标签。柱子的底部对齐(alignItems(VerticalAlign.Bottom)),形成从底部向上生长的柱状图效果。模块底部还有"周末训放占比53%"的统计说明。
赛程构成模块以横向条形图的形式展示了不同赛程类别的占比。每条记录包含标签文字、进度条(Stack布局实现,底层是灰色背景条,上层是彩色进度条)、百分比数字。5个类别的条形图使用各自的主题色,与RACE_CHART数据中的color字段对应。
人气赛鸽橱窗模块以双列网格的形式展示了8羽人气赛鸽。这里使用了一个巧妙的双列布局实现方式:ForEach遍历所有赛鸽,但只在偶数索引(idx % 2 === 0)时渲染一行,每行内部分别渲染当前索引和索引+1的两个赛鸽卡片。这种方式虽然代码略显冗长(左右两列各写了一遍布局代码),但能够精确控制双列布局,且不需要额外的数组处理。每个赛鸽卡片包含图标+等级标签(在同一行内)、赛鸽名称、价格+关注度信息。等级标签使用鸽红色浅色背景和levelColor函数计算的文字颜色。
鸽友动态模块展示了最新的3条鸽友圈动态(使用slice(0, 3)截取前3条)。每条动态使用Row布局,左侧是头像emoji,右侧是Column布局的内容区——顶部是昵称+时间的行、中间是动态文字、底部是点赞数。动态文字设置了maxLines(2)和textOverflow(Ellipsis),超长内容会显示省略号。右侧有"发鸽语"入口,点击可打开发鸽语弹框。
6.5 pagePigeons:赛鸽橱窗页面构建器
pagePigeons方法构建了赛鸽橱窗页面,以列表形式展示全部赛鸽。页面顶部是标题栏(“赛鸽橱窗”+"挂售我的赛鸽"入口),下方是赛鸽列表。
每个赛鸽项是一个白色卡片,内部结构如下:
- 顶部是赛鸽信息行:左侧是4像素宽的羽色标识条(颜色由pigeonTagColor函数根据名称计算),右侧是赛鸽详情(名称+等级标签行、价格+血统行)
- 中部是关注度进度条:Stack布局实现,底层灰色、上层彩色(颜色同样由pigeonTagColor计算),进度值为hot百分比
- 底部是操作行:左侧显示关注度数值,右侧是"加入购物车"按钮
羽色标识条的设计是这个页面的亮点——仅用一条4像素宽的彩色竖线,就为每个赛鸽项赋予了独特的视觉标识,同时与羽色概念直接关联,设计简洁而表意丰富。
6.6 pageLoft:我的鸽舍页面构建器
pageLoft方法构建了我的鸽舍页面,展示训放订单列表。页面顶部是标题"我的鸽舍",下方是训放订单列表。
每个训放订单卡片的结构:
- 顶部是订单信息行:左侧定位pin图标,中间是订单详情(名称+状态标签行、赛线信息行)
- 中部是进度条:Stack布局实现,进度值由statusProgress函数计算,颜色由statusColor函数计算
- 底部是操作行:左侧显示进度百分比,右侧是"编辑训放单"链接(点击打开编辑弹框)
状态标签使用accentLight浅粉色背景和statusColor计算的文字颜色,在白色卡片上形成了清晰的视觉焦点。进度条的颜色与状态标签颜色一致,形成了色彩上的呼应。
6.7 pageRaces:竞翔赛事页面构建器
pageRaces方法构建了竞翔赛事页面,包含赛事信息四格、参赛鸽排行榜和器材速购三个模块。
赛事信息四格是四个并排的信息卡片,分别展示集鸽截止(周六18:00,灰蓝色)、参赛羽数(1320羽,鸽红色)、赛线(西北500km,赭黄色)、天气(多云转晴,绿色)。四个卡片使用相同的布局结构,每个卡片的数值使用不同的主题色,形成了色彩丰富的信息展示区。
参赛鸽排行榜展示了前5羽参赛鸽的排名信息。每条记录包含排名数字(前三名使用鸽红色,其余使用提示色)、赛鸽信息(名称+血统+价格)、关注度数值。排名数字的颜色区分强化了前三名的荣誉地位。
器材速购模块以双列网格形式展示了8件鸽具器材,布局方式与人气赛鸽橱窗类似——偶数索引时渲染一行,每行两列。每个器材卡片包含图标、名称、描述和"加购"链接。
6.8 pageCourse:养训课堂页面构建器
pageCourse方法构建了养训课堂页面,展示课程列表和退课按钮。
课程列表中的每门课程卡片包含:
- 顶部:课程标题+周次
- 中部:进度条(Stack布局,颜色由课程数据中的color字段决定)
- 底部:已学标签+进度百分比
课程进度条的颜色与课程主题色一致,五门课程的进度条呈现出灰蓝、鸽红、绿、黄、红棕的彩虹序列,视觉效果丰富而有序。
页面底部是一个退课按钮,使用danger危险色作为背景色。按钮的实现方式比较特别——先渲染一个透明的Button组件作为点击热区,然后用Text组件显示按钮文字,通过margin负偏移将文字定位到按钮上方。这种方式可能是为了规避Button组件默认文字样式的限制。点击退课按钮会调用openDelB方法,打开退课确认弹框。
6.9 pageCircle:鸽友圈页面构建器
pageCircle方法构建了鸽友圈页面,展示完整的鸽友动态列表。页面顶部是标题栏(“鸽友圈”+"发鸽语"入口),下方是动态列表。
每条动态的布局结构与精选页面中的鸽友动态类似,但有几处不同:
- 头像更大(26px vs 22px)
- 动态文字字号更大(13px vs 12px),且不限制行数
- 底部增加了"💬 回复"入口,提供了互动功能
完整的鸽友圈页面展示了全部8条动态,而精选页面只展示前3条。这种设计符合"首页展示精华,详情页展示全部"的信息架构原则。
6.10 pageGear:器材库页面构建器
pageGear方法构建了器材库页面,包含分类导航和器材列表两个模块。
分类导航是三个并排的分类卡片:饲料区(🌾)、追踪区(📡)、鸽舍区(🏠)。每个卡片包含大图标、分类名称、分类描述,使用居中对齐的Column布局。三个卡片通过layoutWeight(1)平均分配宽度。
器材列表以列表形式展示全部8件器材。每条记录使用Row布局,左侧是图标,中间是器材信息(名称+描述),右侧是"加购"按钮。列表布局相比双列网格更适合信息密度较高的场景,用户可以一览更多器材信息。
页面底部有一个"取消训放"按钮,样式与退课按钮相同(danger色背景、Button+Text的组合方式),点击调用openDelA方法打开取消训放的确认弹框。
6.11 pageMine:我的页面构建器
pageMine方法构建了个人中心页面,包含用户信息卡、训放档案、鸽语记录三个模块。
用户信息卡展示了用户的基本信息:头像(鸽子emoji,40px大图标)、昵称"鸽语竞翔人"、会员信息(金翼会员·训放186次·在养24羽·打卡96天)。信息卡使用白色背景和圆角边框,是个人中心的标志性组件。
我的训放档案模块展示了训放订单的统计信息和前3条记录。头部显示订单总数,列表中每条记录显示订单名称和状态。状态文字使用statusColor函数计算颜色,保持了与其他页面一致的视觉规范。
我的鸽语模块展示了用户发布的动态统计和前3条记录。结构与训放档案模块类似,头部显示动态总数,列表展示昵称和时间。
个人中心页面采用了"信息聚合+快速预览"的设计思路——用户可以在这里快速查看自己的核心数据和最新动态,如需完整列表则跳转到对应详情页面。
6.12 弹框组件构建器
应用包含三个弹框组件:addModalBody(发鸽语)、editModalBody(编辑训放单)、delModalBody(删除确认),以及一个共享的遮罩层modalOverlay。
modalOverlay是弹框的半透明遮罩层,使用黑色背景+60%不透明度实现。遮罩层铺满整个屏幕,点击遮罩层会关闭所有弹框。遮罩层是弹框交互的标准配置——既提供了视觉上的背景虚化效果(突出弹框内容),又提供了交互上的关闭入口(点击空白处关闭)。
addModalBody是发鸽语弹框的内容区,包含标题"发布鸽语"、单行输入框(标题输入)、多行输入框(内容输入)、取消/发布按钮组。两个输入框都绑定了onChange事件,实时更新对应的状态变量。按钮组使用Row布局,两个Button各占一半宽度,取消按钮是灰色背景+深色文字,发布按钮是主色背景+白色文字,形成了清晰的主次区分。
editModalBody是编辑训放单弹框的内容区,结构与发鸽语弹框类似,但包含三个输入字段:训放名称、状态、内容与备注。前两个是单行输入框,第三个是多行输入框。底部同样是取消/保存按钮组。
delModalBody是删除确认弹框的内容区,这是一个简化的确认对话框。标题和提示文字会根据delTarget的值动态变化——如果是’race’则显示"取消训放订单"相关内容,如果是’course’则显示"退出养训课堂"相关内容。底部是取消/确认按钮组,确认按钮使用danger危险色,强化了"危险操作"的视觉警示。
三个弹框虽然内容不同,但布局结构高度一致——白色圆角卡片、顶部标题、中间内容区、底部双按钮。这种一致性设计降低了用户的认知成本,提升了产品的专业感。
6.13 bottomBar:底部导航栏构建器
@Builder
bottomBar() {
Row() {
ForEach(NAV_LIST, (it: NavItem, idx: number) => {
Column({ space: 2 }) {
Text(it.icon)
.fontSize(20)
.opacity(this.mainTab === idx ? 1 : 0.55)
Text(it.label)
.fontSize(10)
.fontWeight(this.mainTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.mainTab === idx ? COLORS.primary : COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.mainTab = idx;
})
}, (it: NavItem) => it.label)
}
.width('100%')
.height(56)
.backgroundColor(COLORS.cardBg)
.borderRadius({ topLeft: 16, topRight: 16 })
}
bottomBar方法构建了底部主导航栏。导航栏高度56像素,白色背景,顶部左右各16像素圆角,形成了悬浮感的视觉效果。
导航栏使用Row布局,ForEach遍历NAV_LIST生成4个导航项。每个导航项使用Column布局,包含图标emoji和文字标签。选中态和未选中态通过三个维度区分:
- 图标透明度:1.0 vs 0.55
- 文字粗细:Bold vs Normal
- 文字颜色:primary vs textHint
选中态的图标使用完全不透明,文字加粗且使用主色调,未选中态则半透明、常规字重、灰色文字。这种设计既保证了选中项的视觉突出,又保持了导航栏整体的简洁优雅。
点击导航项时,通过onClick回调更新mainTab状态,触发页面内容的切换。底部导航栏是应用全局导航的核心载体,4个主tab覆盖了信息浏览(首页)、功能操作(鸽舍)、知识学习(课堂)、个人管理(我的)四大使用场景。
6.14 mainContent:主内容区构建器
mainContent方法构建了页面的主内容区,是整个应用的内容调度中心。它根据mainTab的值决定展示哪个页面的内容,在首页(mainTab === 0)时还会根据subTab的值决定展示哪个子页面。
具体的调度逻辑:
- mainTab === 0(首页):显示header + subNav + 对应子页面内容
- subTab === 0:精选页面(pageFeatured)
- subTab === 1:赛鸽橱窗页面(pagePigeons)
- subTab === 2:我的鸽舍页面(pageLoft)
- subTab === 3:竞翔赛事页面(pageRaces)
- subTab === 4:养训课堂页面(pageCourse)
- subTab === 5:鸽友圈页面(pageCircle)
- subTab === 6:器材库页面(pageGear)
- mainTab === 1(鸽舍):显示赛鸽橱窗页面(pagePigeons)
- mainTab === 2(课堂):显示养训课堂页面(pageCourse)
- mainTab === 3(我的):显示个人中心页面(pageMine)
每个页面内容都包裹在Scroll组件中,设置为纵向滚动且隐藏滚动条。Scroll组件内的Column占满宽度,内容超出屏幕时可以上下滑动浏览。页面左右各有14像素的内边距,底部有20像素的内边距,确保内容不会贴边显示。
首页的内容区使用layoutWeight(1)填充剩余空间,header和subNav则根据内容自适应高度。这种"头部固定+内容滚动"的布局是移动应用的经典模式——用户在滚动浏览内容时,关键的导航和信息始终保持在可视区域内。
6.15 build:根构建方法
build方法是组件的根构建方法,它定义了整个页面的层级结构:
build() {
Stack() {
// 第一层:背景层
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
// 第二层:特效层
this.fxLayer()
// 第三层:内容层
Column() {
this.mainContent()
this.bottomBar()
}
.width('100%')
.height('100%')
// 第四层:弹框层
if (this.addOpen) { ... }
if (this.editOpen) { ... }
if (this.delOpen) { ... }
}
.width('100%')
.height('100%')
}
根组件使用Stack布局,从下到上分为四个层级:
第一层:背景层——一个铺满屏幕的Column组件,背景色为COLORS.bg。这是最底层的视觉基础。
第二层:特效层——fxLayer方法渲染的鸽子剪影和气流光环特效。特效层在背景层之上、内容层之下,作为装饰性元素点缀界面。由于设置了hitTestBehavior(HitTestMode.None),特效层不会拦截任何点击事件。
第三层:内容层——包含mainContent(主内容区)和bottomBar(底部导航栏)的Column组件。这是用户交互的主要区域,所有功能性UI元素都在这一层。
第四层:弹框层——根据弹框开关状态条件渲染的三个弹框。每个弹框使用Stack布局,包含遮罩层和弹框内容两部分。弹框内容设置了zIndex(999)确保在最上层显示,宽度88%,最大高度80%,16像素圆角。
四个弹框(新增、编辑、删除)通过if条件语句控制显隐——当对应的Open状态为true时,弹框才会被渲染到组件树中。这种条件渲染方式相比于"始终渲染但控制透明度"的方式,在弹框关闭时能够完全从渲染树中移除弹框组件,节省了渲染资源。
整个Stack布局的层级设计体现了"从底到顶、从静到动"的组织原则——背景是静态的基础层,特效是装饰性的动画层,内容是核心的交互层,弹框是临时性的模态层。每一层都有明确的职责和定位,共同构成了完整的界面体系。
七、应用架构流程图
上面的流程图展示了鸽语应用的完整架构。从数据层到组件层再到渲染层,形成了一条清晰的单向数据流。用户交互触发事件回调,事件回调更新@State状态变量,状态变化触发框架的差异计算和重渲染机制,最终更新到UI界面。
数据层是整个应用的基石——ColorPalette提供统一的颜色规范,@Observed数据模型定义业务实体结构,静态数据数组提供初始数据内容,工具函数库提供计算和映射能力。这些数据和方法被组件层消费和使用。
组件层是应用的核心——PagePigeonRacing主组件作为容器协调各个子模块,fxLayer负责特效展示,header和subNav负责导航,7个pageXxx Builder负责不同页面的内容渲染,Modal系列Builder负责弹框交互,bottomBar负责底部导航。每个Builder都有明确的职责,通过@Builder装饰器实现了UI的模块化复用。
渲染层是界面的最终呈现——Stack根容器管理四个层级(背景、特效、内容、弹框),每个层级按照Z轴顺序堆叠。最底层是静态背景,上面是装饰性特效,再上面是核心内容,最顶层是模态弹框。这种分层结构确保了视觉和交互的有序性。
八、数据模型对比表
| 模型类 | 装饰器 | 字段数 | 核心语义 | 主要应用场景 | 颜色映射函数 |
|---|---|---|---|---|---|
| PostItem | @Observed | 6 | 社区内容 | 鸽友圈动态、首页动态流 | 无 |
| PigeonItem | @Observed | 6 | 商品实体 | 赛鸽橱窗、人气排行、参赛列表 | pigeonTagColor、levelColor |
| RaceItem | @Observed | 5 | 订单服务 | 我的鸽舍、训放管理、进度追踪 | statusColor、statusProgress |
| GearItem | @Observed | 4 | 物资商品 | 器材库、器材速购、加购操作 | 无 |
| WeekChartItem | interface | 2 | 统计数据 | 周训放柱状图 | pigeonColor、barH |
| RaceChartItem | interface | 3 | 统计数据 | 赛程构成条形图 | 自带color字段 |
| CourseItem | interface | 4 | 学习内容 | 养训课堂课程列表 | 自带color字段 |
| LoftTypeItem | interface | 4 | 功能入口 | 首页玩法四格 | 无 |
| NavItem | interface | 2 | 导航配置 | 底部主导航栏 | 无 |
从上表可以看出,鸽语应用的数据模型呈现出清晰的层次结构:
**@Observed类(4个)**都是核心业务实体,需要支持动态增删改和细粒度的属性变化追踪。其中PigeonItem和RaceItem有对应的颜色映射函数,因为它们在界面中需要通过颜色来区分状态或属性。PostItem和GearItem的展示形式相对统一,不需要特殊的颜色映射。
**普通interface(5个)**包括统计图表数据(WeekChartItem、RaceChartItem)、课程内容(CourseItem)、功能入口(LoftTypeItem)和导航配置(NavItem)。这些数据模型要么是静态的统计数据,要么是配置信息,不需要支持动态修改,因此使用普通interface即可满足需求。
从字段数量来看,PigeonItem和PostItem字段最多(6个),信息最丰富;GearItem和NavItem字段最少(2-4个),结构最简单。字段数量与业务实体的复杂度正相关——赛鸽和动态是应用的核心内容,需要承载的信息也最多。
颜色映射是鸽语应用数据模型的一个显著特色。通过pigeonTagColor、levelColor、statusColor等函数,将业务属性(羽色、等级、状态)映射为视觉属性(颜色),实现了数据的可视化编码。这种设计不仅增强了界面的视觉丰富度,也提升了信息传达的效率。
九、总结与技术价值反思
9.1 声明式UI开发范式的价值体现
鸽语·信鸽竞翔与赛鸽俱乐部应用的整个开发过程,充分体现了声明式UI开发范式的核心价值。在传统的命令式开发中,要实现这样一个包含4个主页面、7个子页面、3种弹框、动态特效的复杂应用,开发者需要手动管理大量的视图引用和状态同步逻辑——点击导航栏要手动切换页面内容、更新选中态样式;发布动态要手动向列表插入新元素、更新计数;特效动画要手动操作DOM的style属性……这些繁琐的命令式操作不仅代码量大,而且极易出错。
而在ArkUI声明式框架下,一切都变得简洁而优雅。UI是状态的函数——给定mainTab=0、subTab=0、addOpen=false、tick=42……就有唯一确定的界面。开发者只需要描述"在什么状态下UI应该是什么样子",框架会自动处理状态变化到UI更新的映射。这种思维方式的转变,将开发者从繁琐的视图操作中解放出来,得以将更多精力投入到业务逻辑和用户体验的优化上。
@Builder装饰器的引入进一步强化了声明式开发的优势。通过将UI片段封装为独立的构建函数,代码结构变得更加清晰模块化——header是头部、subNav是子导航、pageFeatured是精选页、pagePigeons是赛鸽页……每个Builder函数都有明确的职责边界,代码可读性和可维护性大幅提升。而且@Builder函数可以像普通函数一样调用、传参、组合,这种灵活性是传统组件化方案难以比拟的。
9.2 @Observed状态管理的精细之处
@Observed装饰器为应用的数据驱动提供了强大的支撑。在鸽语应用中,PostItem、PigeonItem、RaceItem、GearItem四个核心业务模型都使用了@Observed装饰,这意味着:
第一,属性级别的变化追踪。当用户编辑训放单的状态时,只需要修改raceList中对应项的status属性,框架就会自动检测到这个变化,并更新所有使用了该属性的UI组件(状态标签的颜色、进度条的颜色和宽度等)。不需要替换整个对象,不需要手动调用刷新方法,一切都是自动的。
第二,数组级别的响应式。当用户发布新鸽语时,调用postList.unshift()向数组头部插入新元素,鸽友圈列表会自动更新,显示新增的动态。当用户删除训放单时,调用raceList.splice()移除元素,列表也会自动同步。数组的增删操作都能被框架准确捕获,这对于列表型应用来说至关重要。
第三,性能优化的空间。@Observed配合ForEach的key参数,可以实现列表项的精准复用——当数据变化时,框架只更新变化的那一项,而不是重新渲染整个列表。这种细粒度的更新机制在大数据量场景下的性能优势尤为明显。
9.3 Stack/Column/Row布局系统的设计哲学
鸽语应用的界面完全由Stack、Column、Row三种布局容器构建而成,这三种布局看似简单,却蕴含着深刻的设计哲学。
Stack的堆叠布局完美契合了现代UI的层级化特征——背景、内容、弹框、特效,每个元素都有自己的Z轴位置,互不干扰又协同工作。Stack的alignContent属性可以控制子元素的对齐方式,配合position或translate属性,可以实现任意位置的精确定位。在鸽语应用中,Stack不仅用于根容器的层级管理,还广泛应用于进度条(底层背景条+上层进度条)、角标(图标+角标文字)、弹框(遮罩+内容)等场景,是实现叠加效果的利器。
Column的纵向布局是页面内容的主要组织方式。在移动设备上,纵向滚动是最自然的浏览方式,Column天然适配了这种交互模式。配合Scroll组件,Column可以承载无限多的内容,用户通过上下滑动浏览全部信息。Column的space属性可以统一设置子元素之间的间距,避免了逐个设置margin的繁琐。
Row的横向布局是水平排列内容的首选。无论是导航栏、卡片头部,还是双列网格、按钮组,Row都能胜任。Row配合layoutWeight属性,可以实现灵活的按比例分配——等分布局、两列布局、自适应布局,都可以通过简单的weight设置实现。
三种布局的嵌套组合,可以构建出任意复杂的界面结构。这种"少量原语+组合"的设计思想,类似于前端领域的Flexbox和Grid,但在API设计上更加简洁直观。对于开发者来说,只需掌握三个核心布局容器,就能应对绝大多数布局需求,学习成本极低。
9.4 灰蓝鸽红色彩系统的产品语义
鸽语应用的灰蓝+鸽红色彩系统,不仅仅是一套视觉规范,更是产品语义的视觉化表达。
灰蓝色#5C6B7A作为主色调,传递出专业、沉稳、可信赖的品牌调性。赛鸽运动是一项需要专业知识和长期积累的爱好,灰蓝色的冷静气质与这种专业性高度契合。同时,灰蓝色也让人联想到天空,而天空正是赛鸽的舞台,这种色彩与意象的关联,使得品牌识别更加深入人心。
鸽红色#A63D40作为强调色,注入了热情与活力。赛鸽竞翔本质上是一项竞技运动,需要激情和斗志。鸽红色的存在平衡了灰蓝的冷调,为界面增添了温度和动感。在关键操作按钮(报名、发布)和重要状态标签(在翔、精英)上使用鸽红色,能够有效引导用户注意力,提升操作转化率。
wing羽色#8494A5是整个色彩系统中最具巧思的设计。它介于主色和主色浅色之间,不是核心功能色,而是一个"主题专属色"。在气流光环特效、训放卡片按钮、特殊标签等位置使用wing色,既保持了整体色调的统一性,又增添了赛鸽主题的专属韵味。用户可能不会刻意注意到这个颜色,但它却在潜意识层面强化了"赛鸽"这个产品主题。
9.5 特效层设计的技术智慧
鸽子剪影浮动+气流光环扩散的特效层,是鸽语应用在视觉体验上的一大亮点。而支撑这个特效的技术方案,同样值得深入思考。
首先是特效层的定位。特效层位于背景层和内容层之间,作为装饰性元素存在,不会干扰主要内容的展示。这种"装饰不抢戏"的设计原则,确保了特效的加分属性而不是减分属性。
其次是穿透点击的实现。通过hitTestBehavior(HitTestMode.None)设置,特效层完全不拦截点击事件,用户可以"透过"特效层正常操作下方的按钮和列表。这个技术细节至关重要——如果没有穿透设置,特效元素覆盖的区域将无法点击,严重影响用户体验,特效反而成了负担。
再次是性能考量。特效使用setInterval驱动,90ms的间隔约合11fps,帧率不高但足够流畅。特效元素只有8个(4只鸽子+4个光环),且都是简单的Text和Column组件,渲染开销极低。透明度都控制在0.5以下,不会对内容的可读性产生明显影响。这些细节共同确保了特效的低开销和高性能。
最后是主题契合度。鸽子剪影和气流光环的设计与赛鸽主题高度契合——鸽子是应用的核心意象,气流光环则暗示了飞行速度和空气动力学。特效不是为了炫技而存在,而是为了强化产品主题、营造氛围体验。这种"功能服务于主题"的设计理念,是特效设计的高阶境界。
9.6 双层导航架构的信息组织智慧
底部4主tab + 首页7子tab的双层导航架构,是鸽语应用信息组织的核心骨架。这种架构设计有着深刻的产品逻辑。
底部4主tab构成了应用的一级导航,划分了四大功能板块:首页(信息聚合)、鸽舍(核心功能)、课堂(知识学习)、我的(个人中心)。这是一个经典的移动应用导航模式,用户学习成本低,操作效率高。
首页7子tab构成了二级导航,将首页细分为精选、赛鸽橱窗、我的鸽舍、竞翔赛事、养训课堂、鸽友圈、器材库7个内容板块。这种"首页内聚全功能"的设计,使得用户在首页即可通过横向切换访问应用的全部主要内容,减少了页面跳转的操作成本。对于内容型应用来说,首页的信息密度和可达性直接影响用户的使用深度。
结构顶条式的子导航设计也颇有讲究。顶部指示条+胶囊背景+多维度选中态的组合,既提供了清晰的位置反馈,又保持了界面的精致感。横向滚动的设计使得7个tab能够在有限的屏幕宽度内容纳,用户可以左右滑动查看全部选项。
双层导航架构本质上是一种信息分层策略——一级导航建立全局框架,二级导航提供细分入口。用户既可以通过底部tab在大板块间切换,也可以在首页内通过子tab快速浏览不同内容。这种分层设计平衡了功能丰富度和操作便捷性,是复杂应用信息架构的经典范式。
9.7 弹框交互模式的设计考量
新增(发鸽语)、编辑(编辑训放单)、删除(取消训放/退课)三类弹框,覆盖了应用中最核心的数据操作场景。这三类弹框的设计体现了对用户操作心理的深刻理解。
新增弹框是正向操作——用户发布内容、表达观点,情绪是积极的。因此"发布"按钮使用主色调(灰蓝),传递出安全、可靠的感觉。弹框中包含标题和内容两个输入字段,满足基本的发布需求。
编辑弹框是修改操作——用户调整已有数据,情绪是中性偏谨慎的。"保存"按钮同样使用主色调,三个输入字段(名称、状态、备注)覆盖了训放单的主要可编辑属性。
删除弹框是负向操作——用户取消订单、退出课程,情绪是犹豫和不安的。因此"确认"按钮使用危险色(红棕),既警示用户这是一个不可逆的破坏性操作,又通过色彩强化了"谨慎行事"的心理暗示。删除弹框的内容最少(只有提示文字),但决策权重最大,这符合"破坏比建设容易,但影响更大"的操作心理学原则。
三个弹框共享同一个遮罩层和相似的布局结构(标题+内容+双按钮),保持了交互的一致性。用户在使用过一个弹框后,就能快速理解其他弹框的操作方式,降低了学习成本。同时,弹框的触发方式和位置也保持统一——都是从对应操作按钮触发,都是居中显示,这种一致性进一步增强了产品的专业感。
9.8 纯函数工具库的工程价值
鸽语应用中的12个纯函数(barH、pigeonColor、doveX、doveY、doveA、ringA、ringX、ringY、ringR、statusColor、statusProgress、pigeonTagColor、levelColor)构成了一个小型工具库,这些纯函数虽然代码量不大,但工程价值不容忽视。
纯函数的最大优势是可预测性——给定相同的输入,永远返回相同的输出,不依赖外部状态,也不产生副作用。这种特性使得纯函数非常容易测试和调试,也便于在不同组件间复用。在鸽语应用中,pigeonTagColor函数既在赛鸽橱窗页面使用,也在竞翔赛事页面的排行榜中使用,一处定义,多处复用。
颜色映射函数的集中管理,也使得整个应用的色彩逻辑更加清晰。所有的"业务属性→颜色"的转换规则都集中在函数库中,而不是散落在各个组件内部。当需要调整颜色映射规则时,只需要修改对应的函数即可,无需在整个代码库中搜索替换。这种集中式管理的模式,是大型项目维护代码质量的重要手段。
特效函数组(doveX/Y/A、ringR/A/X/Y)的设计也很有启发性。通过将动画逻辑封装为纯函数,特效层的Builder代码变得非常简洁——只需要在组件属性中调用函数即可。动画参数(速度、范围、起始偏移等)都在函数内部定义,修改动画效果时调整函数参数即可,不需要改动组件结构。
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 场景:赛鸽市集 / 训放竞翔管理 / 秋赛报名 / 鸽友社区
// 底部4主tab(首页/鸽舍/课堂/我的)+ 首页7内容tab(结构顶条式)
// 特效:鸽子剪影浮动 + 气流光环扩散(特效层位于底层,不遮挡点击)
// 弹框:新增(发鸽语) / 编辑(编辑训放单) / 删除(取消训放·退课)
interface ColorPalette {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
cardBg: string;
textPrimary: string;
textSecondary: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
white: string;
wing: string;
}
const COLORS: ColorPalette = {
primary: '#5C6B7A',
primaryLight: '#DCE1E7',
primaryDark: '#3B4754',
accent: '#A63D40',
accentLight: '#F0DCDA',
bg: '#F1F3F5',
cardBg: '#FFFFFF',
textPrimary: '#2B333D',
textSecondary: '#5F6C79',
textHint: '#A6B0BC',
border: '#E2E7EC',
success: '#7BA884',
warning: '#D9A05B',
danger: '#C76B5A',
white: '#FFFFFF',
wing: '#8494A5'
};
// ============ 鸽友动态数据模型 ============
@Observed
export class PostItem {
id: number = 0
nick: string = ''
avatar: string = ''
text: string = ''
time: string = ''
likes: number = 0
constructor(id: number, nick: string, avatar: string, text: string, time: string, likes: number) {
this.id = id; this.nick = nick; this.avatar = avatar; this.text = text
this.time = time; this.likes = likes
}
}
// ============ 赛鸽数据模型 ============
@Observed
export class PigeonItem {
id: number = 0
name: string = ''
craft: string = ''
price: string = ''
level: string = ''
hot: number = 0
constructor(id: number, name: string, craft: string, price: string, level: string, hot: number) {
this.id = id; this.name = name; this.craft = craft; this.price = price
this.level = level; this.hot = hot
}
}
// ============ 训放订单数据模型 ============
@Observed
export class RaceItem {
id: number = 0
name: string = ''
item: string = ''
status: string = ''
progress: number = 0
constructor(id: number, name: string, item: string, status: string, progress: number) {
this.id = id; this.name = name; this.item = item; this.status = status
this.progress = progress
}
}
// ============ 器材数据模型 ============
@Observed
export class GearItem {
id: number = 0
name: string = ''
desc: string = ''
icon: string = ''
constructor(id: number, name: string, desc: string, icon: string) {
this.id = id; this.name = name; this.desc = desc; this.icon = icon
}
}
// ============ 图表与课程接口 ============
interface WeekChartItem {
label: string;
value: number;
}
interface RaceChartItem {
label: string;
value: number;
color: string;
}
interface CourseItem {
title: string;
week: string;
progress: number;
color: string;
}
interface LoftTypeItem {
name: string;
desc: string;
icon: string;
hot: boolean;
}
// ============ 顶层静态数据 ============
const POST_LIST: PostItem[] = [
new PostItem(1, '雨点号主人', '🕊️', '230 公里当天归巢,进棚第一件事抢食。', '6分钟前', 129),
new PostItem(2, '灰壳血统党', '🪶', '这路灰壳鸽子阴天也定向稳,没白养。', '20分钟前', 107),
new PostItem(3, '秋赛冲奖人', '🏆', '集鸽前换了三天清淡料,状态肉眼可见。', '37分钟前', 98),
new PostItem(4, '鸽语主理人', '🏠', '周六鸽舍开放日,欢迎来看新配对。', '1小时前', 90),
new PostItem(5, '新手鸽友', '📖', '第一次上手放飞 30 公里,全程盯着雷达。', '2小时前', 82),
new PostItem(6, '公棚参赛户', '🎟️', '交了 3 羽公棚鸽,静候热身赛。', '3小时前', 73),
new PostItem(7, '脚环收藏控', '🔖', '十年老足环整理成册,编号都还能查。', '5小时前', 65),
new PostItem(8, '夜放观察者', '🌙', '清晨五点放飞的一批,归巢率出奇高。', '昨天', 59)
];
const PIGEON_LIST: PigeonItem[] = [
new PigeonItem(1, '灰壳 · 24-088821', '詹森直孙', '¥6800/羽', '精英', 96),
new PigeonItem(2, '雨点 · 24-066052', '胡本血脉', '¥4500/羽', '精英', 92),
new PigeonItem(3, '绛鸽 · 24-112347', '红绛老血统', '¥2800/羽', '主力', 88),
new PigeonItem(4, '石板灰 · 24-090813', '幕利门系', '¥3800/羽', '主力', 84),
new PigeonItem(5, '麒麟花 · 24-133905', '花鸽观赏', '¥1200/羽', '潜力', 78),
new PigeonItem(6, '白条 · 23-201764', '老国血', '¥860/羽', '潜力', 72),
new PigeonItem(7, '深雨点 · 23-188420', '500 公里冠军直子', '¥9800/羽', '冠军血', 69),
new PigeonItem(8, '幼鸽 · 25-010865', '春季作育', '¥600/羽', '幼鸽', 61)
];
const RACE_LIST: RaceItem[] = [
new RaceItem(1, '雨点号 · 230km 训放', '西北线 · 顶风', '已归巢', 100),
new RaceItem(2, '灰壳二号 · 320km', '北线 · 侧风', '在翔', 74),
new RaceItem(3, '秋赛第一关 · 500km', '竞翔 · 1320 羽', '集鸽中', 52),
new RaceItem(4, '幼鸽路训 · 80km', '分组 · 12 羽', '验鸽中', 38),
new RaceItem(5, '公棚热身赛 · 200km', '交鸽 3 羽', '待放飞', 26),
new RaceItem(6, '绛鸽 · 160km 复放', '东南线', '在翔', 18),
new RaceItem(7, '特比环资格赛', '环号登记', '集鸽中', 12),
new RaceItem(8, '年度总决赛 · 550km', '报名审核', '待放飞', 6)
];
const GEAR_LIST: GearItem[] = [
new GearItem(1, '电子足环', '扫码验鸽', '🔖'),
new GearItem(2, '赛鸽GPS 追踪器', '实时定位', '📡'),
new GearItem(3, '清除饲料 · 清淡', '集鸽前投喂', '🌾'),
new GearItem(4, '营养土 · 矿物质', '日常保健', '🪨'),
new GearItem(5, '栖架 · 木质', '鸽舍改造', '🪵'),
new GearItem(6, '巢盆 · 草编', '繁殖季备用', '🪺'),
new GearItem(7, '鸽舍消毒剂', '每周喷洒', '🧴'),
new GearItem(8, '训放笼 · 便携', '20 羽装', '🎒')
];
const WEEK_CHART: WeekChartItem[] = [
{ label: '周一', value: 3 },
{ label: '周二', value: 5 },
{ label: '周三', value: 4 },
{ label: '周四', value: 6 },
{ label: '周五', value: 8 },
{ label: '周六', value: 13 },
{ label: '周日', value: 10 }
];
const RACE_CHART: RaceChartItem[] = [
{ label: '短程 100-300', value: 34, color: '#5C6B7A' },
{ label: '中程 300-500', value: 28, color: '#A63D40' },
{ label: '长程 500-700', value: 18, color: '#7BA884' },
{ label: '超远程 1000+', value: 12, color: '#D9A05B' },
{ label: '公棚赛', value: 8, color: '#C76B5A' }
];
const TYPE_LIST: LoftTypeItem[] = [
{ name: '赛鸽市集', desc: '名家血统 · 检疫直发', icon: '🕊️', hot: true },
{ name: '训放托管', desc: '分级路训 · 数据留档', icon: '📍', hot: true },
{ name: '秋赛报名', desc: '特比环 · 公棚', icon: '🏆', hot: false },
{ name: '鸽舍诊断', desc: '通风 · 密度 · 保健', icon: '🏠', hot: false }
];
const COURSE_LIST: CourseItem[] = [
{ title: '信鸽品种与羽色识别', week: '第 1 周', progress: 100, color: '#5C6B7A' },
{ title: '幼鸽作育与开家', week: '第 2 周', progress: 86, color: '#A63D40' },
{ title: '路训节奏与体能调控', week: '第 3 周', progress: 54, color: '#7BA884' },
{ title: '竞翔定向原理', week: '第 4 周', progress: 29, color: '#D9A05B' },
{ title: '鸽舍通风与疾病预防', week: '第 5 周', progress: 5, color: '#C76B5A' }
];
// ============ 导航配置 ============
interface NavItem {
icon: string;
label: string;
}
const NAV_LIST: NavItem[] = [
{ icon: '🕊️', label: '首页' },
{ icon: '🏠', label: '鸽舍' },
{ icon: '📚', label: '课堂' },
{ icon: '👤', label: '我的' }
];
const SUB_NAV_LIST: string[] = ['精选', '赛鸽橱窗', '我的鸽舍', '竞翔赛事', '养训课堂', '鸽友圈', '器材库'];
// ============ 顶层纯函数 ============
function barH(v: number, max: number): number {
return Math.round(118 * v / max);
}
function pigeonColor(v: number): string {
if (v > 9) {
return '#5C6B7A';
} else if (v > 6) {
return '#A63D40';
}
return '#D9A05B';
}
function doveX(tick: number, i: number): number {
return 40 + ((tick * 6 + i * 211) % 600);
}
function doveY(tick: number, i: number): number {
return 70 + i * 170 + ((tick + i) % 5) * 6;
}
function doveA(tick: number, i: number): number {
return 0.25 + ((tick + i) % 4) * 0.08;
}
function ringA(tick: number, i: number): number {
return 0.04 + ((tick + i) % 5) * 0.03;
}
function ringX(tick: number, i: number): number {
return 70 + ((tick * 4 + i * 197) % 530);
}
function ringY(tick: number, i: number): number {
return 90 + ((tick * 5 + i * 181) % 560);
}
function ringR(tick: number, i: number): number {
return 16 + ((tick + i) % 5) * 9;
}
function statusColor(s: string): string {
if (s === '已归巢') {
return '#7BA884';
} else if (s === '在翔') {
return '#A63D40';
} else if (s === '集鸽中') {
return '#5C6B7A';
} else if (s === '验鸽中') {
return '#D9A05B';
}
return '#A6B0BC';
}
function statusProgress(s: string): number {
if (s === '已归巢') {
return 100;
} else if (s === '在翔') {
return 72;
} else if (s === '集鸽中') {
return 50;
} else if (s === '验鸽中') {
return 32;
}
return 10;
}
function pigeonTagColor(g: string): string {
if (g.indexOf('雨点') >= 0) {
return '#5C6B7A';
} else if (g.indexOf('绛') >= 0 || g.indexOf('麒麟花') >= 0) {
return '#A63D40';
} else if (g.indexOf('灰') >= 0 || g.indexOf('石板') >= 0) {
return '#7BA884';
}
return '#D9A05B';
}
function levelColor(s: string): string {
if (s === '幼鸽') {
return '#7BA884';
} else if (s === '潜力') {
return '#D9A05B';
} else if (s === '主力') {
return '#5C6B7A';
} else if (s === '精英') {
return '#A63D40';
}
return '#C76B5A';
}
@Entry
@Component
struct PagePigeonRacing {
@State mainTab: number = 0
@State subTab: number = 0
@State tick: number = 0
@State glow: number = 0
@State addOpen: boolean = false
@State editOpen: boolean = false
@State delOpen: boolean = false
@State editIdx: number = 0
@State delTarget: string = 'race'
@State editName: string = ''
@State editStyle: string = ''
@State editNote: string = ''
@State addTitle: string = ''
@State addContent: string = ''
@State postList: PostItem[] = POST_LIST
@State raceList: RaceItem[] = RACE_LIST
@State fxTimer: number = -1
aboutToAppear(): void {
this.fxTimer = setInterval(() => {
this.tick = this.tick + 1;
this.glow = (this.glow + 1) % 2;
}, 90);
}
aboutToDisappear(): void {
if (this.fxTimer > 0) {
clearInterval(this.fxTimer);
this.fxTimer = -1;
}
}
openAdd(): void {
this.addTitle = '';
this.addContent = '';
this.addOpen = true;
}
openEdit(index: number): void {
if (index >= 0 && index < this.raceList.length) {
this.editIdx = index;
this.editName = this.raceList[index].name;
this.editStyle = this.raceList[index].status;
this.editNote = this.raceList[index].item;
}
this.editOpen = true;
}
openDelA(): void {
this.delTarget = 'race';
this.delOpen = true;
}
openDelB(): void {
this.delTarget = 'course';
this.delOpen = true;
}
doAdd(): void {
if (this.addTitle.length > 0) {
this.postList.unshift(new PostItem(999, '我的鸽语', '🕊️', this.addTitle, '刚刚', 0));
}
this.addOpen = false;
}
doEdit(): void {
if (this.editIdx >= 0 && this.editIdx < this.raceList.length) {
this.raceList.splice(this.editIdx, 1, new RaceItem(this.raceList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
}
this.editOpen = false;
}
doDel(): void {
if (this.delTarget === 'race' && this.raceList.length > 0) {
this.raceList.splice(0, 1);
} else if (this.delTarget === 'course' && this.postList.length > 0) {
this.postList.splice(0, 1);
}
this.delOpen = false;
}
@Builder
fxLayer() {
Stack({ alignContent: Alignment.TopStart }) {
ForEach([0, 1, 2, 3], (i: number) => {
Text('🕊️')
.fontSize(16)
.opacity(doveA(this.tick, i))
.rotate({ angle: -8 })
.translate({ x: doveX(this.tick, i), y: doveY(this.tick, i) })
}, (i: number) => i.toString())
ForEach([0, 1, 2, 3], (i: number) => {
Column()
.width(ringR(this.tick, i) * 2)
.height(ringR(this.tick, i) * 2)
.borderRadius(ringR(this.tick, i))
.backgroundColor(COLORS.wing)
.opacity(ringA(this.tick, i))
.translate({ x: ringX(this.tick, i), y: ringY(this.tick, i) })
}, (i: number) => i.toString())
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
}
@Builder
header() {
Column({ space: 10 }) {
// 搜索条 + 购物车
Row({ space: 10 }) {
Text('🕊️')
.fontSize(24)
Row({ space: 6 }) {
Text('🔍')
.fontSize(12)
Text('搜一搜「詹森」「特比环」')
.fontSize(11)
.fontColor(COLORS.textHint)
.layoutWeight(1)
}
.layoutWeight(1)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(COLORS.cardBg)
.borderRadius(18)
Stack() {
Text('🧺')
.fontSize(20)
Text('1')
.fontSize(8)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
.backgroundColor(COLORS.danger)
.borderRadius(8)
.translate({ x: 10, y: -10 })
}
.width(30)
.height(30)
}
.width('100%')
// 训放进度卡
Row({ space: 12 }) {
Column({ space: 4 }) {
Text('雨点号 · 230km 在翔')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
Text('预计 14:20 归巢 · 分速 1280 米/分')
.fontSize(10)
.fontColor('#E5E9EE')
Row({ space: 6 }) {
Text('顶风逆飞')
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primaryDark)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.wing)
.borderRadius(6)
Text('同期在翔 4 羽')
.fontSize(9)
.fontColor('#E5E9EE')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('查看雷达')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primaryDark)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(COLORS.wing)
.borderRadius(14)
.onClick(() => {
this.openEdit(1);
})
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.primary)
.borderRadius(16)
// 秋赛报名横幅
Row({ space: 10 }) {
Text('🏆')
.fontSize(22)
Column({ space: 2 }) {
Text('秋赛特比环报名 · 剩 2 天')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('500km 争霸赛 · 冠军奖金 ¥20000')
.fontSize(10)
.fontColor(COLORS.textSecondary)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('报名')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.backgroundColor(COLORS.accent)
.borderRadius(14)
.onClick(() => {
this.openAdd();
})
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
// 数据四格
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('在翔羽数')
.fontSize(9)
.fontColor(COLORS.textHint)
Text('4 羽')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('累计训放')
.fontSize(9)
.fontColor(COLORS.textHint)
Text('186 次')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('最佳分速')
.fontSize(9)
.fontColor(COLORS.textHint)
Text('1420 米')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.success)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('鸽币')
.fontSize(9)
.fontColor(COLORS.textHint)
Text('¥120')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.warning)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
}
.width('100%')
.padding({ top: 10, bottom: 4 })
}
@Builder
subNav() {
Scroll() {
Row({ space: 6 }) {
ForEach(SUB_NAV_LIST, (item: string, idx: number) => {
Column({ space: 5 }) {
Row()
.width('100%')
.height(3)
.borderRadius(2)
.backgroundColor(this.subTab === idx ? COLORS.accent : COLORS.border)
Text(item)
.fontSize(this.subTab === idx ? 13 : 11)
.fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.subTab === idx ? COLORS.primaryDark : COLORS.textSecondary)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(this.subTab === idx ? COLORS.primaryLight : COLORS.bg)
.borderRadius(8)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.subTab = idx;
})
}, (item: string) => item)
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.padding({ top: 6, bottom: 10 })
}
@Builder
pageFeatured() {
Column({ space: 12 }) {
// 玩法四格
Column() {
Text('在鸽语能玩什么')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
.margin({ bottom: 10 })
Row({ space: 8 }) {
ForEach(TYPE_LIST, (it: LoftTypeItem) => {
Column({ space: 4 }) {
Text(it.icon)
.fontSize(24)
Text(it.name)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(it.desc)
.fontSize(9)
.fontColor(COLORS.textHint)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(it.hot ? '热门' : ' ')
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor(it.hot ? COLORS.danger : COLORS.border)
}
.layoutWeight(1)
.padding({ top: 10, bottom: 8, left: 4, right: 4 })
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
}, (it: LoftTypeItem) => it.name)
}
.width('100%')
}
.width('100%')
// 周训放柱状图
Column() {
Row() {
Text('本周训放放飞次数')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text('单位 次')
.fontSize(10)
.fontColor(COLORS.textHint)
}
.width('100%')
Row({ space: 10 }) {
ForEach(WEEK_CHART, (it: WeekChartItem) => {
Column({ space: 4 }) {
Column()
.width(20)
.height(barH(it.value, 13))
.borderRadius(5)
.backgroundColor(pigeonColor(it.value))
Text(`${it.value}`)
.fontSize(9)
.fontColor(COLORS.textSecondary)
Text(it.label)
.fontSize(9)
.fontColor(COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (it: WeekChartItem) => it.label + it.value.toString())
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
.height(140)
.margin({ top: 12 })
Row({ space: 6 }) {
Text('周末训放占比')
.fontSize(11)
.fontColor(COLORS.textSecondary)
Text('53%')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
// 赛程构成
Column() {
Text('本季参赛赛程构成')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
ForEach(RACE_CHART, (it: RaceChartItem) => {
Row({ space: 8 }) {
Text(it.label)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.width(96)
Stack({ alignContent: Alignment.Start }) {
Row()
.width('100%')
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.border)
Row()
.width(`${it.value}%`)
.height(8)
.borderRadius(4)
.backgroundColor(it.color)
}
.layoutWeight(1)
Text(`${it.value}%`)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width(44)
.textAlign(TextAlign.End)
}
.width('100%')
.margin({ top: 8 })
}, (it: RaceChartItem) => it.label)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
// 人气赛鸽双列
Column() {
Row() {
Text('人气赛鸽橱窗')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text('查看全部')
.fontSize(11)
.fontColor(COLORS.primary)
}
.width('100%')
.margin({ bottom: 10 })
ForEach(PIGEON_LIST, (it: PigeonItem, idx: number) => {
if (idx % 2 === 0) {
Row({ space: 10 }) {
if (idx < PIGEON_LIST.length) {
Column({ space: 6 }) {
Row() {
Text('🕊️')
.fontSize(22)
.layoutWeight(1)
Text(PIGEON_LIST[idx].level)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(levelColor(PIGEON_LIST[idx].level))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.accentLight)
.borderRadius(6)
}
.width('100%')
Text(PIGEON_LIST[idx].name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(`${PIGEON_LIST[idx].price} · 关注 ${PIGEON_LIST[idx].hot}`)
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}
if (idx + 1 < PIGEON_LIST.length) {
Column({ space: 6 }) {
Row() {
Text('🪶')
.fontSize(22)
.layoutWeight(1)
Text(PIGEON_LIST[idx + 1].level)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(levelColor(PIGEON_LIST[idx + 1].level))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.accentLight)
.borderRadius(6)
}
.width('100%')
Text(PIGEON_LIST[idx + 1].name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(`${PIGEON_LIST[idx + 1].price} · 关注 ${PIGEON_LIST[idx + 1].hot}`)
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}
}
.width('100%')
}
}, (it: PigeonItem) => it.id.toString())
}
.width('100%')
// 鸽友动态
Column({ space: 10 }) {
Row() {
Text('鸽友动态')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text('发鸽语')
.fontSize(12)
.fontColor(COLORS.primary)
.onClick(() => {
this.openAdd();
})
}
.width('100%')
ForEach(this.postList.slice(0, 3), (it: PostItem) => {
Row({ space: 10 }) {
Text(it.avatar)
.fontSize(22)
Column({ space: 3 }) {
Row() {
Text(it.nick)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(it.time)
.fontSize(10)
.fontColor(COLORS.textHint)
}
.width('100%')
Text(it.text)
.fontSize(12)
.fontColor(COLORS.textSecondary)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`🕊️ ${it.likes}`)
.fontSize(11)
.fontColor(COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}, (it: PostItem) => it.id.toString())
}
.width('100%')
}
.width('100%')
}
@Builder
pagePigeons() {
Column({ space: 10 }) {
Row() {
Text('赛鸽橱窗')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text('挂售我的赛鸽')
.fontSize(12)
.fontColor(COLORS.primary)
.onClick(() => {
this.openAdd();
})
}
.width('100%')
ForEach(PIGEON_LIST, (it: PigeonItem) => {
Column({ space: 6 }) {
Row() {
Column()
.width(4)
.height(38)
.borderRadius(2)
.backgroundColor(pigeonTagColor(it.name))
Column({ space: 4 }) {
Row() {
Text(it.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(it.level)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(levelColor(it.level))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.bg)
.borderRadius(6)
}
.width('100%')
Row({ space: 8 }) {
Text(`💰 ${it.price}`)
.fontSize(11)
.fontColor(COLORS.textSecondary)
Text(`🧬 ${it.craft}`)
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
Stack({ alignContent: Alignment.Start }) {
Row()
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor(COLORS.border)
Row()
.width(`${it.hot}%`)
.height(6)
.borderRadius(3)
.backgroundColor(pigeonTagColor(it.name))
}
.width('100%')
Row({ space: 10 }) {
Text(`关注度 ${it.hot}`)
.fontSize(10)
.fontColor(COLORS.textHint)
.layoutWeight(1)
Text('加入购物车')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor(COLORS.primary)
.borderRadius(8)
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}, (it: PigeonItem) => it.id.toString())
}
.width('100%')
}
@Builder
pageLoft() {
Column({ space: 10 }) {
Text('我的鸽舍')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
ForEach(this.raceList, (it: RaceItem, idx: number) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Text('📍')
.fontSize(24)
Column({ space: 4 }) {
Row() {
Text(it.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(it.status)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(statusColor(it.status))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(COLORS.accentLight)
.borderRadius(6)
}
.width('100%')
Text(`🏁 ${it.item}`)
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
Stack({ alignContent: Alignment.Start }) {
Row()
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor(COLORS.border)
Row()
.width(`${statusProgress(it.status)}%`)
.height(6)
.borderRadius(3)
.backgroundColor(statusColor(it.status))
}
.width('100%')
Row() {
Text(`训放进度 ${statusProgress(it.status)}%`)
.fontSize(10)
.fontColor(COLORS.textHint)
.layoutWeight(1)
Text('编辑训放单')
.fontSize(11)
.fontColor(COLORS.primary)
.onClick(() => {
this.openEdit(idx);
})
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}, (it: RaceItem) => it.id.toString())
}
.width('100%')
}
@Builder
pageRaces() {
Column({ space: 10 }) {
Text('竞翔赛事')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('集鸽截止')
.fontSize(10)
.fontColor(COLORS.textHint)
Text('周六 18:00')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('参赛羽数')
.fontSize(10)
.fontColor(COLORS.textHint)
Text('1320 羽')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.accent)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('赛线')
.fontSize(10)
.fontColor(COLORS.textHint)
Text('西北 500km')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.warning)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('天气')
.fontSize(10)
.fontColor(COLORS.textHint)
Text('多云转晴')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.success)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
ForEach(PIGEON_LIST.slice(0, 5), (it: PigeonItem, idx: number) => {
Row({ space: 10 }) {
Text(`${idx + 1}`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? COLORS.accent : COLORS.textHint)
.width(20)
Column({ space: 3 }) {
Text(it.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(`${it.craft} · ${it.price}`)
.fontSize(10)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(`关注 ${it.hot}`)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(pigeonTagColor(it.name))
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
}, (it: PigeonItem) => it.id.toString())
Text('器材速购')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
.margin({ top: 4 })
ForEach(GEAR_LIST, (it: GearItem, idx: number) => {
if (idx % 2 === 0) {
Row({ space: 10 }) {
if (idx < GEAR_LIST.length) {
Column({ space: 6 }) {
Text(GEAR_LIST[idx].icon)
.fontSize(26)
Text(GEAR_LIST[idx].name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(GEAR_LIST[idx].desc)
.fontSize(11)
.fontColor(COLORS.textSecondary)
Text('加购')
.fontSize(11)
.fontColor(COLORS.primary)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}
if (idx + 1 < GEAR_LIST.length) {
Column({ space: 6 }) {
Text(GEAR_LIST[idx + 1].icon)
.fontSize(26)
Text(GEAR_LIST[idx + 1].name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(GEAR_LIST[idx + 1].desc)
.fontSize(11)
.fontColor(COLORS.textSecondary)
Text('加购')
.fontSize(11)
.fontColor(COLORS.primary)
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}
}
.width('100%')
}
}, (it: GearItem) => it.id.toString())
}
.width('100%')
}
@Builder
pageCourse() {
Column({ space: 10 }) {
Text('养训课堂')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
ForEach(COURSE_LIST, (it: CourseItem) => {
Column({ space: 8 }) {
Row() {
Text(it.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(it.week)
.fontSize(11)
.fontColor(COLORS.textHint)
}
.width('100%')
Stack({ alignContent: Alignment.Start }) {
Row()
.width('100%')
.height(8)
.borderRadius(4)
.backgroundColor(COLORS.border)
Row()
.width(`${it.progress}%`)
.height(8)
.borderRadius(4)
.backgroundColor(it.color)
}
.width('100%')
Row() {
Text('已学')
.fontSize(11)
.fontColor(COLORS.textSecondary)
.layoutWeight(1)
Text(`${it.progress}%`)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.primary)
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}, (it: CourseItem) => it.title)
Button()
.width('100%')
.height(42)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.opacity(0.9)
.onClick(() => {
this.openDelB();
})
Text('退课')
.fontSize(14)
.fontColor(COLORS.white)
.margin({ left: -28, top: -32 })
}
.width('100%')
}
@Builder
pageCircle() {
Column({ space: 10 }) {
Row() {
Text('鸽友圈')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text('发鸽语')
.fontSize(12)
.fontColor(COLORS.primary)
.onClick(() => {
this.openAdd();
})
}
.width('100%')
ForEach(this.postList, (it: PostItem) => {
Row({ space: 10 }) {
Text(it.avatar)
.fontSize(26)
Column({ space: 4 }) {
Row() {
Text(it.nick)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(it.time)
.fontSize(10)
.fontColor(COLORS.textHint)
}
.width('100%')
Text(it.text)
.fontSize(13)
.fontColor(COLORS.textSecondary)
Text(`🕊️ ${it.likes} · 💬 回复`)
.fontSize(11)
.fontColor(COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
}, (it: PostItem) => it.id.toString())
}
.width('100%')
}
@Builder
pageGear() {
Column({ space: 10 }) {
Text('器材库')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.width('100%')
Row({ space: 8 }) {
Column({ space: 4 }) {
Text('🌾')
.fontSize(28)
Text('饲料区')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('清除 · 营养')
.fontSize(10)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('📡')
.fontSize(28)
Text('追踪区')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('足环 · GPS')
.fontSize(10)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('🏠')
.fontSize(28)
Text('鸽舍区')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('栖架 · 消毒')
.fontSize(10)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
ForEach(GEAR_LIST, (it: GearItem) => {
Row({ space: 10 }) {
Text(it.icon)
.fontSize(20)
Column({ space: 3 }) {
Text(it.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(it.desc)
.fontSize(10)
.fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('加购')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.primary)
.borderRadius(8)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS.cardBg)
.borderRadius(10)
}, (it: GearItem) => it.id.toString())
Button()
.width('100%')
.height(42)
.backgroundColor(COLORS.danger)
.borderRadius(10)
.opacity(0.9)
.onClick(() => {
this.openDelA();
})
Text('取消训放')
.fontSize(14)
.fontColor(COLORS.white)
.margin({ left: -56, top: -32 })
}
.width('100%')
}
@Builder
pageMine() {
Column({ space: 12 }) {
Row({ space: 12 }) {
Text('🕊️')
.fontSize(40)
Column({ space: 4 }) {
Text('鸽语竞翔人')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text('金翼会员 · 训放 186 次 · 在养 24 羽 · 打卡 96 天')
.fontSize(11)
.fontColor(COLORS.textSecondary)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
Column({ space: 10 }) {
Row() {
Text('我的训放档案')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(`${this.raceList.length} 单`)
.fontSize(12)
.fontColor(COLORS.primary)
}
.width('100%')
ForEach(this.raceList.slice(0, 3), (it: RaceItem) => {
Row() {
Text(it.name)
.fontSize(13)
.fontColor(COLORS.textSecondary)
.layoutWeight(1)
Text(it.status)
.fontSize(11)
.fontColor(statusColor(it.status))
}
.width('100%')
}, (it: RaceItem) => it.id.toString())
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
Column({ space: 10 }) {
Row() {
Text('我的鸽语')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
.layoutWeight(1)
Text(`${this.postList.length} 条`)
.fontSize(12)
.fontColor(COLORS.primary)
}
.width('100%')
ForEach(this.postList.slice(0, 3), (it: PostItem) => {
Row() {
Text(it.nick)
.fontSize(13)
.fontColor(COLORS.textSecondary)
.layoutWeight(1)
Text(it.time)
.fontSize(11)
.fontColor(COLORS.textHint)
}
.width('100%')
}, (it: PostItem) => it.id.toString())
}
.width('100%')
.padding(14)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
}
.width('100%')
}
@Builder
modalOverlay() {
Stack() {
Column()
.width('100%')
.height('100%')
.backgroundColor('#000000')
.opacity(0.6)
.onClick(() => {
this.addOpen = false;
this.editOpen = false;
this.delOpen = false;
})
}
.width('100%')
.height('100%')
}
@Builder
addModalBody() {
Column({ space: 12 }) {
Text('发布鸽语')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
TextInput({ placeholder: '一句话鸽舍日常…', text: this.addTitle })
.fontSize(13)
.fontColor(COLORS.textPrimary)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.height(40)
.onChange((v: string) => {
this.addTitle = v;
})
TextArea({ placeholder: '补充细节:羽色、赛线、训放心得…', text: this.addContent })
.fontSize(13)
.fontColor(COLORS.textPrimary)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.height(90)
.onChange((v: string) => {
this.addContent = v;
})
Row({ space: 10 }) {
Button()
.layoutWeight(1)
.height(38)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.onClick(() => {
this.addOpen = false;
})
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.margin({ left: -52 })
Button()
.layoutWeight(1)
.height(38)
.backgroundColor(COLORS.primary)
.borderRadius(8)
.onClick(() => {
this.doAdd();
})
Text('发布')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.margin({ left: -44 })
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
}
@Builder
editModalBody() {
Column({ space: 12 }) {
Text('编辑训放单')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
TextInput({ placeholder: '训放名称', text: this.editName })
.fontSize(13)
.fontColor(COLORS.textPrimary)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.height(40)
.onChange((v: string) => {
this.editName = v;
})
TextInput({ placeholder: '状态(如 在翔)', text: this.editStyle })
.fontSize(13)
.fontColor(COLORS.textPrimary)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.height(40)
.onChange((v: string) => {
this.editStyle = v;
})
TextArea({ placeholder: '训放内容与备注说明', text: this.editNote })
.fontSize(13)
.fontColor(COLORS.textPrimary)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.height(70)
.onChange((v: string) => {
this.editNote = v;
})
Row({ space: 10 }) {
Button()
.layoutWeight(1)
.height(38)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.onClick(() => {
this.editOpen = false;
})
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.margin({ left: -52 })
Button()
.layoutWeight(1)
.height(38)
.backgroundColor(COLORS.primary)
.borderRadius(8)
.onClick(() => {
this.doEdit();
})
Text('保存')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.margin({ left: -44 })
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
}
@Builder
delModalBody() {
Column({ space: 12 }) {
Text(this.delTarget === 'race' ? '取消训放订单' : '退出养训课堂')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.textPrimary)
Text(this.delTarget === 'race' ? '将取消首笔训放订单并退还托管费,确认执行?' : '将退出首门进行中的课程,确认执行?')
.fontSize(13)
.fontColor(COLORS.textSecondary)
Row({ space: 10 }) {
Button()
.layoutWeight(1)
.height(38)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.onClick(() => {
this.delOpen = false;
})
Text('取消')
.fontSize(13)
.fontColor(COLORS.textSecondary)
.margin({ left: -52 })
Button()
.layoutWeight(1)
.height(38)
.backgroundColor(COLORS.danger)
.borderRadius(8)
.onClick(() => {
this.doDel();
})
Text('确认')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.white)
.margin({ left: -44 })
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(COLORS.cardBg)
}
@Builder
bottomBar() {
Row() {
ForEach(NAV_LIST, (it: NavItem, idx: number) => {
Column({ space: 2 }) {
Text(it.icon)
.fontSize(20)
.opacity(this.mainTab === idx ? 1 : 0.55)
Text(it.label)
.fontSize(10)
.fontWeight(this.mainTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.mainTab === idx ? COLORS.primary : COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.mainTab = idx;
})
}, (it: NavItem) => it.label)
}
.width('100%')
.height(56)
.backgroundColor(COLORS.cardBg)
.borderRadius({ topLeft: 16, topRight: 16 })
}
@Builder
mainContent() {
Column() {
this.header()
if (this.mainTab === 0) {
this.subNav()
Scroll() {
Column() {
if (this.subTab === 0) {
this.pageFeatured()
} else if (this.subTab === 1) {
this.pagePigeons()
} else if (this.subTab === 2) {
this.pageLoft()
} else if (this.subTab === 3) {
this.pageRaces()
} else if (this.subTab === 4) {
this.pageCourse()
} else if (this.subTab === 5) {
this.pageCircle()
} else {
this.pageGear()
}
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.padding({ left: 14, right: 14, bottom: 20 })
} else if (this.mainTab === 1) {
Scroll() {
Column() {
this.pagePigeons()
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.padding({ left: 14, right: 14, bottom: 20 })
} else if (this.mainTab === 2) {
Scroll() {
Column() {
this.pageCourse()
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.padding({ left: 14, right: 14, bottom: 20 })
} else {
Scroll() {
Column() {
this.pageMine()
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.padding({ left: 14, right: 14, bottom: 20 })
}
}
.width('100%')
.height('100%')
}
build() {
Stack() {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
this.fxLayer()
Column() {
this.mainContent()
this.bottomBar()
}
.width('100%')
.height('100%')
if (this.addOpen) {
Stack() {
this.modalOverlay()
Column() {
this.addModalBody()
}
.width('88%')
.constraintSize({ maxHeight: '80%' })
.borderRadius(16)
.zIndex(999)
}
.width('100%')
.height('100%')
}
if (this.editOpen) {
Stack() {
this.modalOverlay()
Column() {
this.editModalBody()
}
.width('88%')
.constraintSize({ maxHeight: '80%' })
.borderRadius(16)
.zIndex(999)
}
.width('100%')
.height('100%')
}
if (this.delOpen) {
Stack() {
this.modalOverlay()
Column() {
this.delModalBody()
}
.width('88%')
.constraintSize({ maxHeight: '80%' })
.borderRadius(16)
.zIndex(999)
}
.width('100%')
.height('100%')
}
}
.width('100%')
.height('100%')
}
}
9.9 对HarmonyOS生态应用开发的启示

鸽语·信鸽竞翔与赛鸽俱乐部应用虽然是一个垂直领域的产品,但它所采用的技术方案和设计思路,对整个HarmonyOS生态的应用开发都具有参考价值。
第一,声明式UI是未来方向。ArkUI的声明式开发范式在开发效率、代码质量、可维护性等方面都具有明显优势。随着HarmonyOS生态的不断壮大,越来越多的应用将采用声明式方式构建,掌握声明式开发技能将成为开发者的核心竞争力。
第二,@Observed + @Builder + @State的组合,构成了ArkUI状态管理的"黄金三角"。@State管理组件内部状态,@Observed管理复杂数据模型,@Builder实现UI模块化复用。三者配合使用,可以构建出既高效又易维护的应用。
第三,Stack/Column/Row三驾马车的布局体系,简洁而强大。开发者不需要记忆复杂的布局属性,只需掌握三个核心容器和几个常用属性,就能应对绝大多数布局需求。这种"简单原语+组合"的设计思路,降低了学习门槛,提升了开发效率。
第四,颜色系统的类型化定义值得推广。通过接口约束颜色字段,使用常量对象集中管理颜色值,既保证了类型安全,又便于统一调整。在大型团队协作中,这种规范化的颜色管理能够有效减少视觉不一致的问题。
第五,特效层的穿透设计是一个实用的技术技巧。在需要添加装饰性特效但不希望影响交互的场景中,hitTestBehavior(HitTestMode.None)是一个简洁而有效的解决方案。
第六,纯函数工具库的实践值得借鉴。将可复用的计算逻辑、映射逻辑抽取为纯函数,集中管理,不仅提高了代码的复用性和可测试性,也使得组件代码更加专注于UI描述本身。
更多推荐

所有评论(0)