HarmonyOS 6钟表收藏电商平台通过 Stack 布局叠加在界面之上,并使用 hitTestBehavior(HitTestMode.None) 确保特效不会干扰用户的正常交互
一、技术背景
1.1 HarmonyOS 声明式 UI 框架
HarmonyOS 是华为推出的面向全场景的分布式操作系统,其声明式 UI 开发框架 ArkUI 为开发者提供了一套高效、直观的界面构建方式。与传统命令式 UI 不同,声明式 UI 的核心思想是"描述 UI 应该是什么样子",而非"一步步告诉系统怎么做"。开发者通过组合式的组件结构来描述界面状态,框架会自动处理状态变化与视图更新之间的映射关系。
在 ArkUI 声明式开发范式中,每一个界面元素都由 @Component 装饰的结构体来描述,build() 方法则定义了组件的渲染内容。这种模式带来了诸多优势:代码结构更清晰、状态管理更集中、可读性和可维护性大幅提升。对于钟表收藏电商这类界面层次复杂、交互丰富的应用而言,声明式 UI 框架能够显著降低开发复杂度,让开发者将更多精力投入到业务逻辑和用户体验的打磨上。
ArkUI 还提供了丰富的内置组件和能力,包括 Column、Row、Stack 等布局组件,Text、Button、Image 等基础组件,以及 Scroll、List、Grid 等滚动容器。配合链式调用的属性设置方式,开发者可以非常灵活地构建各种复杂的界面效果。本文分析的钟表收藏电商应用,正是充分利用了这些组件能力,构建了层次丰富、交互流畅的用户界面。
1.2 ArkTS 语言特性
ArkTS 是 HarmonyOS 应用开发的主力语言,它在 TypeScript 的基础上进行了扩展,融合了声明式 UI、状态管理等特性,同时保持了 TypeScript 的类型安全和语法灵活性。ArkTS 支持类、接口、装饰器、泛型等现代编程语言特性,开发者可以用面向对象或函数式的方式来组织代码。
在类型系统方面,ArkTS 提供了严格的静态类型检查,能够在编译期发现大量潜在错误,这对于大型应用的可维护性至关重要。本文分析的代码中定义了多个数据模型类(如 WatchItem、OrderItem、CourseItem 等),通过明确的类型定义保证了数据在整个应用流转过程中的一致性和安全性。
装饰器是 ArkTS 中极具特色的语法特性。@Entry 标记页面入口组件,@Component 标记自定义组件,@State 标记组件内部状态,@Observed 标记可观察的数据类,@Builder 标记可复用的 UI 构建函数。这些装饰器不仅让代码语义更清晰,也让框架能够更精准地进行依赖追踪和性能优化。理解这些装饰器的工作原理,是深入掌握 ArkTS 开发的关键。
1.3 HarmonyOS 生态与跨设备能力
HarmonyOS 的核心设计理念是"一次开发,多端部署",通过分布式软总线、分布式数据管理、分布式任务调度等技术,实现了不同设备之间的无缝协同。对于电商类应用而言,这意味着用户可以在手机上浏览商品、在平板上查看详情、在手表上接收通知,体验连贯且一致。
跨设备能力的实现依赖于 ArkUI 的自适应布局机制。开发者可以使用百分比尺寸、layoutWeight、Flex 布局等方式构建弹性界面,让应用能够自动适配不同尺寸的屏幕。本文分析的钟表收藏电商应用大量使用了 layoutWeight(1) 和百分比宽度,确保界面在各种设备上都能良好呈现。这种设计思路与响应式 Web 开发有异曲同工之妙,但在原生层面提供了更优的性能表现。
HarmonyOS 还提供了原子化服务(Atomic Service)能力,应用可以以更轻量化的形式被用户获取和使用。对于钟表收藏这类垂直领域的应用而言,原子化服务意味着用户无需安装完整应用,即可通过服务卡片等方式快速获取核心信息(如今日走时精度、修复工单进度等),极大地降低了用户的使用门槛。
1.4 @Observed 装饰器与状态管理机制
状态管理是现代 UI 框架的核心议题之一。在 ArkTS 中,状态管理通过一套精心设计的装饰器体系来实现。@State 装饰器用于标记组件内部的状态变量,当状态变化时,框架会自动触发组件的重新渲染。@Observed 装饰器则用于标记类,使得该类的实例属性变化能够被框架追踪,从而实现细粒度的状态更新。
与 @State 只能追踪基本类型和对象引用不同,@Observed 装饰的类可以追踪其属性的深层变化。这意味着当你修改一个 @Observed 类实例的某个属性时,只有依赖该属性的 UI 部分会被更新,而不是整个组件都重新渲染。这种细粒度的更新机制对于性能优化至关重要,尤其是在处理列表、表格等大量数据的场景下。
本文分析的应用中,WatchItem、OrderItem、CourseItem、WatchFriendItem 四个数据模型类都使用了 @Observed 装饰器。这使得当修复工单的表款名称或服务项目被修改时,只有对应的工单卡片会更新,而不会影响列表中的其他卡片。这种设计在保证数据一致性的同时,也确保了界面的流畅性和响应速度。
除了 @State 和 @Observed,ArkTS 还提供了 @Prop、@Link、@Provide、@Consume 等多种状态装饰器,分别用于父子组件单向传递、双向绑定、跨层级共享等不同场景。理解这些装饰器的适用场景和区别,是构建高质量 HarmonyOS 应用的基础。
1.5 钟表收藏行业的技术特点与挑战
钟表收藏电商是一个高度专业化的垂直领域,与普通电商平台相比,它在技术实现上面临诸多独特的挑战。首先是商品信息的复杂度:一枚古董钟表的描述涉及品牌、型号、产地、年代、机芯类型、走时精度、品相等级、修复历史等数十个维度的信息,如何在有限的手机屏幕上清晰地呈现这些信息,同时保持界面的美观和易用性,是设计的核心难点。
其次是信任机制的构建。古董钟表价格昂贵,真伪鉴别和品质评估是用户决策的关键因素。本文分析的应用专门设置了"鉴定台"功能模块,通过专业鉴定师的认证和详细的鉴定报告,为用户提供可信的购买依据。在技术实现上,这需要支持富文本描述、多维度评级、鉴定师信息展示等功能。
第三是服务流程的可视化。钟表修复是一个周期较长的过程,涉及收件、拆检、修复、调校、交付等多个环节。用户需要清晰地了解自己的工单当前处于哪个阶段、预计还需要多长时间。应用通过五步进度条和详细的工单卡片,将复杂的修复流程直观地呈现给用户,降低了用户的等待焦虑,提升了服务透明度。
最后是社区运营的价值。钟表收藏是一个爱好者圈层特征明显的领域,藏家之间的交流和分享对平台的粘性至关重要。应用中的"藏家圈"模块为用户提供了分享入藏喜悦、修复心得、工艺笔记的社区空间,通过 UGC 内容构建平台的内容生态和社交关系链。
二、项目概览
2.1 应用功能全貌
本文分析的钟表收藏电商应用是一款面向古董钟表收藏爱好者的垂直电商平台,应用名称为"金陵东路·摆轮工坊"。应用整体采用底部 Tab 导航的经典结构,共设有四个主 Tab:首页、藏品、工坊、我的。每个主 Tab 承载不同的功能模块,覆盖了浏览、交易、服务、社区等核心业务场景。
首页是应用的信息聚合中心,采用顶部导航 + 内容分区的布局方式。首页内部又设有七个内容子 Tab,分别是:精选、钟表藏品、走时精度、修复工单、工坊课程、藏家圈、鉴定台。这七个子 Tab 以横向滚动的导航条形式呈现,用户可以左右滑动切换不同的内容板块,信息密度高但组织有序。
藏品页是应用的商品展示中心,以列表形式呈现所有在册古董藏品,每枚钟表都配有详细的信息卡片,展示品牌型号、产地年代、机芯类型、走时精度、价格和状态等关键信息。工坊页则展示修复服务价目表,涵盖机芯保养、三问修复、珐琅微修等多种专业服务项目。我的页是用户个人中心,展示用户信息、统计数据和功能入口。
2.2 视觉设计与品牌调性
应用在视觉设计上采用了复古与精致并重的风格,完美契合钟表收藏这一高雅领域的品牌调性。整体配色以暖色调为主,米黄色(#F7F1E3)作为背景色营造出温暖典雅的氛围,黄铜色(#B8912F)和金色(#D9B85C)作为主色调呼应钟表的金属质感,深棕色(#3D2F1A)作为标题文字色提升内容的厚重感。
在色彩体系的构建上,应用定义了多达 18 种颜色变量,涵盖背景、卡片、文字、分割线、强调色等各个维度。其中仅金属色系就有 brass、brassDeep、gold 三个层次,通过明暗深浅的变化构建出丰富的视觉层次。这种精细化的色彩管理方式不仅保证了界面风格的一致性,也为后续的主题扩展(如深色模式)奠定了良好基础。
界面元素大量使用圆角设计,卡片圆角从 10px 到 16px 不等,按钮则采用胶囊形的全圆角设计。圆角元素配合柔和的阴影和渐变效果,营造出温润精致的视觉感受,与古董钟表的精密质感形成呼应。此外,应用还巧妙地运用了线性渐变效果,如首页头部的"今日走时大卡"使用从深铜到浅金的 135 度渐变,营造出金属光泽的视觉效果。
2.3 整体架构设计
从代码结构来看,应用采用了单页面组件的架构模式,所有功能模块都在一个 Page 组件内实现,通过状态变量控制不同模块的显示与隐藏。这种架构在中小型应用中具有开发效率高、状态管理集中的优势。应用的主组件使用 @Entry 和 @Component 装饰器标记,表明它是一个页面级的入口组件。
状态管理方面,应用采用了 @State 装饰器管理组件级状态,配合 @Observed 装饰的数据模型类实现细粒度的数据追踪。主要的状态变量包括主 Tab 索引、子 Tab 索引、特效动画计时器、弹框状态、表单数据以及各业务列表数据。所有状态都集中在主组件中管理,通过 @Builder 装饰的方法将 UI 构建逻辑模块化。
UI 构建方面,应用大量使用 @Builder 装饰器将不同功能区域的 UI 代码封装为独立的构建函数,如 header()、subNav()、tabPick()、tabWatch()、fxLayer()、bottomBar() 等。这种方式既保持了代码的模块化和可维护性,又避免了创建过多子组件带来的状态传递复杂度。每个 @Builder 函数负责一个明确的 UI 区域,职责清晰,易于理解和修改。
在特效实现上,应用采用了 setInterval 驱动的帧动画方案,通过周期性更新 tick 和 glow 两个状态变量,驱动齿轮旋转和摆锤摆动等视觉效果。特效层独立于内容层,通过 Stack 布局叠加在界面之上,并使用 hitTestBehavior(HitTestMode.None) 确保特效不会干扰用户的正常交互。
2.4 交互与弹框设计
应用设计了三种类型的弹框交互,分别是发布藏家动态弹框、编辑修复工单弹框和删除确认弹框。这三种弹框统一采用遮罩层 + 居中卡片的模式,通过 if 条件渲染控制显示与隐藏,使用 constraintSize 限制最大高度,通过 zIndex 确保弹框层级在最上方。
每种弹框都遵循一致的设计规范:半透明深色遮罩(#2B2113B3)、白色圆角卡片、标题文字、表单内容或提示信息、底部双按钮操作区。点击遮罩层可以关闭弹框,提供了便捷的取消操作方式。删除确认弹框还根据删除目标的不同(工单退订或课程退课)显示不同的提示文案,体现了对业务细节的细致考量。
弹框中的表单使用 TextInput 组件实现,支持实时双向数据绑定。用户输入的内容会即时同步到组件状态变量中,点击确认按钮后再执行相应的业务逻辑(新增动态、修改工单、删除条目等)。这种先缓存再提交的模式,确保了用户可以随时取消操作而不会影响原有数据。
三、逐段代码分析
3.1 颜色体系接口定义
interface ColorPalette {
bg: string;
card: string;
card2: string;
title: string;
sub: string;
line: string;
brass: string;
brassDeep: string;
gold: string;
cream: string;
ink: string;
blue: string;
red: string;
green: string;
brown: string;
mask: string;
white: string;
}
应用首先定义了 ColorPalette 接口,用于规范整个应用的颜色体系。该接口包含 17 个颜色属性,涵盖了背景色、卡片色、文字色、分割线色、主题色、功能色等各个维度的颜色需求。接口的定义确保了颜色变量的类型安全,任何对颜色体系的引用都会经过类型检查,避免了拼写错误导致的运行时问题。
颜色命名采用语义化方式,而非直接使用颜色名称。例如 brass(黄铜色)、cream(奶油色)、ink(墨色)等名称不仅描述了颜色的视觉特征,也与应用的复古钟表主题相呼应。这种命名方式比简单的 primary、secondary 更具表达力,开发者在使用时能够直观地理解颜色的用途和视觉效果。
接口中还包含了两级卡片色(card 和 card2)和两级文字色(title 和 sub),通过颜色深浅的变化构建视觉层次。card 用于主卡片背景,card2 用于次级元素(如标签、输入框)的背景;title 用于主要文字,sub 用于次要说明文字。这种层次化的设计使得界面信息的轻重缓急能够通过颜色自然地区分开来。
特别值得注意的是 mask 颜色,它使用了带透明度的十六进制表示(#2B2113B3),最后两位 B3 表示约 70% 的不透明度。这种带透明度的遮罩色在弹框场景中非常实用,既能让用户感知到背后内容的存在,又能有效地将视觉焦点引导到弹框内容上。
3.2 颜色常量的实例化
const COLORS: ColorPalette = {
bg: '#F7F1E3',
card: '#FFFFFF',
card2: '#F0E7D0',
title: '#3D2F1A',
sub: '#8A7A58',
line: '#E5D9BC',
brass: '#B8912F',
brassDeep: '#8C6A1D',
gold: '#D9B85C',
cream: '#F5EBD3',
ink: '#2B2113',
blue: '#3E6C9E',
red: '#B5473A',
green: '#5F8F5A',
brown: '#7A5C33',
mask: '#2B2113B3',
white: '#FFFFFF'
};
COLORS 常量是 ColorPalette 接口的具体实现,定义了应用实际使用的颜色值。整个配色方案以暖色调为基调,背景色 #F7F1E3 是一种柔和的米黄色,营造出温暖、典雅的整体氛围。这种颜色选择与古董钟表的收藏属性高度契合,传达出一种历史感和品质感。
金属色系是整个配色方案的核心亮点。brass(黄铜色 #B8912F)、brassDeep(深黄铜 #8C6A1D)、gold(金色 #D9B85C)三个颜色构成了完整的金属色阶,从暗到亮分别用于不同的强调场景。brassDeep 用于最重要的交互元素(如选中态、主按钮),brass 用于次级强调,gold 用于高光和装饰。
功能色方面,blue、red、green 三个颜色分别用于信息、警示和成功三种语义场景。与常见的纯功能色不同,这些颜色都经过了暖色调的调和,例如绿色偏橄榄绿、红色偏赭石红,使其能够更好地融入整体的暖色调配色体系中,不会显得突兀。
将颜色定义为全局常量而非分散在各组件中,是一种优秀的工程实践。它确保了整个应用颜色使用的一致性,当需要调整配色方案时,只需修改这一处定义即可全局生效。同时,集中式的颜色管理也为后续实现主题切换(如深色模式)提供了便利,只需定义多套 ColorPalette 实例并在运行时切换即可。
3.3 藏品数据模型 WatchItem
@Observed
export class WatchItem {
id: number = 0;
name: string = '';
origin: string = '';
year: string = '';
movement: string = '';
error: string = '';
price: string = '';
status: string = '';
constructor(id: number, name: string, origin: string, year: string,
movement: string, error: string, price: string, status: string) {
this.id = id; this.name = name; this.origin = origin; this.year = year;
this.movement = movement; this.error = error; this.price = price; this.status = status;
}
}
WatchItem 是应用中最核心的数据模型之一,代表一枚古董钟表藏品。类使用 @Observed 装饰器标记,这意味着该类的所有实例属性变化都可以被 ArkUI 框架追踪,从而实现响应式的 UI 更新。当藏品的任何属性发生变化时,依赖该属性的界面元素会自动更新,无需手动触发刷新。
该类包含 8 个属性,全面描述了一枚古董钟表的核心信息:id 是唯一标识符,name 是表款名称,origin 是产地信息,year 是生产年代,movement 是机芯类型,error 是走时精度(日差),price 是价格,status 是当前状态。这些属性覆盖了收藏者在选购古董钟表时最关心的几个维度。
特别值得关注的是 movement(机芯)和 error(日差)这两个属性,它们是钟表收藏领域特有的专业信息。机芯类型(如手动上链、自动上链、导柱轮计时、三问报时等)直接影响钟表的收藏价值和技术含量,而日差数据则反映了钟表的走时精度,是评估其品质的重要指标。将这些专业信息作为一等公民纳入数据模型,体现了应用对垂直领域需求的深入理解。
构造函数采用了经典的全参数构造模式,所有属性都通过构造函数初始化。每个属性还设置了默认值(数字类型为 0,字符串类型为空字符串),这是一种防御性编程的实践,可以避免因未初始化而导致的运行时错误。@Observed 装饰器与 export 关键字的组合使用,使得该数据模型既可以被框架追踪状态变化,又可以在应用的其他模块中复用。
3.4 修复工单数据模型 OrderItem
@Observed
export class OrderItem {
id: number = 0;
watch: string = '';
service: string = '';
step: string = '';
fee: string = '';
days: string = '';
master: string = '';
status: string = '';
constructor(id: number, watch: string, service: string, step: string,
fee: string, days: string, master: string, status: string) {
this.id = id; this.watch = watch; this.service = service; this.step = step;
this.fee = fee; this.days = days; this.master = master; this.status = status;
}
}
OrderItem 类描述了修复工单的数据结构,同样使用 @Observed 装饰器以支持响应式更新。工单是钟表收藏电商平台的核心服务形态之一,用户将藏品送修后,可以通过工单追踪整个修复过程。该模型包含 8 个属性,完整记录了工单的各个维度信息。
watch 属性记录了送修的表款名称,service 描述了服务项目(如机芯完全保养、三问报时修复、珐琅面微修等),step 表示当前修复进度(如摆轮调校、音簧调音、轴件车削等),fee 是服务费用,days 是预计工期,master 是负责修复的师傅姓名,status 是工单当前状态。
这个数据模型的设计体现了钟表修复服务的专业性。与普通电商订单不同,钟表修复工单的进度不是简单的"待发货-已发货-已签收"流程,而是包含了收件、拆检、修复、调校、交付等多个专业步骤。step 属性以自由文本的形式记录当前具体工序,灵活地适应了各种复杂修复项目的进度描述需求。
master(修复师傅)字段的设置也很有特色。在钟表修复领域,技师的名气和手艺直接影响用户的选择和信任度。将修复师傅信息纳入工单模型,不仅让用户知道是谁在处理自己的藏品,也为后续的技师评价、预约指定等功能奠定了数据基础。
3.5 课程与藏家圈数据模型
@Observed
export class CourseItem {
id: number = 0;
name: string = '';
level: string = '';
price: string = '';
quota: string = '';
teacher: string = '';
tool: string = '';
status: string = '';
}
@Observed
export class WatchFriendItem {
id: number = 0;
nick: string = '';
avatar: string = '';
text: string = '';
likes: number = 0;
replies: number = 0;
tag: string = '';
time: string = '';
}
CourseItem 类描述了工坊课程的数据结构。课程是平台提供的另一项核心服务,面向希望学习钟表维修知识的爱好者。该模型包含课程名称、难度等级、价格、名额余量、讲师、工具要求和报名状态等属性。其中 level(等级)字段支持"入门"、“进阶”、“理论”、“体验”、"高级"等多种分类,配合不同的颜色标识,让用户可以快速识别课程的难易程度。
quota(名额)字段采用了"余 X/Y"的文本格式,同时包含了剩余名额和总名额信息。这种设计虽然在数据解析上不如数字类型方便,但在展示层面非常直观,用户一眼就能看到课程的热门程度和剩余机会。tool(工具)字段则描述了课程所需或提供的工具条件,这对于实操类课程来说是非常重要的参考信息。
WatchFriendItem 类描述了藏家圈动态的数据结构。藏家圈是应用的社区模块,用户可以在这里分享收藏心得、修复经验等内容。该模型包含了社区内容所需的基本要素:发布者昵称和头像、正文内容、点赞数、回复数、内容标签和发布时间。其中 tag 字段用于内容分类(如修复手记、入藏播报、机芯赏析、工艺笔记等),方便用户筛选感兴趣的内容类型。
这两个数据模型同样使用 @Observed 装饰器,确保课程报名状态的变化、社区动态点赞数的更新等都能实时反映到界面上。四个 @Observed 类(WatchItem、OrderItem、CourseItem、WatchFriendItem)构成了应用完整的数据层,支撑了首页、藏品、工坊、我的四大模块的内容展示。
3.6 图表数据结构与常量
interface AccChartItem {
day: string;
val: number;
color: string;
}
const accCharts: AccChartItem[] = [
{ day: '一', val: 42, color: '#5F8F5A' },
{ day: '二', val: 68, color: '#8C6A1D' },
{ day: '三', val: 55, color: '#B8912F' },
{ day: '四', val: 82, color: '#D9B85C' },
{ day: '五', val: 74, color: '#B8912F' },
{ day: '六', val: 91, color: '#8C6A1D' },
{ day: '日', val: 63, color: '#5F8F5A' }
];
应用定义了 AccChartItem 接口来描述精度图表的数据项,包含日期标签、数值和颜色三个属性。与简单的数值数组不同,每个数据项都带有独立的颜色配置,这使得柱状图中的每一根柱子都可以有不同的颜色,形成渐变或分类的视觉效果。这种设计比统一颜色的图表更具表现力,也与应用整体丰富的配色体系相呼应。
accCharts 常量提供了一周七天的校准合格率模拟数据。数据呈现出明显的周中高、周末低的波动规律(周四最高 82%,周一最低 42%),这种曲线形态符合实际工坊的运营节奏——周中是工作高峰期,效率和合格率都较高。每根柱子的颜色也随着数值变化而变化,从绿色(低值)到黄铜色(中值)再到金色(高值),形成了自然的色彩过渡。
除了日精度图表,应用还定义了年代分布数据:
interface EraItem {
label: string;
pct: number;
color: string;
}
const eraList: EraItem[] = [
{ label: '怀表黄金期 1890-1930', pct: 92, color: '#D9B85C' },
{ label: '腕表启蒙期 1930-1950', pct: 76, color: '#B8912F' },
{ label: '自动机芯期 1950-1975', pct: 58, color: '#8C6A1D' },
{ label: '石英风暴后 1975-1990', pct: 27, color: '#7A5C33' }
];
EraItem 接口描述了年代分布数据项,包含年代标签、百分比和颜色。eraList 常量定义了四个钟表历史时期的馆藏占比数据,从怀表黄金期的 92% 逐渐递减到石英风暴后的 27%。这个数据分布符合古董收藏的一般规律——年代越久远、越稀有的藏品收藏价值越高,因此在馆藏中的占比呈现出前高后低的特征。
年代数据的颜色设计也很有讲究,金色(最有价值时期)→ 黄铜色 → 深黄铜色 → 棕色,颜色随着年代接近现代而逐渐变暗,既形成了视觉上的层次感,也暗含了收藏价值由高到低的隐喻。这种将数据语义与视觉表现深度融合的设计手法,体现了开发者对用户体验的细致打磨。
3.7 全局模拟数据的设计
const watchList: WatchItem[] = [
new WatchItem(1, '百达翡丽 Ref.96', '瑞士·日内瓦', '1936年', '手动上链 12法分', '日差 +4s', '¥286,000', '在展'),
new WatchItem(2, '江诗丹顿骨雕怀表', '瑞士·日内瓦', '1912年', '三问报时机芯', '日差 +7s', '¥198,000', '洽藏中'),
new WatchItem(3, '浪琴 13ZN 计时', '瑞士·圣伊米耶', '1940年', '导柱轮计时机芯', '日差 +3s', '¥86,000', '在展'),
new WatchItem(4, '欧米茄 30T2 镀金', '瑞士·比尔', '1947年', '手动上链 30T2', '日差 +2s', '¥18,800', '可售'),
new WatchItem(5, '上海 A581 初代', '中国·上海', '1958年', '仿瑞士机芯 SS-1', '日差 -12s', '¥9,600', '可售'),
new WatchItem(6, '劳力士 1570 机芯', '瑞士·日内瓦', '1968年', '自动上链 1570', '日差 +1s', '¥52,000', '修复中'),
new WatchItem(7, '精工 45King 高振频', '日本·信州', '1969年', '36000VPH 手动', '日差 +5s', '¥26,500', '可售'),
new WatchItem(8, '瓦什隆珐琅怀表', '法国·巴黎', '1905年', '大明火珐琅面', '日差 +9s', '¥158,000', '洽藏中')
];
应用在代码中直接硬编码了三组核心业务数据:藏品列表(8 条)、工单列表(6 条)、课程列表(6 条)和藏家圈动态(8 条)。这些数据虽然是模拟的,但设计得非常考究和专业,每一条数据都包含了真实的钟表品牌、型号、年代和技术参数,充分展现了开发团队对钟表收藏领域的深入了解。
藏品列表的 8 枚钟表涵盖了多个知名品牌(百达翡丽、江诗丹顿、浪琴、欧米茄、劳力士、精工等)和不同产地(瑞士、中国、日本、法国),年代从 1905 年到 1969 年不等,价格从近万元到近三十万元跨度较大。这种多样化的数据设计使得界面展示更加丰富,也能满足不同层次收藏爱好者的浏览需求。
特别值得一提的是"上海 A581 初代"这款国表的加入。在以瑞士名表为主的藏品列表中,加入一款国产表不仅丰富了藏品的地域多样性,也能唤起国内用户的民族情感共鸣。这款表的日差为 -12 秒,明显差于其他进口藏品,这种真实的参数设定反而增加了数据的可信度——毕竟初代国产表在精度上确实与瑞士名表存在差距。
工单数据和课程数据同样经过精心设计。工单数据中的修复步骤(摆轮调校、音簧调音、轴件车削、游丝整圆等)都是钟表修复的真实工序,工期从 7 天到 60 天不等,费用从数百元到数千元,反映了不同修复项目的复杂度差异。课程数据则涵盖了从入门体验到高级技术的完整层级,为用户提供了清晰的学习路径。
3.8 底部导航常量定义
interface NavItem {
icon: string;
label: string;
}
const NAV_LIST: NavItem[] = [
{ icon: '⏱️', label: '首页' },
{ icon: '🕰️', label: '藏品' },
{ icon: '🔧', label: '工坊' },
{ icon: '🧑🔬', label: '我的' }
];
底部导航是应用的主要导航方式,通过 NavItem 接口和 NAV_LIST 常量来定义。每个导航项包含一个 emoji 图标和文字标签。使用 emoji 作为图标是一种轻量级的实现方式,不需要额外的图片资源,也能保证在不同设备上的一致性显示。
四个主 Tab 的选择体现了典型的电商 + 社区应用的功能架构:首页是内容聚合入口,藏品是商品浏览,工坊是服务展示,我的是个人中心。这种四分法的导航结构清晰明了,用户能够快速理解和上手。每个 Tab 对应的 emoji 图标也经过精心选择,与 Tab 的功能含义高度契合——秒表代表首页(时间相关)、座钟代表藏品(钟表收藏)、扳手代表工坊(维修服务)、科学家代表我的(个人身份)。
将导航配置抽离为独立的常量数组,是一种良好的解耦实践。当需要调整导航项(如修改文字、更换图标、调整顺序)时,只需修改这一处数据即可,无需改动 UI 构建代码。这种数据驱动的设计模式也为后续实现动态导航(如根据用户角色显示不同 Tab)提供了可能性。
NavItem 接口的定义虽然简单(仅两个字段),但它建立了导航数据的类型契约。随着应用功能的扩展,未来可以很方便地在接口中增加新的字段(如 badge 角标、route 路由路径等),而不会破坏现有的代码结构。
3.9 顶层纯函数工具集
function accBarH(val: number): string {
return (val * 0.8).toString() + '%';
}
function errorColor(e: string): string {
if (e === '日差 +1s' || e === '日差 +2s' || e === '日差 +3s') { return COLORS.green; }
if (e === '日差 -12s') { return COLORS.red; }
return COLORS.brass;
}
function watchStatusColor(s: string): string {
if (s === '洽藏中') { return COLORS.red; }
if (s === '在展') { return COLORS.blue; }
if (s === '修复中') { return COLORS.brassDeep; }
return COLORS.green;
}
应用定义了一组顶层纯函数,用于处理各种数据转换和逻辑判断。这些函数不依赖任何组件状态,输入输出关系明确,可以在代码的任何位置调用。将通用逻辑抽离为纯函数是函数式编程的核心思想之一,它能有效提高代码的可测试性和可复用性。
accBarH 函数用于计算精度柱状图的高度百分比,将原始数值(0-100)乘以 0.8 后转换为百分比字符串。这个 0.8 的缩放系数确保了柱状图不会占满整个容器高度,留出了适当的顶部空间,使图表看起来更加美观和均衡。
errorColor 函数根据日差数据返回对应的颜色值。日差在 +3 秒以内(精度优秀)显示为绿色,日差为 -12 秒(精度较差)显示为红色,其他情况显示为黄铜色。这种通过颜色直观反映走时精度的设计,让用户一眼就能判断一枚钟表的走时品质。不过目前的实现方式是字符串精确匹配,如果数据格式稍有变化就可能失效,更健壮的实现方式应该是解析数值后进行比较判断。
watchStatusColor 函数根据藏品状态返回对应的颜色值。“洽藏中”(热门、抢手)显示为红色以吸引注意,"在展"显示为蓝色表示信息展示,"修复中"显示为深黄铜色表示进行中,其他状态(可售)显示为绿色表示可购买。每种状态颜色都有明确的语义含义,帮助用户快速识别藏品的当前状态。
此外还有几个与特效相关的工具函数:
function gearAngle(tick: number): number {
return (tick * 24) % 360;
}
function pendAngle(glow: boolean): number {
return glow ? 18 : -18;
}
function sparkX(seed: number, tick: number): number {
return ((seed * 43 + tick * 5) % 92) + 4;
}
function sparkY(seed: number, tick: number): number {
return ((seed * 29 + tick * 9) % 86) + 7;
}
gearAngle 函数根据计时器 tick 值计算齿轮的旋转角度,每 tick 旋转 24 度,对 360 取模实现循环。pendAngle 函数根据 glow 布尔值返回摆锤的摆动角度,在 +18 度和 -18 度之间切换,模拟钟摆左右摆动的效果。
sparkX 和 sparkY 函数用于计算金属屑光点的位置,通过种子值和 tick 值的组合运算生成伪随机位置。使用不同的乘数(43 和 29)确保 X 和 Y 方向的运动模式不同步,从而产生更加自然和随机的漂浮效果。% 92 + 4 和 % 86 + 7 的运算确保光点始终在容器范围内移动,不会超出边界。
3.10 主组件与状态定义
@Entry
@Component
struct Page {
@State mainTab: number = 0;
@State subTab: number = 0;
@State tick: number = 0;
@State glow: boolean = false;
// 弹框状态
@State addModal: boolean = false;
@State editModal: boolean = false;
@State delModal: boolean = false;
@State editIdx: number = -1;
@State delTarget: string = 'course';
@State editWatch: string = '';
@State editService: string = '';
@State addNick: string = '';
@State addText: string = '';
主组件 Page 使用 @Entry 和 @Component 两个装饰器标记。@Entry 表示这是页面的入口组件,一个页面有且仅有一个 @Entry 组件。@Component 则表示这是一个自定义组件,可以拥有自己的状态和构建方法。结构体(struct)是 ArkTS 中定义组件的方式,与类(class)不同,结构体更轻量,专门用于 UI 组件的定义。
状态变量是组件的核心数据,全部使用 @State 装饰器标记。这些状态变量可以分为几组:导航状态(mainTab、subTab)用于控制当前显示的页面和内容 Tab;动画状态(tick、glow)用于驱动特效动画;弹框状态(addModal、editModal、delModal)用于控制三种弹框的显示与隐藏;编辑数据(editIdx、delTarget、editWatch、editService、addNick、addText)用于缓存弹框中的表单数据。
mainTab 和 subTab 都使用数字类型存储索引值,默认值为 0 表示默认选中第一个 Tab。使用索引而非字符串标识符的好处是与 ForEach 的索引参数天然对应,切换 Tab 时只需赋值索引即可。但这种方式也有局限性——如果 Tab 顺序调整,所有引用索引的地方都需要同步修改。
弹框相关的状态设计体现了"单一弹框原则"——虽然有三种弹框,但在设计上确保同一时间最多只有一个弹框显示。在打开某个弹框的方法中,会显式将其他两个弹框的状态设为 false。这种设计虽然不是强制的(理论上通过状态组合可以同时显示多个弹框),但通过方法封装保证了行为的一致性。
3.11 列表状态数据初始化
@State orders: OrderItem[] = [
new OrderItem(1, '百达翡丽 Ref.96', '机芯完全保养', '摆轮调校', '¥2,800', '约 21 天', '顾师傅', '进行中'),
new OrderItem(2, '江诗丹顿骨雕怀表', '三问报时修复', '音簧调音', '¥6,800', '约 45 天', '老唐', '待配件'),
new OrderItem(3, '劳力士 1570 机芯', '自动陀换轴', '轴件车削', '¥1,200', '约 10 天', '小陆', '进行中'),
new OrderItem(4, '上海 A581 初代', '游丝整理校准', '游丝整圆', '¥860', '约 7 天', '顾师傅', '已收件'),
new OrderItem(5, '浪琴 13ZN 计时', '导柱轮检修', '计时测试', '¥3,600', '约 30 天', '老唐', '进行中'),
new OrderItem(6, '瓦什隆珐琅怀表', '珐琅面微修', '补珐琅', '¥4,200', '约 60 天', '特邀·巴黎', '候单')
];
@State courses: CourseItem[] = [
new CourseItem(1, '拆装机芯入门·ETA6497', '入门', '¥1,280', '余 4/8', '顾师傅', '提供练习机芯+镊子', '报名中'),
new CourseItem(2, '游丝整圆与调快慢', '进阶', '¥2,680', '余 2/6', '老唐', '需自备放大镜', '即将满员'),
new CourseItem(3, '摆轮平衡与校准', '进阶', '¥2,980', '余 5/6', '老唐', '含摆轮组件一套', '报名中'),
new CourseItem(4, '古董表鉴定实务', '理论', '¥880', '余 12/20', '特邀讲师·沈', '案例全部真品', '热抢'),
new CourseItem(5, '珐琅修复体验半日', '体验', '¥680', '余 6/10', '特邀·巴黎工匠', '现场烧制小样', '报名中'),
new CourseItem(6, '洗油保养全流程', '高级', '¥3,980', '余 1/4', '顾师傅', '需完成入门课', '最后名额')
];
@State friends: WatchFriendItem[] = [
new WatchFriendItem(1, '顾师傅', '🔧', '今天给一只 1936 年的 Ref.96 做完全保养...', 386, 32, '修复手记', '1小时前'),
new WatchFriendItem(2, '老唐修表记', '⌚', '骨雕怀表的音簧终于调出教堂钟般的回响...', 342, 27, '修复手记', '3小时前'),
// ... 更多数据
];
fxTimer: number = -1;
应用在组件内部直接初始化了三组业务列表数据:工单列表、课程列表和藏家圈动态列表。与全局的 watchList、orderList、courseList 等常量不同,这些是组件级的 @State 变量,它们的值可以被用户操作修改(如添加工单、退课、发布动态等)。
将可变数据和不可变数据分开管理是一种清晰的设计思路。全局常量存储的是基础展示数据(如藏品列表),它们在应用运行期间不会改变;而组件状态变量存储的是用户可操作的数据(如我的工单、我的课程、我的动态),用户的增删改操作会直接反映在这些状态上。
工单列表和课程列表中的数据与全局常量中的对应数据基本一致,但存储为独立的 @State 变量,这样用户的操作(如编辑工单、退课)不会影响全局常量数据。这种数据副本的模式虽然在简单应用中可行,但在实际生产环境中通常会引入统一的数据管理层(如状态管理库或服务层)来避免数据不一致的问题。
fxTimer 变量用于存储特效动画的定时器 ID,它没有使用 @State 装饰器,因为它只是一个普通的实例变量,不需要触发 UI 更新。定时器 ID 在 aboutToAppear 生命周期中被赋值,在 aboutToDisappear 中被清除,确保了资源的正确管理。将定时器 ID 设为组件属性而非局部变量,是为了在组件销毁时能够正确地清除它,防止内存泄漏。
3.12 生命周期方法
aboutToAppear(): void {
this.fxTimer = setInterval(() => {
this.tick = this.tick + 1;
this.glow = !this.glow;
}, 600);
}
aboutToDisappear(): void {
clearInterval(this.fxTimer);
}
aboutToAppear 和 aboutToDisappear 是 ArkUI 组件的两个核心生命周期回调函数。aboutToAppear 在组件即将出现在屏幕上时被调用,适合执行初始化操作;aboutToDisappear 在组件即将从屏幕上移除时被调用,适合执行清理操作。这对生命周期函数构成了组件资源管理的完整闭环。
在 aboutToAppear 中,应用启动了一个 setInterval 定时器,每 600 毫秒执行一次回调。回调函数做两件事:将 tick 计数器加 1,以及翻转 glow 布尔值。tick 的递增驱动了齿轮的持续旋转和金属光点的位置变化,glow 的翻转则驱动了摆锤的左右摆动和光点的明暗闪烁。
600 毫秒的间隔选择是一个值得玩味的设计决策。它既不是很快(不会产生闪烁感),也不是很慢(不会让用户觉得卡顿),恰好能营造出一种机械钟表的节奏感。与真实钟表每秒滴答一次相比,600 毫秒的频率稍快一些,这是为了在有限的视觉停留时间内让用户明显感知到动画效果——如果真的设置为 1 秒,用户可能还没注意到齿轮在转动就已经划走了。
aboutToDisappear 中的 clearInterval 调用至关重要。如果组件销毁后定时器仍然运行,不仅会持续消耗 CPU 资源,还会因为回调函数中引用了已销毁组件的状态变量而导致内存泄漏或运行时错误。在声明式 UI 框架中,开发者必须自觉维护好资源的创建与销毁平衡,aboutToAppear 和 aboutToDisappear 就是实现这种平衡的关键钩子。
3.13 弹框控制方法
openAdd(): void {
this.addNick = '';
this.addText = '';
this.addModal = true;
this.editModal = false;
this.delModal = false;
}
openEdit(idx: number): void {
this.editIdx = idx;
this.editWatch = this.orders[idx].watch;
this.editService = this.orders[idx].service;
this.addModal = false;
this.editModal = true;
this.delModal = false;
}
openDelOrder(idx: number): void {
this.delTarget = 'order';
this.editIdx = idx;
this.addModal = false;
this.editModal = false;
this.delModal = true;
}
应用定义了三个打开弹框的方法:openAdd(打开发布动态弹框)、openEdit(打开编辑工单弹框)、openDelOrder(打开删除工单确认弹框),以及 openDelCourse(打开退课确认弹框)。每个方法都遵循相同的模式:先设置弹框所需的数据,再将目标弹框的状态设为 true,同时将其他弹框的状态设为 false。
这种"打开即重置"的设计模式确保了弹框每次打开时都是干净的状态。以 openAdd 为例,它在打开弹框前会清空 addNick 和 addText 两个输入框的内容,避免上一次的输入残留到下一次操作中。openEdit 则会根据传入的索引从工单列表中取出对应的数据,填充到 editWatch 和 editService 中,实现编辑功能的数据回填。
openDelOrder 和 openDelCourse 共用同一个删除确认弹框(delModal),通过 delTarget 字段区分删除目标类型(‘order’ 或 ‘course’)。这种共用弹框的设计减少了代码重复,但也需要在删除执行方法中根据目标类型执行不同的删除逻辑。设计上的取舍体现了代码复用与职责单一之间的平衡。
每个方法都会显式关闭其他两个弹框,虽然在正常使用流程中不太可能出现多个弹框同时打开的情况,但这种防御性编程确保了即使出现异常状态,也只会有一个弹框显示。同时设置三个布尔值的写法虽然略显冗余,但逻辑清晰、可读性好,比用一个枚举值来表示当前弹框类型的方式更直观。
3.14 弹框业务操作方法
doAdd(): void {
if (this.addText.length > 0) {
const n = this.friends.length + 1;
this.friends.push(new WatchFriendItem(n, this.addNick.length > 0 ? this.addNick : '新表友', '⌚', this.addText, 0, 0, '新动态', '刚刚'));
this.addModal = false;
}
}
doEdit(): void {
if (this.editIdx >= 0 && this.editIdx < this.orders.length) {
this.orders[this.editIdx].watch = this.editWatch;
this.orders[this.editIdx].service = this.editService;
this.editModal = false;
}
}
doDel(): void {
if (this.delTarget === 'order') {
if (this.editIdx >= 0 && this.editIdx < this.orders.length) {
this.orders.splice(this.editIdx, 1);
this.delModal = false;
}
} else {
if (this.editIdx >= 0 && this.editIdx < this.courses.length) {
this.courses.splice(this.editIdx, 1);
this.delModal = false;
}
}
}
与打开弹框的方法对应,应用定义了三个执行业务操作的方法:doAdd(发布动态)、doEdit(保存编辑)、doDel(确认删除)。这些方法是弹框交互的核心逻辑,负责将用户的操作转化为数据变化。
doAdd 方法首先检查正文内容是否非空,这是一个基本的表单验证——没有内容的动态不应该被发布。然后创建一个新的 WatchFriendItem 实例,ID 取当前列表长度加 1(简单的自增 ID 策略),昵称如果用户没填则默认为"新表友",头像使用默认的手表 emoji,点赞数和回复数初始为 0,标签设为"新动态",时间设为"刚刚"。创建完成后通过 push 方法添加到 friends 列表末尾,然后关闭弹框。
doEdit 方法根据 editIdx 索引找到对应的工单,将弹框中编辑的表款名称和服务项目写回到工单对象中。由于 OrderItem 类使用了 @Observed 装饰器,属性值的修改会被框架自动追踪,对应的工单卡片会立即更新显示。修改完成后关闭弹框。方法中还对索引进行了边界检查,确保不会越界访问数组。
doDel 方法根据 delTarget 的值执行不同的删除操作:如果是工单则从 orders 数组中移除对应项,如果是课程则从 courses 数组中移除对应项。使用 splice 方法可以原地修改数组,直接触发 UI 更新。与编辑方法一样,删除前也进行了索引边界检查,增强了代码的健壮性。
这三个操作方法体现了一个共同的设计原则:操作验证 → 数据修改 → 关闭弹框。先验证再操作的顺序确保了数据的完整性和一致性,而操作完成后立即关闭弹框的设计则给用户明确的操作成功反馈。
3.15 特效层 Builder
@Builder
fxLayer() {
Stack({ alignContent: Alignment.TopStart }) {
ForEach([0, 1], (s: number) => {
Column() {
Text('⚙️')
.fontSize(22 - s * 6)
.rotate({ angle: gearAngle(this.tick) + s * 30 })
}
.position({ x: s === 0 ? 6 : 78, y: s === 0 ? 12 : 30 })
}, (s: number) => s.toString())
fxLayer 是特效层的构建函数,使用 @Builder 装饰器标记。@Builder 是 ArkTS 中特有的装饰器,用于将 UI 构建逻辑封装为可复用的函数。与普通方法不同,@Builder 函数内部可以使用声明式 UI 的语法(组件嵌套、属性链式调用等),调用时使用 this.xxx() 的方式,就像使用内置组件一样。
特效层使用 Stack 布局作为容器,因为各种特效元素(齿轮、摆锤、光点)需要绝对定位在不同的位置上。Stack 的 alignContent 设为 TopStart,表示子元素默认从左上角开始定位,配合 .position() 方法可以精确控制每个元素的坐标。
第一个 ForEach 循环创建了两个齿轮图标(⚙️),使用 emoji 字符作为齿轮的图形表示。第一个齿轮较大(22px),位于左上角(x:6, y:12);第二个齿轮较小(16px,22 - 1*6 = 16),位于右上角(x:78, y:30)。两个齿轮的旋转角度都通过 gearAngle 函数计算,基于 this.tick 值动态变化,并且第二个齿轮有 30 度的初始相位偏移,使得两个齿轮的转动节奏略有差异,更具视觉层次感。
齿轮的旋转通过 .rotate({ angle: ... }) 属性实现,这是 ArkUI 提供的变换属性之一。由于 tick 是 @State 变量,每次 tick 值变化时,gearAngle 函数会被重新计算,rotate 属性会被更新,从而驱动齿轮持续旋转。这种"状态驱动视图"的模式是声明式 UI 的核心思想。
ForEach([2, 3], (s: number) => {
Column() {
Text('🪝')
.fontSize(16)
.rotate({ angle: pendAngle(this.glow) * (s === 2 ? 1 : -1) })
}
.position({ x: s === 2 ? 40 : 60, y: 58 })
}, (s: number) => s.toString())
ForEach([4, 5, 6, 7, 8, 9], (s: number) => {
Column() {
Text('·')
.fontSize(s % 3 === 0 ? 12 : 8)
.fontColor(s % 3 === 0 ? COLORS.gold : COLORS.brass)
.opacity(this.glow ? 0.9 : 0.2)
}
.position({ x: sparkX(s, this.tick), y: sparkY(s + 2, this.tick) })
}, (s: number) => s.toString())
}
.width('100%')
.height('100%')
.clip(true)
}
第二个 ForEach 创建了两个摆锤图标(🪝),分别位于中下方的左右两侧(x:40 和 x:60,y:58)。摆锤的摆动角度由 pendAngle 函数计算,根据 glow 布尔值在 +18 度和 -18 度之间切换。两个摆锤的摆动方向相反(一个乘以 1,一个乘以 -1),形成对称摆动的视觉效果,就像钟表的双摆装置。
第三个 ForEach 创建了六个金属屑光点,使用简单的点字符(·)表示。光点有两种尺寸(12px 和 8px)和两种颜色(金色和黄铜色),通过 s % 3 === 0 的条件来区分,每三个中有一个较大较亮的光点,形成大小错落的视觉效果。光点的透明度随 glow 变化,在 0.9 和 0.2 之间切换,产生闪烁的效果。
光点的位置由 sparkX 和 sparkY 函数计算,每个光点有不同的种子值,结合 tick 的变化产生伪随机的漂浮运动。由于六个光点使用不同的种子值,它们的运动轨迹各不相同,看起来像是在容器内随机漂浮的金属碎屑,非常符合钟表工坊的场景氛围。
特效层最后设置了 .clip(true),确保超出容器范围的特效元素被裁剪掉,不会溢出到界面其他区域。整个特效层虽然元素不多,但通过巧妙的动画设计和位置布局,营造出了丰富的视觉层次感和场景氛围。
3.16 头部 Builder — 定位与搜索栏
@Builder
header() {
Column({ space: 10 }) {
Row({ space: 8 }) {
Column() { Text('📍').fontSize(15) }
.width(32).height(32).borderRadius(16).backgroundColor(COLORS.card)
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text('金陵东路·摆轮工坊').fontSize(11).fontColor(COLORS.sub)
Text('晴 22°C · 湿度 52%·宜机芯作业').fontSize(9).fontColor(COLORS.brassDeep)
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Row({ space: 6 }) {
Column() { Text('🔍').fontSize(14) }
.width(30).height(30).borderRadius(15).backgroundColor(COLORS.card)
.justifyContent(FlexAlign.Center)
Column() { Text('🛒').fontSize(14) }
.width(30).height(30).borderRadius(15).backgroundColor(COLORS.card)
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
header 是首页头部的构建函数,包含三个主要区域:定位信息栏、今日走时大卡和热门藏品横滚。头部是应用的品牌展示区和快捷入口区,设计上采用了信息密度适中、视觉层次分明的布局方式。
第一行是定位与搜索栏,采用 Row 横向布局。左侧是定位信息,包含一个圆形的定位图标按钮和两行文字信息。第一行文字显示工坊位置"金陵东路·摆轮工坊",这也是应用的名称;第二行文字显示天气信息"晴 22°C · 湿度 52%·宜机芯作业",将气象信息与钟表维修的行业特性结合起来——湿度和温度确实会影响钟表的走时精度,这种细节设计体现了对垂直领域的深刻理解。
中间使用一个 layoutWeight(1) 的空 Column 撑开空间,将左右两侧的内容推开。右侧是搜索和购物车两个圆形图标按钮,以较小的尺寸(30x30)排列,作为快捷功能入口。所有的图标按钮都使用白色圆形背景,与米黄色的背景形成对比,保持了视觉上的一致性。
这一行的设计巧妙地融合了品牌展示、环境信息和快捷操作三种功能。天气信息的加入不仅是装饰,更有实际的行业参考价值——高湿度环境下机芯容易受潮,温度变化会影响金属部件的热胀冷缩,这些都是钟表保养中需要关注的因素。
3.17 头部 Builder — 今日走时大卡

// 今日走时大卡
Row({ space: 12 }) {
Column({ space: 3 }) {
Text('工坊在修机芯').fontSize(10).fontColor(COLORS.cream)
Text('14').fontSize(30).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
Text('枚 · 平均校准 21 天').fontSize(9).fontColor(COLORS.cream)
}
.alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Column({ space: 5 }) {
Row({ space: 4 }) {
ForEach([0, 1, 2, 3, 4, 5], (b: number) => {
Column()
.width(9)
.height(b < 4 ? 8 + b * 7 : 0)
.borderRadius(4)
.backgroundColor(b < 3 ? COLORS.gold : COLORS.cream)
}, (b: number) => b.toString())
}
.alignItems(VerticalAlign.Bottom)
Text('交付准时率 · 99.2%').fontSize(8).fontColor(COLORS.cream)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(14)
.borderRadius(16)
.backgroundColor(COLORS.brass)
.linearGradient({
angle: 135,
colors: [['#8C6A1D', 0], ['#D9B85C', 1]]
})
"今日走时大卡"是头部最醒目的区域,使用了从深铜色到金色的 135 度线性渐变背景,营造出金属质感的视觉效果。卡片内部采用左右分栏布局:左侧展示在修机芯的数量统计,右侧展示交付准时率的微型柱状图。
左侧区域以大号数字"14"为视觉中心,配合"工坊在修机芯"的说明文字和"枚 · 平均校准 21 天"的补充信息,清晰地传达了工坊当前的工作负载和平均维修周期。数字使用 30px 的粗体白色文字,在渐变背景上非常醒目,让用户一眼就能获取关键信息。
右侧区域是一个由 6 根柱子组成的微型柱状图,通过 ForEach 循环生成。前 4 根柱子有高度(从 8px 到 29px 递增),后 2 根高度为 0 即不显示。前 3 根使用金色,第 4 根使用奶油色,通过颜色区分来增加图表的层次感。柱状图底部对齐(alignItems(VerticalAlign.Bottom)),确保柱子从底部向上生长。
柱状图下方是"交付准时率 · 99.2%"的文字说明,用数据量化工坊的服务质量。99.2% 的高准时率既是服务品质的体现,也是建立用户信任的重要手段。整个大卡区域将工坊的核心运营指标以直观的方式呈现给用户,既有数字展示又有图形化表达,信息传达效率很高。
3.18 头部 Builder — 热门藏品横滚
// 热门藏品横滚
Scroll() {
Row({ space: 10 }) {
ForEach(watchList, (w: WatchItem) => {
Column({ space: 4 }) {
Text('⌚').fontSize(18)
Text(w.name.length > 7 ? w.name.slice(0, 7) : w.name).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
Text(w.year).fontSize(8).fontColor(COLORS.brassDeep)
}
.width(92).padding({ top: 8, bottom: 8 }).borderRadius(10)
.backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
}, (w: WatchItem) => w.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.height(72)
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(COLORS.bg)
}
头部的第三个区域是热门藏品横滚列表,使用 Scroll 组件包裹 Row 实现横向滚动效果。每个藏品卡片固定宽度为 92px,竖向排列了手表图标、表款名称和年代信息三个元素。卡片使用白色圆角背景,在米黄色的头部背景上形成清晰的视觉层次。
表款名称做了长度截断处理(超过 7 个字符时截取前 7 位),确保在有限的卡片宽度内不会出现文字溢出。这种细节处理在移动端 UI 设计中非常重要,因为屏幕空间有限,必须保证各种长度的文本都能优雅地展示。虽然 ArkUI 也提供了 .maxLines() 和 .textOverflow() 等属性来处理长文本,但手动截断并配合省略号的方式可以更精确地控制展示效果。
横滚列表使用 .scrollBar(BarState.Off) 隐藏了滚动条,让界面更加简洁。用户通过左右滑动来浏览更多藏品,这种交互模式在移动端应用中非常普遍,用户学习成本低。滚动容器设置了固定高度 72px,确保头部区域的高度可控,不会因为内容变化而影响整体布局。
整个头部区域最后设置了内边距和背景色,形成了一个完整的视觉模块。头部设计遵循了"品牌展示 → 核心数据 → 快速浏览"的信息层次,用户从上到下依次获取品牌信息、工坊运营状态和热门藏品,逻辑清晰、节奏明快。
3.19 内容 Tab 导航 Builder
@Builder
subNav() {
Scroll() {
Row({ space: 8 }) {
ForEach(['精选', '钟表藏品', '走时精度', '修复工单', '工坊课程', '藏家圈', '鉴定台'], (t: string, i: number) => {
Column() {
Text(t)
.fontSize(11)
.fontColor(this.subTab === i ? COLORS.white : COLORS.sub)
.fontWeight(this.subTab === i ? FontWeight.Bold : FontWeight.Normal)
}
.padding({ left: 16, right: 16, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(this.subTab === i ? COLORS.brassDeep : COLORS.card)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.subTab = i; })
}, (t: string) => t)
}
.padding({ left: 12, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
}
subNav 是首页内容 Tab 的导航条构建函数,包含七个 Tab:精选、钟表藏品、走时精度、修复工单、工坊课程、藏家圈、鉴定台。由于 Tab 数量较多,超出了一屏的显示宽度,因此使用横向滚动的方式来容纳所有选项。
Tab 项采用了胶囊形按钮的设计风格,未选中状态为白色背景加灰色文字,选中状态为深黄铜色背景加白色粗体文字。选中与未选中之间有明显的视觉对比,用户可以清晰地知道当前所在的位置。每个 Tab 左右有 16px 的内边距,上下有 7px 的内边距,16px 的圆角半径形成了完整的胶囊形状。
Tab 切换通过 .onClick() 事件实现,点击时将 subTab 状态变量设为对应的索引值。由于 subTab 是 @State 变量,状态变化会触发 UI 更新,Tab 的样式(背景色、文字色、字重)会自动重新计算并应用。这种声明式的状态绑定使得交互逻辑非常简洁,开发者不需要手动操作 DOM 或调用刷新方法。
ForEach 的第二个参数(键生成函数)使用了 Tab 文字本身作为键,而不是索引值。这是一种良好的实践,因为当列表项的顺序或数量发生变化时,使用稳定的键值可以确保框架正确地追踪每个元素的身份,避免不必要的重建和状态丢失。
Tab 导航条的设计在信息架构上起到了承上启下的作用。它位于头部和内容区之间,既是内容分类的导航工具,也是当前位置的状态指示。七个 Tab 的设置涵盖了平台的主要内容维度,用户可以根据自己的兴趣快速切换到相应的内容板块。
3.20 精选 Tab — 横幅与双指标卡
@Builder
tabPick() {
Scroll() {
Column({ space: 10 }) {
Scroll() {
Row({ space: 10 }) {
ForEach([0, 1, 2], (b: number) => {
Column({ space: 6 }) {
Text(b === 0 ? '⌚ 春季名匠专场' : (b === 1 ? '🔧 修复工坊开放日' : '🎓 制表入门课'))
.fontSize(16).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
Text(b === 0 ? '八大古董珍品限时洽藏' : (b === 1 ? '观摩机芯完全保养' : '零基础拆装 ETA6497'))
.fontSize(9).fontColor(COLORS.cream)
Row({ space: 4 }) {
Text(b === 0 ? '去洽藏' : (b === 1 ? '去预约' : '去报名')).fontSize(9)
.fontColor(COLORS.brassDeep).padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(COLORS.white)
}
}
.width(200).padding(14).borderRadius(14).alignItems(HorizontalAlign.Start)
.backgroundColor(b === 0 ? COLORS.brassDeep : (b === 1 ? COLORS.blue : COLORS.green))
}, (b: number) => b.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
tabPick 是"精选"Tab 的内容构建函数,也是首页的默认展示内容。精选页聚合了平台最重要的信息和功能入口,采用竖向滚动 + 模块分区的布局方式,从上到下依次是横幅轮播、双指标卡和藏品大卡列表。
顶部横幅区域采用横向滚动的卡片轮播形式,展示三个运营活动 Banner:春季名匠专场(深黄铜色背景)、修复工坊开放日(蓝色背景)、制表入门课(绿色背景)。每个横幅卡片宽度为 200px,包含标题、副标题和行动按钮三个层次的信息,整体采用左对齐的排版方式。三个横幅使用不同的背景色进行区分,分别对应平台的三大业务方向:藏品交易、修复服务、教学培训。
行动按钮采用白色背景加深黄铜色文字的反色设计,在各自的彩色背景上形成鲜明的视觉焦点。按钮文案根据活动类型有所不同(“去洽藏”、“去预约”、“去报名”),使用更贴合场景的动词而非统一的"查看详情",这种细节化的文案设计能够有效提升用户的点击意愿。
横幅下方是"— 本周工坊志 —"的分隔文字,以居中对称的装饰线形式出现,既起到了内容分区的作用,又带有复古手账的风格,与整体的复古钟表主题相呼应。
Text('— 本周工坊志 —').fontSize(11).fontColor(COLORS.sub).width('100%').textAlign(TextAlign.Center)
Row({ space: 10 }) {
Column({ space: 6 }) {
Text('🔬').fontSize(26)
Text('校表仪实时').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('+1.2s/日 · 摆幅 285°').fontSize(9).fontColor(COLORS.green)
Text('测试机芯 1570').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(12).borderRadius(12)
.backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)
Column({ space: 6 }) {
Text('📦').fontSize(26)
Text('配件到货').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('NOS 摆轴 ×3 已入库').fontSize(9).fontColor(COLORS.brassDeep)
Text('老库存全新件·稀缺').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(12).borderRadius(12)
.backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)
}
.width('100%')
双指标卡区域并排展示两个实时信息卡片:校表仪实时数据和配件到货通知。两张卡片平分屏幕宽度(各占 layoutWeight(1)),使用统一的白色圆角卡片样式。每张卡片从上到下依次是图标(26px emoji)、标题、核心数据和补充说明,信息层次清晰。
"校表仪实时"卡片展示了当前正在测试的机芯数据:日差 +1.2 秒、摆幅 285 度。日差数据使用绿色显示,表示精度优秀(日差越小越好,+1.2 秒是非常不错的成绩)。摆幅 285 度也是一个健康的数值(通常满弦摆幅在 270-310 度之间为佳)。这些专业数据的展示增加了平台的技术感和专业度。
"配件到货"卡片展示了稀缺配件的入库信息:NOS 摆轴 3 个已入库。"NOS"是 New Old Stock 的缩写,指老库存全新零件,在古董表维修领域非常珍贵。这些专业术语的正确使用体现了平台对行业的深入理解,也能赢得专业用户的认可。
ForEach(watchList, (w: WatchItem) => {
Row({ space: 12 }) {
Column() { Text('⌚').fontSize(24) }
.width(56).height(56).borderRadius(28)
.border({ width: 2, color: COLORS.gold })
.backgroundColor(COLORS.cream)
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text(w.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(w.origin + ' · ' + w.year).fontSize(8).fontColor(COLORS.sub)
Row({ space: 6 }) {
Text(w.movement).fontSize(8).fontColor(COLORS.brassDeep)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor(COLORS.cream)
Text(w.error).fontSize(8).fontColor(errorColor(w.error))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor(COLORS.card2)
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 2 }) {
Text(w.price).fontSize(11).fontColor(COLORS.brassDeep).fontWeight(FontWeight.Bold)
Text(w.status).fontSize(8).fontColor(watchStatusColor(w.status))
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
}, (w: WatchItem) => w.id.toString())
}
.width('100%')
.padding(12)
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
精选页的下半部分是藏品大卡列表,展示全部 8 枚精选藏品。每张卡片采用左中右三栏布局:左侧是圆形的手表图标(56px 直径,带金色描边),中间是藏品信息(名称、产地年代、机芯标签、日差标签),右侧是价格和状态。
中间信息区的标签设计很有特色。机芯类型标签使用奶油色背景加深黄铜色文字,走时精度标签使用浅米色背景加动态颜色(根据精度等级显示绿色、红色或黄铜色)。两种不同深浅的背景色将两个标签区分开来,同时又保持了整体色调的统一。标签使用圆角矩形的胶囊形状,是移动端应用中常见的信息标记形式。
右侧的价格使用深黄铜色粗体字,突出显示价值信息;状态文字使用动态颜色,不同状态对应不同的语义色。价格和状态的右对齐布局符合用户的视觉习惯——用户浏览列表时,左侧看商品信息,右侧看价格和操作,视线移动路径最短。
整个精选页的内容容器使用 Scroll 组件包裹,支持竖向滚动浏览。滚动条设置为隐藏状态,保持界面简洁。内容区域设置了 12px 的内边距,确保内容与屏幕边缘有适当的呼吸空间。
3.21 钟表藏品 Tab — 表格式列表

@Builder
tabWatch() {
Scroll() {
Column({ space: 10 }) {
Text('在册古董藏品 TOP8').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
// 表头
Row({ space: 6 }) {
Text('表款').fontSize(9).fontColor(COLORS.sub).width(120)
Text('年代').fontSize(9).fontColor(COLORS.sub).width(46)
Text('日差').fontSize(9).fontColor(COLORS.sub).width(52)
Text('状态').fontSize(9).fontColor(COLORS.sub).width(52)
}
.width('100%').padding({ left: 12, right: 12, top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(COLORS.card2)
tabWatch 是"钟表藏品"Tab 的构建函数,采用表格形式展示藏品列表。与精选页的大卡列表不同,这里使用了更紧凑的表格式布局,在一屏内可以展示更多的藏品信息,适合用户进行横向对比和快速浏览。
页面顶部是标题"在册古董藏品 TOP8",明确了列表的内容和数量。接下来是表头行,使用浅米色(COLORS.card2)背景与数据行形成区分。表头包含四列:表款(120px)、年代(46px)、日差(52px)、状态(52px)。每列使用固定宽度的布局方式,确保表头和数据行的对齐。
固定宽度的表格布局在移动端开发中是一种简单直接的实现方式。它的优点是实现简单、对齐精确;缺点是灵活性不足,如果列的内容变长可能会出现溢出。对于藏品展示这种数据格式相对固定的场景,固定宽度布局是完全可以接受的选择。
ForEach(watchList, (w: WatchItem, i: number) => {
Row({ space: 6 }) {
Text((i + 1).toString() + '. ' + w.name).fontSize(10).fontColor(COLORS.title).width(120)
Text(w.year).fontSize(9).fontColor(COLORS.sub).width(46)
Text(w.error).fontSize(9).fontColor(errorColor(w.error)).fontWeight(FontWeight.Bold).width(52)
Text(w.status).fontSize(9).fontColor(watchStatusColor(w.status)).width(52)
}
.width('100%').padding({ left: 12, right: 12, top: 10, bottom: 10 })
.borderRadius(10)
.backgroundColor(COLORS.card)
.border({ width: i < 3 ? 1 : 0, color: COLORS.gold })
}, (w: WatchItem) => w.id.toString())
数据行的结构与表头一一对应,每列使用相同的宽度确保对齐。表款名称前加上了序号(i + 1),形成了排行榜的视觉效果。日差列使用了动态颜色(errorColor 函数)并加粗显示,让精度信息更加醒目。状态列同样使用动态颜色(watchStatusColor 函数),直观反映藏品状态。
一个巧妙的设计是:前三名的藏品卡片有金色边框(i < 3 时显示 1px 的金色边框)。这种设计强化了"TOP8"的排行榜属性,前三名作为重点推荐对象通过金色边框获得额外的视觉强调。边框颜色使用金色而非红色等亮色,既突出了前三名的特殊地位,又不会显得过于张扬,与整体的典雅风格保持一致。
表格设计的优势在于信息密度高、对比性强。用户可以快速扫视多行数据,比较不同藏品的年代、精度和状态,非常适合有明确选购目标的用户进行筛选和对比。与精选页的大卡列表(适合浏览探索)形成了互补,满足了不同使用场景下的信息需求。
Text('藏家入门三问').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 8 }) {
Column({ space: 4 }) {
Text('📜').fontSize(20)
Text('问出处').fontSize(10).fontColor(COLORS.title)
Text('档案与序号可查').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('🔧').fontSize(20)
Text('问保养').fontSize(10).fontColor(COLORS.title)
Text('上次洗油时间').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('🧲').fontSize(20)
Text('问磁场').fontSize(10).fontColor(COLORS.title)
Text('远离音响与包扣').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
}
.width('100%')
}
}
}
列表下方是"藏家入门三问"科普区块,以三个并排卡片的形式介绍了古董表收藏的入门知识:问出处(档案与序号可查)、问保养(上次洗油时间)、问磁场(远离音响与包扣)。三张卡片平分宽度,每张包含图标、标题和说明三个元素,采用居中对齐的布局方式。
这个科普区块的设计体现了平台的教育属性和用户关怀。对于刚入门的收藏爱好者来说,这些基础知识非常实用,能够帮助他们建立正确的收藏观念,避免踩坑。同时,提供有价值的科普内容也能提升平台的专业形象,增强用户的信任感和粘性。
"藏家入门三问"的内容选择也很有代表性。出处(真伪溯源)、保养(维护成本)、磁场(日常注意)是古董表收藏中最基础也最重要的三个问题,覆盖了选购、保养和使用三个关键环节。以简洁的卡片形式呈现,用户一眼就能记住,达到了很好的科普效果。
3.22 走时精度 Tab — 柱状图与年代分布
@Builder
tabAcc() {
Scroll() {
Column({ space: 12 }) {
Text('本周工坊校准合格率(%)').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 10 }) {
ForEach(accCharts, (c: AccChartItem) => {
Column({ space: 4 }) {
Column()
.width(20)
.height(accBarH(c.val))
.borderRadius(6)
.backgroundColor(c.color)
Text('周' + c.day).fontSize(8).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Center)
}, (c: AccChartItem) => c.day)
}
.width('100%')
.height(150)
.padding(12)
.borderRadius(12)
.backgroundColor(COLORS.card)
.alignItems(VerticalAlign.Bottom)
tabAcc 是"走时精度"Tab 的构建函数,通过数据可视化的方式展示工坊的校准质量和馆藏分布。这个 Tab 包含两个核心图表:本周校准合格率柱状图和馆藏年代分布进度条,以及一个精度小知识科普区块。
柱状图是第一个图表模块,标题为"本周工坊校准合格率(%)"。图表使用 Row 横向排列 7 根柱子,对应一周七天。每根柱子由一个竖向的 Column 组件构成,柱子的高度通过 accBarH 函数计算(数值 × 0.8 后转为百分比),颜色则直接使用数据项中定义的颜色值。柱子底部对齐(alignItems(VerticalAlign.Bottom)),确保所有柱子从同一基线向上生长,这是柱状图的标准布局方式。
柱子下方是日期标签(“周一”、"周二"等),使用 8px 的灰色小字,不抢视觉焦点。整个图表容器高度固定为 150px,使用白色卡片背景和 12px 圆角,与应用中其他卡片保持一致的设计语言。
纯代码实现的柱状图虽然功能相对简单(不支持交互、没有坐标轴数值),但对于展示简单的趋势数据已经足够。相比引入第三方图表库,这种纯组件实现的方式包体更小、性能更好,也更符合轻量级应用的设计原则。
Text('馆藏年代分布').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
ForEach(eraList, (e: EraItem) => {
Column({ space: 6 }) {
Row({ space: 8 }) {
Text('🕰️').fontSize(14)
Text(e.label).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
Column().layoutWeight(1)
Text(e.pct.toString() + '%').fontSize(10).fontColor(e.color).fontWeight(FontWeight.Bold)
}
.width('100%')
Row() {
Column().height(8).borderRadius(4).backgroundColor(e.color).width(e.pct.toString() + '%')
}
.width('100%')
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card2)
}, (e: EraItem) => e.label)
年代分布图是第二个图表模块,标题为"馆藏年代分布"。与柱状图的垂直方向不同,这个图表使用水平进度条的形式展示四个历史时期的馆藏占比。每个时期占一行,包含图标、年代标签、百分比数值和进度条四个元素。
进度条的实现非常巧妙:外层是一个全宽的 Row,内层是一个 Column(作为进度条本身),其宽度使用百分比值(e.pct.toString() + '%')。由于父容器是全宽的,子元素设置百分比宽度就会自动计算出对应的实际宽度,从而实现进度条的效果。进度条高度只有 8px,非常纤细,配合圆角设计显得精致优雅。
每行数据使用浅米色(COLORS.card2)的卡片背景,与柱状图的白色卡片形成层次区分。年代标签和百分比数值分别位于行的左右两端,中间通过 layoutWeight(1) 的空列撑开。百分比数值的颜色与进度条颜色一致,形成视觉上的呼应。
Text('精度小知识').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 8 }) {
Column({ space: 4 }) {
Text('📐').fontSize(20)
Text('方位差').fontSize(10).fontColor(COLORS.title)
Text('五方位逐项测试').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('🌡️').fontSize(20)
Text('温度差').fontSize(10).fontColor(COLORS.title)
Text('8°C/38°C 双温测试').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('🌀').fontSize(20)
Text('摆幅').fontSize(10).fontColor(COLORS.title)
Text('满弦 270° 为佳').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
}
.width('100%')
}
}
}
页面底部是"精度小知识"科普区块,展示了三个钟表精度相关的专业概念:方位差、温度差和摆幅。每个概念用一张卡片表示,包含图标、名称和简要说明。卡片采用居中对齐的布局,三卡片平分宽度。
"方位差"指的是钟表在不同摆放方位(面上、面下、柄左、柄右、柄上)下的走时误差差异,专业的校表需要在五个方位逐项测试。"温度差"指的是温度变化对走时精度的影响,高精度钟表需要在 8°C 和 38°C 两种温度下分别测试。"摆幅"指的是摆轮左右摆动的角度,满弦状态下摆幅在 270 度左右为最佳。
这些专业知识的展示不仅丰富了页面内容,也体现了平台的专业深度。对于懂行的收藏爱好者来说,这些术语能够引起共鸣;对于新手用户来说,也能从中学习到有用的知识。科普内容的加入让应用不仅仅是一个交易工具,更是一个学习和交流的平台。
3.23 修复工单 Tab — 进度条与工单卡
@Builder
tabOrder() {
Scroll() {
Column({ space: 12 }) {
Text('我的修复工单').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
// 五步流程图例
Row({ space: 0 }) {
ForEach(['收件', '拆检', '修复', '调校', '交付'], (s: string, i: number) => {
Column({ space: 4 }) {
Column() {
Text((i + 1).toString()).fontSize(9).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
}
.width(20).height(20).borderRadius(10)
.backgroundColor(i < 3 ? COLORS.brassDeep : COLORS.line)
.justifyContent(FlexAlign.Center)
Text(s).fontSize(8).fontColor(i < 3 ? COLORS.brassDeep : COLORS.sub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: string) => s)
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
tabOrder 是"修复工单"Tab 的构建函数,展示用户的修复工单列表。工单页面的设计重点在于进度可视化,让用户能够清晰地了解自己的藏品修复到了哪个阶段。
页面顶部是一个五步流程图例,展示了钟表修复的完整流程:收件 → 拆检 → 修复 → 调校 → 交付。每个步骤用一个圆形数字标识 + 文字标签的形式表示,五个步骤平均分配宽度(各占 layoutWeight(1))。
已完成的步骤(前 3 步)使用深黄铜色填充圆形和文字,未完成的步骤(后 2 步)使用浅灰色。颜色的区分清晰地传达了当前的整体进度。虽然这只是一个静态的总体进度示意(没有根据具体工单动态变化),但它为用户建立了修复流程的心理模型,让用户知道后续还有哪些步骤。
这种五步流程的设计在服务类应用中非常常见,它的价值在于将复杂的服务过程透明化,降低用户的不确定性和焦虑感。对于古董钟表修复这种周期长、专业性强的服务来说,进度可视化尤其重要——用户把价值不菲的藏品送修,最关心的就是修到哪一步了、什么时候能修好。
ForEach(this.orders, (o: OrderItem, i: number) => {
Column({ space: 8 }) {
Row({ space: 8 }) {
Column() { Text('🔧').fontSize(20) }
.width(40).height(40).borderRadius(12)
.backgroundColor(COLORS.cream)
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text(o.watch).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(o.service + ' · 当前:' + o.step).fontSize(8).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(o.status).fontSize(9).fontColor(watchStatusColor(o.status === '进行中' ? '修复中' : (o.status === '待配件' ? '洽藏中' : '在展')))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(COLORS.card2)
}
.width('100%')
Row({ space: 10 }) {
Text('💰 ' + o.fee).fontSize(10).fontColor(COLORS.brassDeep).fontWeight(FontWeight.Bold)
Text('🗓️ ' + o.days).fontSize(9).fontColor(COLORS.sub)
Text('🧑🔧 ' + o.master).fontSize(9).fontColor(COLORS.sub)
}
.width('100%')
Row({ space: 8 }) {
Button().width(64).height(26).borderRadius(13).backgroundColor(COLORS.card2)
.onClick(() => { this.openEdit(i); })
Text('调整').fontSize(9).fontColor(COLORS.sub).margin({ left: -52 })
Button().width(64).height(26).borderRadius(13).backgroundColor(COLORS.card2)
.onClick(() => { this.openDelOrder(i); })
Text('取消').fontSize(9).fontColor(COLORS.red).margin({ left: -52 })
}
.width('100%')
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
}, (o: OrderItem) => o.id.toString())
}
}
}
工单列表是页面的核心内容,每张工单卡片包含三行信息。第一行是基本信息:左侧是维修图标,中间是表款名称和服务项目/当前进度,右侧是工单状态标签。状态标签使用了一个巧妙的映射——由于 watchStatusColor 函数原本是为藏品状态设计的,工单状态通过一个三元表达式转换为对应的藏品状态值(“进行中"→"修复中”,“待配件"→"洽藏中”,其他→"在展"),从而复用已有的颜色逻辑。
第二行是费用、工期和师傅信息,三个信息项使用 emoji 图标 + 文字的形式横向排列。费用信息使用深黄铜色加粗显示,突出金额信息;工期和师傅信息使用次要的灰色文字。这种主次分明的排版让用户能够快速抓住最关心的信息。
第三行是操作按钮区,包含"调整"和"取消"两个按钮。按钮使用了一个有趣的实现方式:先创建一个 Button 组件作为背景,然后用 Text 组件通过负边距覆盖在按钮上方显示文字。这种实现方式可能是为了精确控制文字的位置和样式,因为直接使用 Button 的文字属性可能在某些情况下不够灵活。
"调整"按钮点击后打开编辑弹框,用户可以修改表款名称和服务项目;"取消"按钮点击后打开删除确认弹框,用户确认后取消工单。两个操作按钮的存在赋予了用户对工单的控制权,体现了以用户为中心的设计理念。
3.24 工坊课程 Tab — 等级标签与课程卡
@Builder
tabCourse() {
Scroll() {
Column({ space: 10 }) {
Text('工坊制表课程').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
ForEach(this.courses, (c: CourseItem, i: number) => {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text(c.level).fontSize(10).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
}
.width(52).height(52).borderRadius(14)
.backgroundColor(c.level === '入门' ? COLORS.green : (c.level === '进阶' ? COLORS.brass : (c.level === '理论' ? COLORS.blue : COLORS.red)))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(c.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('讲师:' + c.teacher).fontSize(8).fontColor(COLORS.sub)
Text('工具:' + c.tool).fontSize(8).fontColor(COLORS.sub)
Row({ space: 8 }) {
Text('💰 ' + c.price).fontSize(10).fontColor(COLORS.brassDeep).fontWeight(FontWeight.Bold)
Text('🎫 ' + c.quota).fontSize(8).fontColor(COLORS.blue)
Text(c.status).fontSize(8).fontColor(watchStatusColor(c.status === '热抢' ? '洽藏中' : '在展'))
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 6 }) {
Button().width(48).height(22).borderRadius(11).backgroundColor(COLORS.card2)
.onClick(() => { this.openDelCourse(i); })
Text('退课').fontSize(8).fontColor(COLORS.red).margin({ top: -18 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
}, (c: CourseItem) => c.id.toString())
}
}
}
tabCourse 是"工坊课程"Tab 的构建函数,展示平台提供的制表培训课程。课程列表的设计有一个非常鲜明的特点:左侧的等级标签采用了大尺寸的彩色方块,非常醒目。
等级标签是课程卡片的视觉焦点。52x52 的方形尺寸、14px 的圆角、居中显示的等级文字,再加上不同等级对应的不同颜色,形成了强烈的视觉识别系统。"入门"对应绿色(轻松入门,生机勃勃),"进阶"对应黄铜色(稳步提升,专业进阶),"理论"对应蓝色(知识学习,理性深邃),"体验"和"高级"对应红色(稀缺珍贵,高级限定)。这种色彩编码系统让用户可以通过颜色快速识别课程的难度等级,不需要阅读文字就能获取关键信息。
中间的课程信息区包含课程名称、讲师、工具要求和价格/名额/状态三行信息。价格使用深黄铜色加粗,名额使用蓝色,状态使用动态颜色(通过 watchStatusColor 函数映射)。三个不同颜色的信息项在一行内紧凑排列,信息密度高但不杂乱。
右侧是"退课"按钮,尺寸较小(48x22),使用浅米色背景加红色文字。按钮文字颜色使用红色暗示这是一个需要谨慎操作的"危险"动作,符合常规的交互设计规范。点击退课按钮会弹出确认对话框,让用户二次确认,防止误操作。
3.25 藏家圈 Tab — 动态列表与发布
@Builder
tabFriends() {
Scroll() {
Column({ space: 10 }) {
Row({ space: 8 }) {
Button().width(110).height(32).borderRadius(16).backgroundColor(COLORS.brassDeep)
.onClick(() => { this.openAdd(); })
Text('📝 发布动态').fontSize(10).fontColor(COLORS.white).margin({ left: -102 })
Column().layoutWeight(1)
Text('按最新排序').fontSize(9).fontColor(COLORS.sub)
}
.width('100%')
tabFriends 是"藏家圈"Tab 的构建函数,这是应用的社区模块。藏家圈是用户生成内容(UGC)的主要阵地,用户可以在这里发布动态、分享收藏心得、与其他藏家互动。
页面顶部是操作栏,左侧是"发布动态"按钮,右侧是排序说明"按最新排序"。发布按钮使用深黄铜色背景加白色文字,是整个页面的主操作按钮(Primary Action),视觉上非常突出。按钮同样采用了"Button 背景 + Text 文字"的实现方式,通过负边距将文字定位到按钮上方。
"按最新排序"的文字虽然看起来像是一个可点击的排序选择器,但实际上只是静态的提示文字。在真实的应用中,这里通常会设计为可点击的下拉菜单,支持按最新、最热等多种方式排序。当前的实现简化了交互,但保留了排序信息的展示。
ForEach(this.friends, (f: WatchFriendItem) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() { Text(f.avatar).fontSize(24) }
.width(44).height(44).borderRadius(12)
.backgroundColor(COLORS.cream)
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(f.nick).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(f.time + ' · ' + f.tag).fontSize(8).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🪪 认证藏家').fontSize(7)
.fontColor(COLORS.brassDeep)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(COLORS.cream)
}
.width('100%')
Text(f.text).fontSize(10).fontColor(COLORS.sub).width('100%').lineHeight(16)
Row({ space: 16 }) {
Text('❤️ ' + f.likes.toString()).fontSize(9).fontColor(COLORS.brass)
Text('💬 ' + f.replies.toString()).fontSize(9).fontColor(COLORS.sub)
Text('⤴ 分享').fontSize(9).fontColor(COLORS.sub)
Column().layoutWeight(1)
Button().width(48).height(20).borderRadius(10).backgroundColor(COLORS.card2)
.onClick(() => { this.openDelCourse(-1); this.editIdx = f.id - 1; this.delTarget = 'friend'; })
Text('删').fontSize(8).fontColor(COLORS.red).margin({ left: -38 })
}
.width('100%')
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
}, (f: WatchFriendItem) => f.id.toString())
}
}
}
动态列表是藏家圈的核心内容。每条动态采用标准的社交平台卡片布局:顶部是用户信息栏(头像、昵称、时间、标签、认证标识),中间是正文内容,底部是互动操作栏(点赞、评论、分享、删除)。
用户信息栏的设计有几个亮点。首先是"认证藏家"标签,使用小尺寸(7px 文字)的奶油色胶囊标签,加上身份证 emoji 图标,传达出用户身份经过认证的信任感。在收藏领域,认证身份对于建立信誉非常重要,因为交易的标的价值较高,用户身份的可信度直接影响交易意愿。
正文内容使用 10px 的灰色文字,行高设为 16px(约 1.6 倍行高),保证了良好的阅读舒适性。正文是动态的核心内容,其字数不做限制,直接完整展示。对于较长的内容,在实际产品中可能会考虑"展开/收起"的交互设计,但在当前实现中全部展示,保持了简洁性。
底部互动栏包含点赞、评论、分享三个标准社交动作,以及一个删除按钮。点赞数使用黄铜色(与点赞图标❤️的红色不同,这里统一使用品牌色),评论和分享使用灰色。删除按钮只对发布者可见(在这个示例中每条动态都有删除按钮,实际应用中应该只显示自己发布的动态的删除按钮)。删除按钮的点击处理逻辑比较特别——它调用了 openDelCourse(-1) 然后手动设置 editIdx 和 delTarget,这是一种代码复用的权宜之计,复用了删除确认弹框的显示逻辑,但删除目标变成了藏家圈动态。
3.26 鉴定台 Tab — 案例列表
@Builder
tabAuth() {
Scroll() {
Column({ space: 10 }) {
Text('本周鉴定台案例').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
ForEach([0, 1, 2, 3, 4], (k: number) => {
Column({ space: 8 }) {
Row({ space: 8 }) {
Text(k % 2 === 0 ? '✅ 真品' : '⚠️ 存疑').fontSize(10)
.fontColor(k % 2 === 0 ? COLORS.green : COLORS.red).fontWeight(FontWeight.Bold)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor(k % 2 === 0 ? '#EDF5EC' : '#F8ECEA')
Column().layoutWeight(1)
Text('鉴定编号 GB-2026-' + (1160 + k * 7).toString()).fontSize(8).fontColor(COLORS.sub)
}
.width('100%')
tabAuth 是"鉴定台"Tab 的构建函数,展示平台的鉴定案例。鉴定服务是钟表收藏电商平台的重要增值服务,通过专业鉴定师对藏品进行真伪鉴别和品质评估,为买家提供信任背书。
每个鉴定案例包含鉴定结果、鉴定编号、鉴定描述、鉴定师、用时和费用等信息。鉴定结果是最醒目的信息,使用大号标签展示:真品使用绿色(文字 + 浅绿背景),存疑使用红色(文字 + 浅红背景)。绿色和红色是全球通用的"通过/不通过"语义色,用户能够瞬间理解鉴定结果。
鉴定编号是案例的唯一标识符,格式为"GB-2026-XXXX",其中 GB 可能是"国检"或"工坊"的缩写,2026 是年份,后面是流水号。编号的展示增加了鉴定服务的正式感和权威性,让用户感觉这是一个规范、专业的服务。
Text(k === 0 ? '百达翡丽 Ref.96:表盘印刷网点与年代特征相符,机芯打磨为原厂鱼鳞纹,序号与档案一致。'
: (k === 1 ? '仿制珐琅怀表:釉面气泡分布异常,边缘露铜为后补,机芯夹板刻字深浅不一,判定为近代仿品。'
: (k === 2 ? '浪琴 13ZN:导柱轮结构与原厂图纸一致,计时按钮为后配,整体评级 B+,可收藏。'
: (k === 3 ? '上海 A581:表壳原装未打磨,秒针为后期更换,机芯状态佳,国产收藏入门佳品。'
: '欧米茄 30T2:表盘翻写痕迹明显,但机芯原装度 95%,建议换原盘后收藏价值更高。'))))
.fontSize(10).fontColor(COLORS.sub).width('100%').lineHeight(16)
Row({ space: 8 }) {
Text('鉴定师:' + (k % 2 === 0 ? '沈先生' : '顾师傅')).fontSize(8).fontColor(COLORS.brassDeep)
Text('用时 ' + (1 + k).toString() + ' 天').fontSize(8).fontColor(COLORS.sub)
Column().layoutWeight(1)
Text('¥' + (280 + k * 60).toString()).fontSize(10).fontColor(COLORS.brassDeep).fontWeight(FontWeight.Bold)
}
.width('100%')
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
}, (k: number) => k.toString())
}
}
}
鉴定描述是案例的核心内容,详细说明了鉴定的依据和结论。五条案例的描述都非常专业,涉及表盘印刷网点、机芯打磨纹理、序号档案核对、导柱轮结构对比等专业鉴定要点。这些专业细节的展示不仅让用户了解鉴定的深度和专业性,也能从中学习到鉴定知识,提升自己的鉴别能力。
鉴定结果分为不同的等级:完全真品(百达翡丽 Ref.96)、仿品(仿制珐琅怀表)、部分原装(浪琴 13ZN 计时按钮后配、上海 A581 秒针更换、欧米茄 30T2 表盘翻写)。这种分级结果比简单的"真/假"二元判断更加细致和实用,因为古董表的收藏价值不仅仅取决于真伪,还与原装度、品相等级等因素密切相关。
案例底部显示鉴定师姓名、用时和费用。鉴定师有两位:沈先生(真品案例)和顾师傅(存疑案例)。费用从 280 元到 520 元不等,随案例复杂度递增。这些信息让用户对鉴定服务的价格和周期有清晰的预期,便于做出是否送鉴的决策。
3.27 主 Tab 页面 — 藏品、工坊、我的
@Builder
pageWatch() {
Scroll() {
Column({ space: 12 }) {
Text('🕰️ 藏品总览').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
ForEach(watchList, (w: WatchItem) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() { Text('⌚').fontSize(24) }
.width(48).height(48).borderRadius(24)
.border({ width: 2, color: COLORS.gold })
.backgroundColor(COLORS.cream)
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text(w.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(w.origin + ' · ' + w.year).fontSize(9).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(w.status).fontSize(9).fontColor(watchStatusColor(w.status))
}
.width('100%')
Row({ space: 6 }) {
Text(w.movement).fontSize(9).fontColor(COLORS.brassDeep)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8).backgroundColor(COLORS.cream)
Text(w.error).fontSize(9).fontColor(errorColor(w.error))
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8).backgroundColor(COLORS.card2)
Text(w.price).fontSize(9).fontColor(COLORS.blue).fontWeight(FontWeight.Bold)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8).backgroundColor(COLORS.card2)
}
.width('100%')
}
.width('100%').padding(14).borderRadius(14)
.backgroundColor(COLORS.card)
}, (w: WatchItem) => w.id.toString())
}
}
}
除了首页的七个内容 Tab,应用还有三个独立的主 Tab 页面:藏品(pageWatch)、工坊(pageShop)、我的(pageMine)。这三个页面是应用的核心功能模块,各自承载不同的业务场景。
藏品总览页(pageWatch)以列表形式展示全部藏品,每张卡片采用上下两行布局:上方是手表图标 + 名称产地 + 状态的横向布局,下方是机芯、日差、价格三个标签的横向排列。与精选页的大卡列表(左中右三栏)和钟表藏品 Tab 的表格布局(四列)不同,这里的布局更加舒展,信息排列也更有层次感。
标签行的设计很有特色,三个标签分别使用不同的背景色和文字色。机芯标签使用奶油色背景 + 深黄铜色文字,日差标签使用浅米色背景 + 动态颜色,价格标签使用浅米色背景 + 蓝色粗体。三个标签使用相同的圆角胶囊形状,但通过颜色区分了信息类型,视觉上既统一又有变化。
@Builder
pageShop() {
Scroll() {
Column({ space: 12 }) {
Text('🔧 修复服务价目').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
ForEach(['机芯完全保养', '三问报时修复', '珐琅面微修', '表壳翻新抛光', '古董零件定制', '防水深度检测'], (s: string, i: number) => {
Row({ space: 10 }) {
Text('⚙️').fontSize(18)
Column({ space: 3 }) {
Text(s).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(['含拆洗、加油、调校', '音簧调音与打磨', '补珐琅与烧制', '保留锋线的轻度处理', '摆轴/柄轴车削', '干湿双法 100m 内'][i]).fontSize(8).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 2 }) {
Text('¥' + (860 + i * 940).toString()).fontSize(12).fontColor(COLORS.brassDeep).fontWeight(FontWeight.Bold)
Text(['7天', '45天', '60天', '10天', '21天', '3天'][i] + ' 起').fontSize(8).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12)
.backgroundColor(COLORS.card)
}, (s: string) => s)
}
}
}
工坊页(pageShop)展示修复服务价目表,共有六项服务:机芯完全保养、三问报时修复、珐琅面微修、表壳翻新抛光、古董零件定制、防水深度检测。每项服务使用一行卡片展示,包含齿轮图标、服务名称和说明、价格和工期。
服务价格采用递增的方式计算(860 + i × 940),从最便宜的 860 元到最贵的 5560 元,反映了不同服务项目的复杂度差异。工期也各不相同,从 3 天(防水检测)到 60 天(珐琅修复),工期与价格大致正相关——越复杂的修复项目越贵也越耗时。
服务说明文字使用数组索引的方式获取,每项服务都有一句简短的描述,概括服务内容。例如"机芯完全保养"的说明是"含拆洗、加油、调校",“三问报时修复"的说明是"音簧调音与打磨”。这些说明文字帮助用户快速了解服务的具体内容,做出选择。
@Builder
pageMine() {
Scroll() {
Column({ space: 12 }) {
Row({ space: 12 }) {
Column() { Text('🧑🔬').fontSize(30) }
.width(64).height(64).borderRadius(18)
.backgroundColor(COLORS.cream)
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text('守时人·阿澄').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('入门制表学员 · 修表 8 枚').fontSize(9).fontColor(COLORS.sub)
Text('工龄 1 年 · 已完成 2 门课程').fontSize(8).fontColor(COLORS.brassDeep)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%').padding(16).borderRadius(16)
.backgroundColor(COLORS.card)
我的页(pageMine)是用户个人中心,顶部是用户信息卡片,展示头像、昵称、身份和统计数据。用户昵称"守时人·阿澄"和身份"入门制表学员"都很有特色,与钟表收藏的主题紧密结合。"工龄 1 年 · 已完成 2 门课程"的信息展示了用户的成长轨迹,带有游戏化的元素,能够激励用户继续学习和参与。
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('8').fontSize(18).fontColor(COLORS.brassDeep).fontWeight(FontWeight.Bold)
Text('送修藏品').fontSize(8).fontColor(COLORS.sub)
}
.layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
// 更多统计卡片...
}
ForEach(['我的工单', '课程记录', '藏品档案', '鉴定证书', '常用师傅', '设置'], (s: string) => {
Row({ space: 10 }) {
Text('›').fontSize(16).fontColor(COLORS.brass)
Text(s).fontSize(11).fontColor(COLORS.title)
Column().layoutWeight(1)
Text('>').fontSize(12).fontColor(COLORS.sub)
}
.width('100%').padding(14).borderRadius(12)
.backgroundColor(COLORS.card)
}, (s: string) => s)
}
}
}
用户信息下方是四个统计数据卡片:送修藏品(8 件,黄铜色)、在学课程(2 门,蓝色)、鉴定记录(5 次,绿色)、圈内积分(128 分,红色)。四个数据使用不同的颜色编码,形成鲜明的视觉区分。再下方是功能入口列表,包含我的工单、课程记录、藏品档案、鉴定证书、常用师傅、设置六项。每个列表项使用经典的"图标 + 文字 + 箭头"的设计模式,简洁清晰。
3.28 底部导航栏与主构建函数
@Builder
bottomBar() {
Row({ space: 0 }) {
ForEach(NAV_LIST, (ni: NavItem, i: number) => {
Column({ space: 3 }) {
Text(ni.icon).fontSize(18)
Text(ni.label)
.fontSize(9)
.fontColor(this.mainTab === i ? COLORS.brassDeep : COLORS.sub)
.fontWeight(this.mainTab === i ? FontWeight.Bold : FontWeight.Normal)
}
.layoutWeight(1)
.padding({ top: 6, bottom: 6 })
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.mainTab = i; this.subTab = 0; })
}, (ni: NavItem) => ni.label)
}
.width('100%')
.backgroundColor(COLORS.white)
.border({ width: 1, color: COLORS.line })
}
bottomBar 是底部导航栏的构建函数,是应用全局导航的核心组件。底部导航栏使用 Row 横向排列四个 Tab 项,每个 Tab 项平均分配宽度(layoutWeight(1)),确保在各种屏幕尺寸下都能均匀分布。
每个 Tab 项采用图标 + 文字的上下布局。图标使用 18px 的 emoji,文字使用 9px 的字号。选中态的文字使用深黄铜色加粗,未选中态使用灰色常规字重。颜色和字重的双重变化确保了选中状态的显著性,用户可以清晰地识别当前所在的页面。
点击 Tab 时会同时更新 mainTab 和 subTab 两个状态变量。mainTab 切换到对应的主 Tab,subTab 重置为 0(第一个子 Tab)。这种设计确保了用户切换主 Tab 后,回到首页时总是显示第一个子 Tab(精选),避免了子 Tab 状态残留导致的导航混乱。
底部导航栏使用白色背景,顶部有一条 1px 的浅米色分割线,与内容区域形成视觉分隔。这种设计是移动端应用的标准模式,用户已经形成了使用习惯。
build() {
Stack() {
Column() {
this.header()
if (this.mainTab === 0) {
Column() {
this.subNav()
if (this.subTab === 0) {
this.tabPick()
} else if (this.subTab === 1) {
this.tabWatch()
} else if (this.subTab === 2) {
this.tabAcc()
} else if (this.subTab === 3) {
this.tabOrder()
} else if (this.subTab === 4) {
this.tabCourse()
} else if (this.subTab === 5) {
this.tabFriends()
} else {
this.tabAuth()
}
}
.layoutWeight(1)
.width('100%')
.alignItems(HorizontalAlign.Center)
.backgroundColor(COLORS.bg)
} else if (this.mainTab === 1) {
this.pageWatch()
} else if (this.mainTab === 2) {
this.pageShop()
} else {
this.pageMine()
}
this.bottomBar()
}
.width('100%')
.height('100%')
build() 方法是组件的核心渲染函数,定义了整个页面的 UI 结构。最外层使用 Stack 布局,因为需要叠加多层内容:主内容层、特效层和弹框层。Stack 布局的特点是子元素按顺序堆叠,后渲染的元素在上方。
主内容层是一个 Column,从上到下依次排列头部、内容区和底部导航。内容区的高度通过 layoutWeight(1) 自适应填充剩余空间,确保头部和底部导航始终固定在屏幕的顶部和底部,中间内容区可以滚动。
内容区的显示逻辑通过 if-else if 条件语句实现。当 mainTab 为 0(首页)时,显示子 Tab 导航和对应的子 Tab 内容;当 mainTab 为 1、2、3 时,分别显示藏品、工坊、我的三个独立页面。子 Tab 的切换同样使用 if-else if 条件链,根据 subTab 的值显示不同的内容 Builder。
这种条件渲染的方式是声明式 UI 的典型模式。与命令式 UI 中手动控制元素的显示/隐藏不同,声明式 UI 通过状态变量直接描述"什么状态下显示什么内容",框架负责具体的渲染控制。代码逻辑清晰直观,可读性很强。
// 特效层仅覆盖内容区(头部之下)
Column() {
this.fxLayer()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
.hitTestBehavior(HitTestMode.None)
// 弹框层
if (this.addModal) {
Column() {
this.modalOverlay(() => { this.addModal = false; })
Column() {
this.addModalBody()
}
.constraintSize({ maxHeight: '80%' })
.zIndex(999)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// 编辑弹框和删除弹框类似...
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
特效层叠加在主内容层之上,通过 justifyContent(FlexAlign.End) 定位到底部,只覆盖内容区域而不遮挡头部。hitTestBehavior(HitTestMode.None) 是一个关键属性,它让特效层不响应触摸事件,所有的触摸操作都会穿透特效层到达下方的内容层。这确保了齿轮、摆锤等装饰性特效不会影响用户的正常交互操作。
弹框层是最顶层的元素,三种弹框(发布动态、编辑工单、删除确认)分别通过 if 条件控制显示。每个弹框都包含一个全屏遮罩层和一个居中的弹框卡片。遮罩层点击时会关闭弹框,提供了便捷的取消方式。弹框卡片使用 constraintSize 限制最大高度(分别为 80%、70%、60%),防止内容过多时超出屏幕。zIndex(999) 确保弹框层级最高,不会被其他元素遮挡。
整个 build() 函数的三层结构(主内容层 → 特效层 → 弹框层)体现了非常清晰的层级设计思路。每一层都有明确的职责,层与层之间通过 Stack 堆叠和 hitTestBehavior 协调交互关系。这种结构既保证了视觉效果的丰富性,又确保了交互的正确性。
3.29 弹框组件设计
@Builder
modalOverlay(onClose: () => void) {
Column()
.width('100%')
.height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
modalOverlay 是一个通用的遮罩层构建函数,接收一个 onClose 回调函数作为参数。当用户点击遮罩层时,会调用这个回调函数来关闭弹框。将遮罩层抽离为独立的 @Builder 函数,实现了代码复用——三种弹框都使用同一个遮罩层,避免了重复代码。
遮罩层使用半透明的深色背景(COLORS.mask,即 #2B2113B3,约 70% 不透明度的深棕色),既能有效地将用户的注意力引导到弹框内容上,又能让用户感知到背后页面的存在,保持场景的连续性。点击遮罩关闭弹框是移动端的标准交互模式,用户已经形成了肌肉记忆。
@Builder
addModalBody() {
Column({ space: 12 }) {
Text('发布藏家动态').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
TextInput({ placeholder: '昵称(可留空)', text: this.addNick })
.fontSize(11).height(38).borderRadius(10)
.backgroundColor(COLORS.card2).fontColor(COLORS.title)
.onChange((v: string) => { this.addNick = v; })
TextInput({ placeholder: '分享你的入藏喜悦或修复心得…', text: this.addText })
.fontSize(11).height(60).borderRadius(10)
.backgroundColor(COLORS.card2).fontColor(COLORS.title)
.onChange((v: string) => { this.addText = v; })
Row({ space: 10 }) {
Button().layoutWeight(1).height(36).borderRadius(18).backgroundColor(COLORS.card2)
.onClick(() => { this.addModal = false; })
Text('取消').fontSize(11).fontColor(COLORS.sub).margin({ left: -66 })
Button().layoutWeight(1).height(36).borderRadius(18).backgroundColor(COLORS.brassDeep)
.onClick(() => { this.doAdd(); })
Text('发布').fontSize(11).fontColor(COLORS.white).fontWeight(FontWeight.Bold).margin({ left: -66 })
}
.width('100%')
}
.width('86%')
.padding(18)
.borderRadius(16)
.backgroundColor(COLORS.white)
}
发布动态弹框(addModalBody)是三种弹框中内容最多的一个,包含标题、昵称输入框、正文输入框和底部双按钮。弹框宽度为屏幕的 86%,使用白色圆角卡片样式,与应用整体的设计语言保持一致。
两个 TextInput 输入框实现了双向数据绑定。text 属性绑定到对应的状态变量(addNick、addText),onChange 回调在输入内容变化时更新状态变量。这种双向绑定确保了输入框的显示内容与状态变量始终保持同步,用户输入的内容可以即时被业务逻辑读取和处理。
底部双按钮使用"取消 + 确认"的经典组合。取消按钮使用浅米色背景加灰色文字,确认(发布)按钮使用深黄铜色背景加白色粗体文字。主按钮使用品牌色填充,视觉上更加突出,引导用户点击。两个按钮平分宽度(各占 layoutWeight(1)),大小一致,形成平衡的视觉效果。
@Builder
delModalBody() {
Column({ space: 12 }) {
Text('确认操作').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(this.delTarget === 'order' ? '取消工单后已拆检的机芯将原样装回,收取 30% 检测费,确认吗?' : '退课后已排工位将释放,开课前 48 小时内退课收取 20% 材料费,确认吗?')
.fontSize(10).fontColor(COLORS.sub).textAlign(TextAlign.Center)
Row({ space: 10 }) {
Button().layoutWeight(1).height(36).borderRadius(18).backgroundColor(COLORS.card2)
.onClick(() => { this.delModal = false; })
Text('再想想').fontSize(11).fontColor(COLORS.sub).margin({ left: -82 })
Button().layoutWeight(1).height(36).borderRadius(18).backgroundColor(COLORS.red)
.onClick(() => { this.doDel(); })
Text('确认').fontSize(11).fontColor(COLORS.white).fontWeight(FontWeight.Bold).margin({ left: -58 })
}
.width('100%')
}
.width('86%')
.padding(18)
.borderRadius(16)
.backgroundColor(COLORS.white)
}
删除确认弹框(delModalBody)的设计体现了对用户操作的谨慎态度。确认按钮使用红色背景,明确传达这是一个"危险"操作,需要用户慎重考虑。取消按钮的文案不是简单的"取消",而是"再想想",这种更具人情味的文案能够在一定程度上降低用户的误操作率,也体现了产品的温度。
提示文案根据删除目标的不同而变化:取消工单的提示是"取消工单后已拆检的机芯将原样装回,收取 30% 检测费,确认吗?“;退课的提示是"退课后已排工位将释放,开课前 48 小时内退课收取 20% 材料费,确认吗?”。两种不同的提示文案都明确说明了取消的后果和费用规则,让用户在做出决定前有充分的信息预期,避免事后产生纠纷。
四、Mermaid 流程图
五、技术对比表格
| 技术维度 | 实现方式 | 设计特点 | 性能考量 |
|---|---|---|---|
| 状态管理 | @State + @Observed 装饰器 | 组件级状态集中管理,数据类细粒度追踪 | @Observed 实现属性级更新,避免全组件重渲染 |
| UI 构建 | @Builder 函数 + 声明式组件 | 模块化拆分,每个 Builder 职责单一 | 条件渲染按需构建,减少无效节点 |
| 列表渲染 | ForEach + 键生成函数 | 数据驱动,自动 diff 更新 | 稳定的 key 确保元素复用,减少 DOM 重建 |
| 颜色体系 | ColorPalette 接口 + 全局常量 | 语义化命名,17 色完整色板 | 集中管理便于主题切换,统一引用减少冗余 |
| 动画实现 | setInterval + 状态驱动 | 统一 tick 驱动多元素动画 | 600ms 低频刷新,CPU 占用低 |
| 特效层 | Stack 叠加 + hitTestBehavior=None | 视觉层与交互层分离 | 纯文本 emoji 渲染,无图片资源开销 |
| 弹框管理 | if 条件渲染 + zIndex 层级 | 三种弹框独立控制,共用遮罩 | 条件渲染按需挂载,不常驻内存 |
| 导航架构 | 4 主 Tab + 7 子 Tab 双层 | 主 Tab 底部导航 + 子 Tab 横滚 | 条件切换直接替换内容区,无路由开销 |
| 数据模型 | @Observed class + 构造函数 | 全属性初始化,类型安全 | 引用追踪 + 属性监听,精确到字段 |
| 图表实现 | 纯组件构建柱状图/进度条 | 轻量无依赖,样式高度可控 | 无第三方库,包体积小 |
| 表单处理 | TextInput + onChange 双向绑定 | 状态缓存,确认后提交 | 输入即时同步,无延迟反馈 |
| 图片资源 | 全 emoji 图标方案 | 零图片资源,全平台一致 | 无图片加载开销,首屏快 |
| 布局系统 | Column/Row/Stack 组合 | Flex 布局 + 百分比 + 固定值混合 | layoutWeight 自适应,性能优 |
| 生命周期 | aboutToAppear / aboutToDisappear | 成对出现,资源自动管理 | 定时器及时清理,防内存泄漏 |
| 工具函数 | 顶层纯函数 | 无副作用,可独立测试 | 计算逻辑与 UI 分离,易于优化 |
| 文字排版 | 多级字号 + 行高控制 | 标题/正文/说明三级字号体系 | 行高 1.6 倍,保证可读性 |
六、详细总结
6.1 架构设计总结
该钟表收藏电商应用采用了单页面组件的架构设计模式,将所有功能模块集中在一个 Page 组件内,通过状态变量和条件渲染控制不同模块的显示与隐藏。这种架构在中小型应用中具有开发效率高、状态管理集中、数据流清晰等优势。整个应用的结构层次分明:最外层是 Stack 三层布局(主内容层、特效层、弹框层),主内容层又分为头部、内容区、底部导航三段式结构,内容区内部通过 Tab 切换实现多模块展示。
UI 构建的模块化是该应用架构的一大亮点。应用大量使用 @Builder 装饰器将不同功能区域的 UI 代码封装为独立的构建函数,每个函数负责一个明确的 UI 区域。这种方式虽然不如独立组件那样具有完全的封装性和复用性,但在单页面应用的场景下,它既保持了代码的模块化和可维护性,又避免了组件间状态传递的复杂度。从代码组织的角度来看,这种 @Builder 化的拆分方式是一种轻量级的模块化方案,非常适合当前规模的应用。
数据层与视图层的分离也做得比较好。数据模型类(WatchItem、OrderItem 等)定义在组件外部,使用 @Observed 装饰器支持响应式更新;工具函数(errorColor、watchStatusColor 等)也是顶层纯函数,不依赖组件状态。这种分离使得数据逻辑和视图逻辑各司其职,代码的可读性和可测试性都比较好。
6.2 状态管理总结
应用的状态管理采用了 @State + @Observed 的双层机制。@State 用于管理组件级的基本类型状态(如 Tab 索引、弹框开关、表单输入等),@Observed 用于装饰数据模型类,实现对象属性级别的细粒度追踪。这种组合使用的方式充分利用了 ArkTS 状态管理的特性,既保证了状态变化能够触发 UI 更新,又实现了更新的精确性——只有真正变化的属性才会触发对应 UI 的重渲染。
弹框状态的设计是状态管理中的一个典型场景。应用使用三个独立的布尔变量(addModal、editModal、delModal)分别控制三种弹框的显示,并在打开某个弹框时显式关闭其他弹框。这种设计虽然在状态数量上略多,但逻辑清晰、直观易懂。相比使用一个枚举值来表示当前弹框类型的方案,布尔值的方式在条件渲染时更加直接,不需要额外的比较判断。
列表数据的状态管理也有值得注意的地方。全局常量数据(watchList 等)与组件状态数据(this.orders、this.courses、this.friends)分开存储,前者用于只读展示,后者支持用户操作修改。这种区分确保了基础数据的纯净性,同时也让可变数据的管理更加集中。在实际生产环境中,可以进一步引入状态管理库或服务层来统一管理数据,实现多组件间的数据共享和同步。
6.3 性能优化总结
应用在性能优化方面有几个值得称道的设计。首先是特效动画的实现方式。使用 setInterval 统一驱动所有动画元素,而不是为每个元素单独设置定时器,减少了定时器的数量和回调次数。600ms 的刷新频率既保证了视觉效果,又不会因为过于频繁的重绘而消耗过多的 CPU 资源。此外,动画元素全部使用文本 emoji 而非图片或 Canvas 绘制,渲染开销非常低。
其次是 @Observed 装饰器的使用。通过对数据模型类进行细粒度的属性追踪,框架可以精确地知道哪个属性发生了变化,从而只更新依赖该属性的 UI 部分,而不是整个列表或整个组件都重新渲染。在列表数据较多的场景下,这种细粒度更新带来的性能提升是非常显著的。ForEach 的键生成函数也遵循了最佳实践,使用稳定的 ID 而非索引作为键值,确保了列表项的正确复用。
第三是条件渲染的合理运用。应用大量使用 if 条件语句来控制不同 Tab 内容的显示,这意味着未选中的 Tab 内容不会被构建和渲染,减少了初始渲染的工作量和内存占用。相比一次性渲染所有 Tab 再通过显隐控制的方式,条件渲染在内存占用和初始加载速度上都更有优势,尤其是在 Tab 数量较多、内容较复杂的情况下。
最后,hitTestBehavior(HitTestMode.None) 的使用确保了特效层不会拦截触摸事件,避免了因为视觉特效而影响交互性能的问题。这种细节处理体现了对用户体验的深入理解——视觉效果是加分项,但绝不能以牺牲交互流畅性为代价。
6.4 交互设计总结
应用的交互设计充分体现了移动端的特点和钟表收藏领域的专业特色。导航交互采用了底部 Tab + 内容子 Tab 的双层导航架构,这种架构在信息架构较复杂的应用中非常实用,既保证了主要功能的快速可达,又通过子 Tab 对内容进行了细粒度的分类。子 Tab 使用横向滚动而非多行排列,节省了垂直方向的屏幕空间,让更多空间留给内容展示。
弹框交互的设计遵循了移动端的最佳实践。半透明遮罩 + 居中卡片的模式用户学习成本低,点击遮罩关闭的操作方式也很直观。表单弹框采用了"输入即缓存、确认才提交"的模式,用户输入的内容会实时同步到状态变量中,但只有点击确认按钮后才会真正修改业务数据。这种设计既保证了输入的即时反馈,又防止了误操作对数据的破坏。删除确认弹框则使用了红色确认按钮和详细的后果说明,强化了操作的风险性,降低了误删的概率。
列表交互方面,不同页面采用了不同的列表样式(大卡列表、表格列表、双行标签列表等),以适应不同场景下的信息密度需求。精选页的大卡列表适合探索浏览,钟表藏品页的表格适合对比筛选,藏品总览页的双行标签适合详细查看。多样化的列表形式让用户在不同场景下都能获得最佳的信息获取效率。
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
6.5 总结:

从架构角度看,应用的可扩展性还有较大的提升空间。目前所有功能都集中在一个组件内,随着功能的增加,组件的体积会越来越大,维护成本也会随之上升。未来可以考虑将各个 Tab 页面拆分为独立的子组件,通过 @Prop 和 @Link 进行父子组件间的状态传递,或者引入全局状态管理方案来实现跨组件的数据共享。
数据层的可扩展性同样需要关注。目前数据都是硬编码在代码中的常量,未来接入真实 API 后,需要引入网络请求层、数据缓存层等基础设施。可以考虑将数据访问逻辑封装为 Service 层,UI 组件只通过 Service 接口获取数据,这样既能解耦数据获取与 UI 展示,也便于进行单元测试和数据模拟。
视觉层面的可扩展性做得相对较好。统一的颜色体系、一致的卡片样式、标准化的按钮和标签设计,都为后续功能的扩展奠定了良好的设计基础。新增功能模块时,只需遵循已有的设计规范和组件模式,就能快速构建出风格一致的界面。颜色体系的接口化定义(ColorPalette)也为未来实现主题切换(如深色模式、节日主题等)提供了便利。
特效系统的可扩展性也比较强。目前的特效层采用了 Stack + 绝对定位的方式,新增特效元素只需在 fxLayer 中添加新的 ForEach 或单个元素即可。动画驱动统一由 tick 和 glow 两个状态变量负责,新增的动画元素只要基于这两个状态计算视觉属性,就能自动融入现有的动效系统。
更多推荐



所有评论(0)