基于 HarmonyOS API 24 的 ArkTS 声明式开发实战:球鞋引力交易平台从零到一的完整架构剖析与 HarmonyOS ArkTS API 24 深度解析,基于 HarmonyOS 6.
引言
在当今移动互联网高速发展的时代背景下,跨平台应用开发框架层出不穷,而华为推出的 HarmonyOS 操作系统凭借其分布式架构和声明式 UI 编程范式,正在为开发者提供一个全新的技术选择。HarmonyOS 6.1.1 作为华为鸿蒙操作系统的重要版本迭代,不仅在前代基础上进一步优化了系统底层的渲染性能和内存管理机制,还为开发者带来了更加丰富完善的 ArkTS 语言特性和 ArkUI 声明式组件库。基于 HarmonyOS API 24 的应用开发能力,开发者可以充分利用系统级的能力来构建出具有原生体验的高品质移动应用,这些应用不仅在性能上能够媲美原生开发,更在开发效率和代码可维护性方面展现出显著优势。本文将以一个完整的球鞋交易平台页面为切入点,深入剖析 HarmonyOS ArkTS API 24 下的声明式 UI 编程实践,从接口定义、数据建模、纯函数架构到组件化构建和弹框交互系统,全面解读每一个技术决策背后的设计思路。
球鞋交易作为一个垂直且高频的电商领域,具有其独特的业务特征和技术挑战。它不仅需要处理大量的商品数据展示,还需要实时更新价格走势图表、管理用户收藏与交易记录,同时提供专业的鉴定服务流程。这样一个复杂的业务场景,恰恰能够全面检验 HarmonyOS ArkTS API 24 在数据驱动渲染、Canvas 绘图能力、定时器动画管理、多弹框状态控制以及声明式布局系统等方面的综合表现。通过本文的逐行代码分析,读者将深入理解如何运用 ArkTS 的 @Entry、@Component、@State、@Builder 等核心装饰器来组织一个具有生产级复杂度的单文件页面应用。每一个技术点的展开都将结合实际业务需求,说明为何选择某种实现方式而非另一种,以及不同方案在性能、可读性和可维护性之间的权衡考量。
声明式 UI 编程范式的核心思想是"状态驱动视图",即开发者只需要描述界面在任何给定状态下的样子,而由框架负责当状态变化时高效地更新 DOM 树。HarmonyOS ArkTS API 24 在这一范式上做了大量优化工作,包括但不限于细粒度的状态依赖追踪、差分更新算法的改进以及对 ForEach 列表渲染的性能优化。在本文所分析的球鞋交易平台中,我们可以清晰地看到这一范式如何应用于实际场景:当用户切换 Tab 时,currentTab 状态的变化会自动触发对应内容区域的重新渲染;当倒计时每秒递减时,flashSec 状态的更新会精确地反映到倒计时显示组件上;当漂浮表情每 60 毫秒更新位置时,floatEmojis 数组的变化会驱动整个浮动图层的流畅动画。这些场景共同构成了对声明式 UI 框架的全面检验。
在架构设计层面,本文所分析的平台采用了"数据层—纯函数层—组件层"的三层分离架构。数据层由一组 TypeScript 接口定义和常量数据构成,为整个页面提供类型安全的数据模型和初始状态;纯函数层包含所有不依赖组件状态的逻辑函数,如价格格式化、趋势颜色计算、图表绘制等,这些函数具有高度的复用性和可测试性;组件层则由 @Entry 修饰的主页面组件及其内部通过 @Builder 定义的一系列构建器函数组成,负责将数据和函数转化为可视化的 UI 结构。这种分层设计不仅使代码职责清晰,更在团队协作和后续维护中展现出巨大价值——数据结构变更不会波及 UI 逻辑,UI 调整也无需修改纯函数实现。基于 HarmonyOS API 24 的开发实践证明,这种架构在应对需求变更和功能扩展时具有极高的韧性。
此外,本文还将重点关注 HarmonyOS ArkTS API 24 中 Canvas 2D 绘图能力在金融级数据可视化场景中的应用。球鞋交易平台的行情页面需要展示价格走势折线图和成交量柱状图,这两种图表的绘制涉及到坐标系映射、刻度网格绘制、数据点连线、填充渲染以及文本标注等多个步骤。通过分析 drawLineChart 和 drawBarChart 两个纯函数的实现,读者将理解如何在不依赖第三方图表库的情况下,利用 ArkTS 提供的 CanvasRenderingContext2D 和 RenderingContextSettings 来实现自定义图表绘制。这不仅在包体积控制上具有优势,更在绘制性能和定制灵活性方面提供了更高的上限。基于 HarmonyOS 6.1.1 系统的 Canvas 加速渲染能力,这些自定义图表能够在保证帧率的同时呈现精细的视觉效果。
一、系统整体架构与接口设计
1.1 项目架构总览
在深入每一行代码之前,我们首先从宏观层面理解整个球鞋引力平台的架构设计。该平台采用了 HarmonyOS ArkTS API 24 所推崇的单文件页面架构,即将接口定义、数据常量、纯函数逻辑和 UI 组件全部组织在一个源文件中。这种架构选择在中小型项目中具有显著优势:它消除了跨文件依赖管理的复杂度,使开发者能够在一个编辑器视图中纵览整个页面的全貌,同时也便于代码审查和快速迭代。
@State 变 ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'
从上述架构图可以清晰看到,整个系统从下至上分为五个层次。最底层的接口定义层为整个系统提供了类型安全的契约,确保数据在各层之间传递时不会出现类型不匹配的问题。数据常量层则模拟了后端 API 返回的真实数据,使前端开发能够在不依赖后端服务的情况下独立推进。纯函数层是整个系统的业务逻辑核心,它不持有任何状态,仅负责接收输入并返回确定的输出,这种设计使得这些函数可以独立于 UI 进行单元测试。组件状态层和 Builder 组件层共同构成了视图层,它们通过 @State 装饰器建立状态与视图之间的绑定关系,最终在 build 方法中组装成完整的页面。
1.2 ColorPalette 接口:色彩系统的类型契约
interface ColorPalette {
bg: string
card: string
cardLight: string
primary: string
accent: string
gold: string
textPrimary: string
textSecondary: string
textDim: string
border: string
success: string
danger: string
}

ColorPalette 接口定义了整个应用的色彩系统类型契约,这是整个设计系统中最基础的一环。在 HarmonyOS ArkTS API 24 中,接口不仅用于约束对象的结构,更在编译期提供了类型安全保障,使得任何使用 ColorPalette 类型的变量都必须包含全部十二个颜色字段,任何缺失都会在编译阶段被立即捕获。
这里定义了十二个颜色字段,每一个都有其明确的语义用途。bg 代表页面背景色,card 代表卡片背景色,cardLight 是卡片的亮色变体,这三者构成了页面层级的基础。primary 是主文本颜色,accent 是强调色(用于按钮、激活态等交互元素),gold 是金色(用于徽章、特殊标注),这三者定义了视觉焦点的色彩语言。textPrimary、textSecondary、textDim 构成文本颜色的三级层次体系,分别对应主要文本、次要文本和弱化文本,这种分级设计使信息层级在视觉上清晰可辨。border 定义边框颜色,success 和 danger 分别表示成功和危险状态,用于趋势指示和状态标签等场景。
通过将颜色定义抽象为接口而非直接使用字符串字面量,整个应用获得了一个关键优势:色彩变更的集中管理。如果未来需要支持浅色主题或自定义主题,只需提供不同的 ColorPalette 实现即可,而不需要修改任何组件代码。这种设计在 HarmonyOS ArkTS API 24 的声明式 UI 框架中尤为重要,因为 UI 组件通过属性链式调用设置颜色(如 .fontColor(COLORS.textPrimary)),如果颜色散落在各处,主题切换将变成一场灾难。
1.3 Sneaker 接口:商品数据模型
interface Sneaker {
id: number
name: string
brand: string
series: string
price: number
originalPrice: number
retailPrice: number
emoji: string
color: string
colorDeep: string
trend: string
changePercent: number
hot: boolean
releaseDate: string
tags: string[]
}
Sneaker 接口定义了球鞋商品的数据模型,这是整个交易平台最核心的数据结构。在这个接口中,我们可以看到商品信息被分解为几个语义维度:基本标识(id、name、brand、series)、价格信息(price、originalPrice、retailPrice)、视觉表现(emoji、color、colorDeep)、市场动态(trend、changePercent、hot)以及元数据(releaseDate、tags)。
特别值得分析的是价格信息的三层设计。price 表示当前市场价,originalPrice 表示原始市场价(用于展示划线价效果),retailPrice 表示官方发售价。这三层价格信息共同构成了球鞋交易的核心叙事:用户可以直观看到当前市价与发售价之间的差距,从而判断溢价空间;同时通过当前价与原价的对比,感知价格变动趋势。这种设计在球鞋交易领域非常普遍,但在技术实现上需要对数据模型有清晰的理解,确保三个价格字段在 UI 中被正确地展示在不同的位置和语义上下文中。
color 和 colorDeep 字段的设计体现了视觉一致性的考量。color 用于收藏卡片中球鞋展示区域的背景色,而 colorDeep 则是一个更深的变体,用于热卖列表中球鞋图标的背景色。这种"同色不同深度"的设计使同一球鞋在不同场景下能够产生不同的视觉强度——在卡片中需要较亮的背景以突出球鞋本身,而在列表图标中需要更深的背景以与卡片背景产生层次感。emoji 字段则是巧妙地利用了 Unicode 表情符号来替代真实图片资源,这在原型设计和演示阶段极大地简化了资源管理。
1.4 AuthRecord 接口:鉴定记录模型
interface AuthRecord {
id: number
sneakerName: string
brand: string
submitDate: string
status: string
channel: string
price: number
result: string
}

AuthRecord 接口定义了球鞋鉴定记录的数据模型。在球鞋交易领域,鉴定是保障交易安全的核心环节——由于市场上存在大量仿品,平台需要为每笔交易提供专业的鉴定服务。这个接口的字段设计紧紧围绕鉴定流程的信息需求展开。
status 字段是一个字符串枚举,在实际使用中取值为"通过"、“待审”、“存疑"或"不通过”,这四种状态覆盖了鉴定的完整生命周期。值得注意的是,这里选择使用字符串而非数字枚举,主要考量是可读性——在数据源和 UI 渲染中都能直接看到语义化的状态值,便于调试和维护。channel 字段记录购买渠道,这与后续的渠道筛选和数据分析功能相关。result 字段是鉴定结果的详细描述,它提供了超出 status 之外的具体信息,如"正品 · 五项全对"或"中底胶水异常",这些信息对于用户理解鉴定结论至关重要。
1.5 CollectionItem 接口:收藏数据模型
interface CollectionItem {
id: number
name: string
brand: string
size: number
acquiredPrice: number
currentPrice: number
emoji: string
color: string
profit: number
profitPercent: number
liked: boolean
}

CollectionItem 接口定义了用户收藏的球鞋数据模型。与 Sneaker 接口相比,它增加了几个关键的财务字段:size(尺码)、acquiredPrice(入手价)、currentPrice(当前估值)、profit(盈亏额)和 profitPercent(盈亏百分比)。这些字段共同构成了用户的"投资组合"视图,使用户能够一目了然地看到自己收藏的球鞋在当前市场上的价值变化。
liked 字段是一个布尔值,表示用户是否对该收藏项进行了"点赞"标记。在 UI 层面,这个字段控制着收藏卡片右上角显示的是红心还是空心。虽然从业务角度看,"已收藏"的物品默认都应该 liked 为 true,但保留这个字段为未来可能的"收藏夹分组"或"优先级标记"功能预留了扩展空间。这种"看似冗余但预留扩展"的设计思路在大型项目的长期演进中非常有价值。
profit 和 profitPercent 的设计体现了数据冗余与计算性能的权衡。理论上,这两个值可以通过 currentPrice - acquiredPrice 和 ((currentPrice - acquiredPrice) / acquiredPrice) * 100 实时计算得出。但在实际应用中,将这些值预计算并存储在数据中,可以避免在列表渲染时为每个项目重复执行计算逻辑,这在收藏列表较长时能够显著提升渲染性能。基于 HarmonyOS API 24 的渲染优化机制,预计算的数值字段可以直接绑定到 Text 组件上,无需额外的计算函数调用开销。
1.6 RankItem 接口:排行榜数据模型
interface RankItem {
id: number
name: string
avatar: string
tradeCount: number
volume: number
profit: number
badge: string
}
RankItem 接口定义了交易达人排行榜的数据模型。排行榜是社交化交易平台的重要功能,它通过游戏化机制激励用户活跃交易。这个接口的字段设计简洁而精准:name 和 avatar 用于用户标识(avatar 使用 emoji 而非图片 URL,与商品数据保持一致的设计风格),tradeCount 是交易笔数,volume 是交易总额,profit 是盈利总额,badge 是用户徽章标签。
值得注意的是,volume 和 profit 字段的数值范围可能达到数十万甚至上百万级别,这在前端展示时需要进行格式化处理。后面我们会看到 formatVolume 函数专门处理大数字的"万"单位转换,这正是针对这一数据特征设计的。
1.7 图表数据与辅助接口
interface PricePoint {
day: string
price: number
}
interface VolumePoint {
day: string
volume: number
}
PricePoint 和 VolumePoint 是两个极简的数据点接口,分别用于折线图和柱状图的数据输入。它们各自只包含两个字段:日期标签和数值。这种极简设计使图表绘制函数的接口更加清晰,数据点的构造也更加轻量。在 HarmonyOS ArkTS API 24 中,这种小接口的编译产物非常精简,不会引入额外的运行时开销。
interface FloatEmoji {
emoji: string
y: number
speed: number
seed: number
}
FloatEmoji 接口定义了漂浮表情动画的数据模型。y 字段表示垂直位置百分比(0 到 100),speed 表示每帧(60ms)向上移动的百分比,seed 是用于生成随机性参数的种子值。这个接口的设计体现了"数据驱动动画"的理念——每个表情的状态被完全描述为一个数据对象,动画的推进就是对这些数据对象的更新,而 UI 渲染则自动响应数据变化。
interface StatBlock {
label: string
value: string
sub: string
}
interface WalletInfo {
balance: number
income: number
expense: number
frozen: number
}
interface UserProfile {
name: string
avatar: string
level: string
verified: boolean
trades: number
followers: number
following: number
}

StatBlock 是一个通用的统计信息块接口,包含标签、值和子说明三个字段,用于"我的"页面中的交易统计区域。WalletInfo 定义了钱包信息,包含余额、收入、支出和冻结金额。UserProfile 定义了用户个人资料,特别值得注意的是 verified 布尔字段,它在 UI 中控制认证标识的显示与否——这种将显示逻辑与数据绑定的方式是声明式 UI 的典型应用模式。
二、色彩调色板系统
2.1 COLORS 常量定义
const COLORS: ColorPalette = {
bg: '#0A0A0A',
card: '#1A1A1A',
cardLight: '#2A2A2A',
primary: '#FFFFFF',
accent: '#FF4444',
gold: '#FFD700',
textPrimary: '#FFFFFF',
textSecondary: '#999999',
textDim: '#555555',
border: '#333333',
success: '#00C853',
danger: '#FF1744'
}
COLORS 常量是 ColorPalette 接口的唯一实现实例,它定义了整个应用的视觉基调。从色彩学角度分析,这套配色方案是一个典型的"暗黑街头风"设计——以近黑色(#0A0A0A)为背景基调,通过多层次的灰色(#1A1A1A、#2A2A2A、#333333)构建空间层次感,再以纯白(#FFFFFF)作为主文本色形成高对比度,最后以红色(#FF4444)作为强调色来引导用户的视觉焦点。
这种色彩选择并非随意为之。球鞋文化本身根植于街头潮流文化,而暗黑配色正是街头风视觉语言的核心元素。从用户体验角度,暗色模式在高亮度 OLED 屏幕上具有更好的能效表现,同时在高对比度场景下(如价格数字、趋势箭头)能够提供更清晰的视觉辨识度。HarmonyOS 6.1.1 系统对暗色模式的渲染优化也使得这种配色方案在性能上具有优势——大量使用黑色背景意味着 OLED 屏幕的对应像素可以完全关闭,从而节省电量。
具体来看每个颜色值的应用场景:bg: '#0A0A0A' 是最深的背景色,用于整个页面的底层背景;card: '#1A1A1A' 是卡片背景,与背景色之间有微妙的亮度差(约 10% 的灰度提升),这种差异在视觉上足以区分卡片边界,但又不会过于突兀;cardLight: '#2A2A2A' 是卡片的亮色变体,用于需要额外强调的卡片内部区域,如弹框中的信息块背景。accent: '#FF4444' 是整个应用中最亮眼的颜色,它被用于按钮背景、激活状态的标签、Tab 下划线等所有需要用户注意的交互元素。gold: '#FFD700' 则用于特殊标识,如"HOT"徽章、限量标签等,金色在暗色背景上的视觉冲击力极强,能够有效吸引用户的注意力。
文本颜色的三级体系——textPrimary: '#FFFFFF'(纯白)、textSecondary: '#999999'(中灰)、textDim: '#555555'(深灰)——构建了一个清晰的信息层级。在球鞋交易平台中,商品名称、价格等核心信息使用白色,确保最高可读性;品牌名、日期等辅助信息使用中灰,降低视觉权重但不影响阅读;而版权声明、手续费说明等非关键信息使用深灰,仅在用户主动关注时才会被注意到。这种层级设计遵循了格式塔心理学中的"图底关系"原则,使主信息自然成为视觉焦点。
三、静态数据模型层
3.1 商品数据:SNEAKERS 数组
const SNEAKERS: Sneaker[] = [
{
id: 1, name: 'Air Jordan 1 Retro High OG', brand: 'Jordan', series: '芝加哥 Chicago',
price: 3899, originalPrice: 4299, retailPrice: 1299, emoji: '🏀', color: '#8B0000', colorDeep: '#4A0000',
trend: 'up', changePercent: 6.8, hot: true, releaseDate: '2025-09-14',
tags: ['经典配色', '冲冲冲']
},
// ... 更多商品数据
]

SNEAKERS 数组是商品数据层的核心,包含了 14 款球鞋的完整信息。每一款球鞋的数据都严格遵循 Sneaker 接口定义的结构,这得益于 TypeScript 的类型系统在编译期的检查。从业务角度看,这 14 款球鞋涵盖了市场主流品牌(Jordan、Nike、Adidas、New Balance、Converse、Asics、Li-Ning)和多种品类(篮球鞋、休闲鞋、老爹鞋、德训鞋等),为 UI 展示提供了足够丰富的数据样本。
特别值得关注的是 tags 字段的设计。每款球鞋附带 2 个标签字符串,这些标签在 UI 中以小徽章的形式展示在商品名称下方。标签的内容既有品类描述(如"经典配色"、“实战好鞋”),也有用户社区常用的口语化表达(如"冲冲冲"、“年度鞋王”),这种设计使商品列表不仅有干瘪的技术参数,更增添了社区氛围和情感色彩。在 HarmonyOS ArkTS API 24 的 ForEach 渲染中,tags 数组的每个元素会被渲染为一个独立的 Text 组件,并通过 margin 属性实现标签之间的间距控制。
emoji 字段的选择也颇具匠心。每款球鞋的 emoji 都与其特征相关联——Air Jordan 1 芝加哥用篮球 emoji(🏀),Travis Scott 联名用马蹄 emoji(🥿),熊猫 Dunk 用运动鞋 emoji(👟),斑马 Yeezy 用斑马 emoji(🦓)。这种视觉化的商品标识在列表渲染中提供了快速识别的视觉锚点,同时避免了加载真实图片资源的网络开销和内存占用。在 HarmonyOS 6.1.1 系统中,emoji 的渲染由系统字体直接支持,无需额外的字体文件或图片资源。
3.2 鉴定记录数据:AUTH_RECORDS 数组
const AUTH_RECORDS: AuthRecord[] = [
{ id: 1, sneakerName: 'Air Jordan 1 Chicago', brand: 'Jordan', submitDate: '2026-08-20', status: '通过', channel: '得物', price: 3899, result: '正品 · 五项全对' },
// ... 更多鉴定记录
]

AUTH_RECORDS 数组包含了 13 条鉴定记录,覆盖了四种可能的鉴定状态(通过、待审、存疑、不通过)和四种购买渠道(得物、官网、淘宝、线下)。这种数据多样性确保了 UI 在各种状态下都能被充分测试和展示。
result 字段的文案设计值得分析。"正品 · 五项全对"使用了"项目符号 + 结果"的格式,"中底胶水异常"则直接描述了问题所在,"检测中 · 预计24h"提供了进度信息。这些文案在 UI 中以灰色小字展示在记录卡片的底部,虽然视觉权重不高,但提供了用户最关心的核心信息。在真实应用中,这些文案应该由后端的鉴定系统根据检测结果自动生成,前端只需负责展示。
3.3 收藏数据:COLLECTIONS 数组
const COLLECTIONS: CollectionItem[] = [
{ id: 1, name: 'AJ1 芝加哥', brand: 'Jordan', size: 42, acquiredPrice: 3299, currentPrice: 3899, emoji: '🏀', color: '#8B0000', profit: 600, profitPercent: 18.2, liked: true },
// ... 更多收藏数据
]
COLLECTIONS 数组包含了 13 条收藏记录。与 SNEAKERS 不同的是,这里的每条记录都包含了用户的个人交易信息——入手价、当前估值和盈亏数据。profit 字段的值有正有负,profitPercent 字段也相应地有正有负,这种数据分布确保了 UI 中的盈利(绿色)和亏损(红色)两种状态都能被展示。
值得注意的是收藏数据中的尺码信息。size 字段使用的是 US 尺码标准(38-46 范围),但在 UI 展示时会同时显示 US 和 EU 两种尺码标准(如"US 42 (EU 26cm)")。这种尺码双标展示在球鞋交易领域是标准做法,因为不同品牌和地区使用不同的尺码标准,用户需要同时看到两种尺码才能做出准确的判断。
3.4 排行榜数据:RANKINGS 数组
const RANKINGS: RankItem[] = [
{ id: 1, name: 'SneakerKing_钉子', avatar: '👑', tradeCount: 1286, volume: 1286000, profit: 268000, badge: '鞋皇' },
// ... 更多排行数据
]

RANKINGS 数组包含了 13 位交易达人的排行数据。volume 字段的值范围从 18 万到 128 万,profit 字段的值范围从 3 万到 26 万,这些大数字在 UI 中需要通过 formatVolume 函数进行"万"单位转换后展示。badge 字段是每个达人的特色标签,如"鞋皇"、“猎手”、"东城"等,这些标签增强了排行榜的趣味性和社交属性。
第一名用户的 avatar 使用了皇冠 emoji(👑),而其他用户使用各自特色的 emoji。在 UI 的前三名领奖台区域,冠亚季军分别使用奖牌 emoji(🥇🥈🥉)作为头像,这与列表区域使用的 avatar 形成了视觉区分,突出了前三名的特殊地位。
3.5 行情数据与配置常量
const PRICE_TREND: PricePoint[] = [
{ day: '08-17', price: 1999 },
{ day: '08-18', price: 2089 },
{ day: '08-19', price: 2042 },
{ day: '08-20', price: 2168 },
{ day: '08-21', price: 2210 },
{ day: '08-22', price: 2145 },
{ day: '08-23', price: 2199 }
]
const VOLUME_BARS: VolumePoint[] = [
{ day: '08-17', volume: 1860 },
{ day: '08-18', volume: 2420 },
{ day: '08-19', volume: 1680 },
{ day: '08-20', volume: 3250 },
{ day: '08-21', volume: 2890 },
{ day: '08-22', volume: 2140 },
{ day: '08-23', volume: 3560 }
]

PRICE_TREND 和 VOLUME_BARS 是行情页面的两个图表数据源,分别包含 7 天的价格走势和成交量数据。这两个数据集的设计模拟了真实的市场波动——价格在 1999 到 2210 之间波动,成交量在 1680 到 3560 之间波动,呈现出真实市场的起伏特征。这些数据将分别传入 drawLineChart 和 drawBarChart 函数进行 Canvas 绘制。
const TREND_TAGS: string[] = ['AJ1 芝加哥', '熊猫 Dunk', 'Samba 德训', '倒钩 Mocha', 'NB 550', '黑猫 AJ4', '绿蛇 Kobe', '全城 10']
const SIZE_OPTIONS: number[] = [38, 39, 40, 41, 42, 43, 44, 45, 46]
const CONDITION_OPTIONS: string[] = ['全新未拆', '仅试穿', '有穿着痕迹']
const CHANNEL_OPTIONS: string[] = ['官网', '得物', '淘宝', '线下']
const BRAND_OPTIONS: string[] = ['Jordan', 'Nike', 'Adidas', 'New Balance', 'Converse', 'Asics', 'Li-Ning']
这组配置常量为 UI 提供了选项数据。TREND_TAGS 是顶部搜索栏下方的热门标签列表,SIZE_OPTIONS 是尺码选择器的可选值,CONDITION_OPTIONS 是成色选择器的选项,CHANNEL_OPTIONS 是购买渠道选择器的选项,BRAND_OPTIONS 是品牌偏好选择器的选项。将这些配置抽离为常量而非硬编码在组件中,使得未来修改选项内容时只需更新常量定义,而不需要修改组件代码。这种"配置与逻辑分离"的原则在基于 HarmonyOS API 24 的开发实践中是非常重要的工程习惯。
3.6 用户数据与钱包信息
const USER_PROFILE: UserProfile = {
name: 'SneakerGravity_大卫', avatar: '🧢', level: 'LV.7 银牌玩家', verified: true,
trades: 326, followers: 2841, following: 189
}
const WALLET: WalletInfo = { balance: 26888.5, income: 96820.0, expense: 69931.5, frozen: 1200 }
const TRANS_STATS: StatBlock[] = [
{ label: '本月买入', value: '12 单', sub: '支出 ¥42,300' },
{ label: '本月卖出', value: '9 单', sub: '收入 ¥48,900' },
{ label: '在售', value: '5 单', sub: '冻结 ¥1,200' },
{ label: '鉴定', value: '23 次', sub: '通过率 91%' }
]
这三个常量定义了"我的"页面所需的用户数据。USER_PROFILE 中的 level 字段采用了"等级 + 称号"的格式(“LV.7 银牌玩家”),这种设计在游戏化社交系统中很常见,能够激励用户通过交易提升等级。verified: true 表示用户已通过实名认证,在 UI 中会显示认证标识。
WALLET 中的 balance: 26888.5 是一个带有小数的浮点数,在 UI 中通过 formatPrice 函数格式化时会显示为"¥26,888.5"。frozen: 1200 表示被冻结的金额——当用户挂出售卖时,平台会冻结一定金额作为保证金,这个值与 TRANS_STATS 中的"冻结 ¥1,200"保持一致,体现了数据的一致性设计。
TRANS_STATS 数组定义了四个统计信息块,每个块包含标签、值和子说明。这种结构化的统计信息设计使 UI 能够以统一的 ForEach 方式渲染所有统计项,而不需要为每个统计项编写不同的 UI 代码。这种数据驱动的 UI 渲染方式是 HarmonyOS ArkTS API 24 声明式编程的核心优势之一。
四、纯函数架构层
4.1 趋势颜色与符号函数
function trendColor(t: string): string {
if (t === 'up') {
return COLORS.success
}
if (t === 'down') {
return COLORS.danger
}
return COLORS.textSecondary
}
function trendSymbol(t: string): string {
if (t === 'up') {
return '↑'
}
if (t === 'down') {
return '↓'
}
return '→'
}
trendColor 和 trendSymbol 是一对紧密关联的纯函数,它们根据趋势字符串返回对应的颜色和符号。这两个函数的设计体现了"单一职责原则"——每个函数只做一件事,颜色和符号的映射逻辑被分离到不同的函数中,使得未来修改映射规则时不会产生连锁影响。
trendColor 函数将"up"映射为绿色(COLORS.success),“down"映射为红色(COLORS.danger),其他值(包括"stable”)映射为中灰色(COLORS.textSecondary)。这种"涨绿跌红"的色彩约定在金融行情领域是通用的视觉语言,用户无需阅读文字就能通过颜色判断趋势方向。trendSymbol 函数则将趋势映射为箭头符号——"↑"表示上涨,"↓"表示下跌,"→"表示平稳。
这两个函数在 UI 中的配合使用形成了一个完整的趋势指示器:箭头符号提供方向信息,颜色提供情感强化。在商品列表卡片中,这两个函数的返回值被同时应用到两个 Text 组件上——一个显示箭头,一个显示百分比变化值,两者使用相同的颜色,形成了视觉上的统一性。这种"数据到视觉"的映射完全由纯函数承担,组件代码只需调用函数并应用返回值,不需要包含任何条件判断逻辑,这大大简化了组件代码的复杂度。
4.2 状态颜色函数
function statusColor(s: string): string {
if (s === '通过') {
return COLORS.success
}
if (s === '待审') {
return COLORS.gold
}
if (s === '存疑') {
return COLORS.accent
}
return COLORS.danger
}
statusColor 函数是鉴定状态到颜色的映射函数,它将鉴定记录的四种状态映射到四种不同的颜色。"通过"使用绿色(COLORS.success),传达积极正面的信号;"待审"使用金色(COLORS.gold),表示需要等待但并非负面;"存疑"使用强调红色(COLORS.accent),警示用户注意潜在风险;"不通过"使用危险红色(COLORS.danger),明确传达负面结果。
这里值得注意的设计细节是"存疑"和"不通过"使用了不同的红色——COLORS.accent(#FF4444)和 COLORS.danger(#FF1744)。虽然两者都是红色,但 accent 偏亮偏暖,danger 偏深偏冷。这种微妙的色彩差异在 UI 上为用户提供了额外的信息层次:"存疑"是一个需要关注的警示状态,而"不通过"是一个确定性的负面结果。在基于 HarmonyOS API 24 的开发中,这种精细化的色彩语义设计能够显著提升用户体验,使用户在快速浏览列表时就能感知到不同状态的严重程度。
4.3 价格与成交量格式化函数
function formatPrice(p: number): string {
return '¥' + p.toLocaleString('zh-CN')
}
function formatVolume(v: number): number | string {
if (v >= 10000) {
return '¥' + (v / 10000).toFixed(1) + '万'
}
return '¥' + v.toLocaleString('zh-CN')
}
formatPrice 函数将数字格式化为带人民币符号和中式千位分隔符的字符串。toLocaleString('zh-CN') 是 JavaScript/TypeScript 内置的国际化方法,它会根据指定的区域设置自动添加千位分隔符——例如 3899 会被格式化为 3,899,26888.5 会被格式化为 26,888.5。这个函数在整个应用中被广泛使用,从商品价格到钱包余额,所有金额展示都通过它进行格式化,确保了全局一致的数字展示格式。
formatVolume 函数在 formatPrice 的基础上增加了"万"单位的智能转换。当数值大于等于 10000 时,它会将数值除以 10000 并保留一位小数,然后加上"万"后缀——例如 1286000 会被格式化为 ¥128.6万。这种设计在中文语境下非常自然,因为中文习惯使用"万"作为大数字的计量单位。当数值小于 10000 时,函数回退到与 formatPrice 相同的逻辑。这两个函数共同构成了应用的价格格式化体系,使得所有金额展示都遵循统一的格式规范。
4.4 数据分块函数:chunkPairs
function chunkPairs(items: CollectionItem[]): CollectionItem[][] {
const pairs: CollectionItem[][] = []
let i: number = 0
while (i < items.length) {
if (i + 1 < items.length) {
const pair: CollectionItem[] = [items[i], items[i + 1]]
pairs.push(pair)
} else {
const single: CollectionItem[] = [items[i]]
pairs.push(single)
}
i = i + 2
}
return pairs
}
chunkPairs 函数是一个通用的数组分块工具,它将一个一维数组转换为包含二元子数组的二维数组。这个函数在收藏页面的双列网格布局中起着关键作用——HarmonyOS ArkTS API 24 的 Row + Column 布局系统不直接支持 CSS Grid 风格的多列布局,因此需要通过将数据预先分组为"对"来实现双列效果。
函数的逻辑清晰而严谨:使用 while 循环以步长 2 遍历输入数组,每次迭代检查当前位置之后是否还有元素。如果有,则取两个元素组成一对;如果没有,则将最后一个元素单独组成一组。这种处理确保了当输入数组长度为奇数时,最后一个元素不会被丢弃,而是作为单独的一组被保留。
在 HarmonyOS ArkTS API 24 的 ForEach 渲染中,chunkPairs 的返回值被用于生成双列布局——每个"对"被渲染为一个包含两个 Column(各占 layoutWeight(1))的 Row,两个 Column 之间有一个 10 宽的间隔 Column。当"对"只有一个元素时(即奇数长度的最后一个),第二个 Column 位置会被一个空的 Column().layoutWeight(1) 占位,确保布局对齐。这种通过数据预处理实现布局的方式是声明式 UI 中处理复杂网格布局的常用模式。
4.5 数据查找函数
function getSneakerById(id: number): Sneaker {
for (let i = 0; i < SNEAKERS.length; i++) {
if (SNEAKERS[i].id === id) {
return SNEAKERS[i]
}
}
return SNEAKERS[0]
}
function getCollectionById(id: number): CollectionItem {
for (let i = 0; i < COLLECTIONS.length; i++) {
if (COLLECTIONS[i].id === id) {
return COLLECTIONS[i]
}
}
return COLLECTIONS[0]
}
getSneakerById 和 getCollectionById 是两个数据查找函数,它们根据 ID 在静态数据数组中查找对应的记录。这两个函数在弹框组件中被频繁调用——当用户点击某个商品的"买入"按钮时,listingTargetId 状态被设置为该商品的 ID,挂售弹框通过 getSneakerById(this.listingTargetId) 获取商品的完整信息进行展示。
这两个函数的实现采用了简单的线性查找(for 循环遍历),这在数据量较小(14 条商品记录、13 条收藏记录)的场景下是完全可以接受的。线性查找的时间复杂度为 O(n),对于小规模数据来说与哈希表查找的实际性能差异可以忽略不计,而代码的简洁性却更优。函数的容错设计也值得关注:当查找的 ID 不存在时,函数返回数组的第一个元素作为默认值,而不是返回 null 或 undefined。这种设计使得调用方不需要处理空值情况,简化了组件代码——即使查找失败,UI 也不会因为空值引用而崩溃,而是显示默认商品的信息。
4.6 漂浮表情动画函数群
function floatSize(seed: number): number {
return 14 + (seed % 5) * 6
}
function floatX(seed: number): number {
return 3 + (seed * 37) % 90
}
function floatOpacity(y: number): number {
if (y > 96) {
return (106 - y) / 10
}
if (y < 12) {
return y / 12
}
return 1
}
function floatWobble(seed: number): number {
return (seed % 2 === 0) ? -1 : 1
}
这组函数共同构成了漂浮表情动画的"渲染参数生成器"。每个函数接收一个或多个参数(通常是 seed 种子值或 y 位置值),返回一个确定性的视觉参数。这种"确定性随机"的设计使得表情的初始位置和大小虽然看起来是随机的,但实际上是可复现的——相同的 seed 总是产生相同的视觉参数。
floatSize 函数根据种子值生成表情的字体大小,范围在 14 到 38 之间(14 + 06 到 14 + 46)。floatX 函数生成水平位置百分比,范围在 3 到 93 之间。floatOpacity 函数根据垂直位置生成透明度——当表情接近顶部(y < 12)或底部(y > 96)时,透明度逐渐降低,形成淡入淡出效果。floatWobble 函数生成一个 +1 或 -1 的摆动方向,用于在水平位置上添加微小的偏移,增加动画的自然感。
advanceFloat 函数是动画推进的核心,它接收当前的 FloatEmoji 数组,返回一个新的数组,其中每个表情的 y 值减少了 speed(即向上移动)。当一个表情的 y 值降到 -6 以下时,它被重置为 108(即从底部重新出现),实现了循环动画效果。这个函数的"纯函数"特性非常重要——它不修改输入数组,而是创建并返回一个全新的数组,这确保了 ArkTS 的状态管理系统能够正确检测到状态变化并触发重新渲染。
function advanceFloat(items: FloatEmoji[]): FloatEmoji[] {
const next: FloatEmoji[] = []
for (let i = 0; i < items.length; i++) {
const it: FloatEmoji = items[i]
let ny: number = it.y - it.speed
if (ny < -6) {
ny = 108
}
const moved: FloatEmoji = { emoji: it.emoji, y: ny, speed: it.speed, seed: it.seed }
next.push(moved)
}
return next
}
buildFloatEmojis 函数是动画的初始化函数,它创建了 12 个 FloatEmoji 对象的初始数组。每个表情的初始 y 值通过 i * 9 % 100 计算,确保 12 个表情在垂直方向上均匀分布。speed 值通过 0.35 + (seeds[i] % 4) * 0.18 计算,范围在 0.35 到 0.89 之间,不同表情以不同速度上升,增强了动画的层次感。
4.7 倒计时函数群
function padTwo(n: number): string {
if (n < 10) {
return '0' + n
}
return '' + n
}
function countdownHours(total: number): string {
return padTwo(Math.floor(total / 3600) % 24)
}
function countdownMinutes(total: number): string {
return padTwo(Math.floor(total / 60) % 60)
}
function countdownSeconds(total: number): string {
return padTwo(total % 60)
}
倒计时函数群负责将总秒数转换为"时:分:秒"格式的字符串。padTwo 是一个辅助函数,将单位数前面补零——例如 5 变成 05,12 保持 12。这个函数确保了倒计时显示的固定两位数格式,避免了数字位数变化导致的 UI 抖动。
countdownHours、countdownMinutes 和 countdownSeconds 三个函数分别从总秒数中提取时、分、秒部分。Math.floor(total / 3600) % 24 计算小时数(取模 24 是因为小时显示不超过 23),Math.floor(total / 60) % 60 计算分钟数,total % 60 计算秒数。这三个函数在 UI 中被分别调用,返回值被绑定到三个独立的 Text 组件上,中间用冒号分隔,形成了经典的倒计时显示效果。
在组件状态层面,flashSec 状态变量初始值为 3 * 3600 + 42 * 60 + 18(即 3 小时 42 分 18 秒),每秒通过定时器递减 1。当递减到 0 时,重置为初始值。这种"倒计时归零后重置"的设计模拟了限时抢购活动的循环特性——每个活动结束后立即开始下一个活动。
4.8 Canvas 折线图绘制函数
function drawLineChart(ctx: CanvasRenderingContext2D, points: PricePoint[]): void {
const w: number = ctx.width
const h: number = ctx.height
const pad: number = 26
const minP: number = 1950
const maxP: number = 2260
ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = '#333333'
ctx.lineWidth = 0.6
for (let i = 0; i <= 3; i++) {
const gy: number = pad + (h - pad * 2) * i / 3
ctx.beginPath()
ctx.moveTo(pad, gy)
ctx.lineTo(w - pad / 2, gy)
ctx.stroke()
}
ctx.strokeStyle = '#FF4444'
ctx.lineWidth = 2
ctx.beginPath()
for (let i = 0; i < points.length; i++) {
const x: number = pad + (w - pad * 1.5) * i / (points.length - 1)
const y: number = (h - pad) - (points[i].price - minP) / (maxP - minP) * (h - pad * 2)
if (i === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.stroke()
for (let i = 0; i < points.length; i++) {
const x: number = pad + (w - pad * 1.5) * i / (points.length - 1)
const y: number = (h - pad) - (points[i].price - minP) / (maxP - minP) * (h - pad * 2)
ctx.beginPath()
ctx.arc(x, y, 3, 0, Math.PI * 2)
ctx.fillStyle = '#FFFFFF'
ctx.fill()
}
ctx.fillStyle = '#999999'
ctx.font = '10px sans-serif'
for (let i = 0; i < points.length; i++) {
const x: number = pad + (w - pad * 1.5) * i / (points.length - 1)
ctx.fillText(points[i].day, x - 14, h - 6)
}
}
drawLineChart 函数是整个应用中最复杂的纯函数之一,它使用 Canvas 2D API 绘制一个完整的价格走势折线图。这个函数的实现可以分解为四个绘制阶段:清除画布与绘制网格线、绘制折线、绘制数据点、绘制日期标签。
第一阶段是清除画布并绘制背景网格。ctx.clearRect(0, 0, w, h) 清除整个画布区域,确保每次绘制都是从一个干净的画布开始。然后使用一个 for 循环绘制 4 条水平网格线(i 从 0 到 3),网格线的颜色为深灰色(#333333),线宽为 0.6。网格线的 y 坐标通过 pad + (h - pad * 2) * i / 3 计算,使得 4 条线在上下边距之间均匀分布。这些网格线为用户提供了价格刻度的视觉参考。
第二阶段是绘制折线本身。折线使用强调红色(#FF4444)和 2 像素线宽。数据点的 x 坐标通过 pad + (w - pad * 1.5) * i / (points.length - 1) 计算,使得 7 个数据点在水平方向上均匀分布。y 坐标的计算是核心——(h - pad) - (points[i].price - minP) / (maxP - minP) * (h - pad * 2) 这个公式将价格值映射到画布坐标。其中 minP 和 maxP 分别是价格范围的最小值和最大值(1950 和 2260),(points[i].price - minP) / (maxP - minP) 计算价格在范围内的归一化比例(0 到 1),然后乘以可用高度 (h - pad * 2) 并从底部偏移 (h - pad) 开始向上计算,最终得到 y 坐标。这种"价格到像素"的映射是数据可视化的基础算法。
第三阶段是绘制数据点。每个数据点位置绘制一个半径为 3 像素的白色实心圆,这些圆点标注了每个价格数据点的精确位置,使用户能够清楚地看到每个数据点的位置。ctx.arc(x, y, 3, 0, Math.PI * 2) 绘制一个完整的圆(从 0 到 2π 弧度),ctx.fillStyle = '#FFFFFF' 设置填充颜色为白色。
第四阶段是绘制日期标签。使用 ctx.font = '10px sans-serif' 设置字体大小和字体族,然后遍历数据点,在每个数据点下方绘制日期文本。x - 14 的偏移量使文本相对于数据点居中显示(10px 字体下,日期文本宽度约为 28 像素,偏移 14 像素使其居中)。
4.9 Canvas 柱状图绘制函数
function drawBarChart(ctx: CanvasRenderingContext2D, points: VolumePoint[]): void {
const w: number = ctx.width
const h: number = ctx.height
const pad: number = 22
const maxV: number = 3800
const barW: number = (w - pad * 1.5) / points.length * 0.5
ctx.clearRect(0, 0, w, h)
ctx.strokeStyle = '#333333'
ctx.lineWidth = 0.6
for (let i = 0; i <= 2; i++) {
const gy: number = pad + (h - pad * 2) * i / 2
ctx.beginPath()
ctx.moveTo(pad, gy)
ctx.lineTo(w - pad / 2, gy)
ctx.stroke()
}
for (let i = 0; i < points.length; i++) {
const x: number = pad + (w - pad * 1.5) * (i + 0.5) / points.length - barW / 2
const barH: number = (points[i].volume / maxV) * (h - pad * 2)
const y: number = h - pad - barH
ctx.fillStyle = '#FFD700'
ctx.fillRect(x, y, barW, barH)
ctx.fillStyle = '#999999'
ctx.font = '9px sans-serif'
ctx.fillText(points[i].day, x - 8, h - 6)
}
}
drawBarChart 函数绘制成交量柱状图,其实现逻辑与折线图有相似之处,但柱状图的绘制重点在于每个柱子的位置和高度计算。
网格线绘制部分与折线图类似,但只绘制 3 条水平线(i 从 0 到 2),因为柱状图的视觉重点在柱子本身而非网格线。柱子的宽度 barW 通过 (w - pad * 1.5) / points.length * 0.5 计算——先计算每个数据点分配的水平空间,然后乘以 0.5 得到柱子宽度(柱子宽度为数据点间距的一半,留出间隙使柱子之间有视觉分隔)。
每个柱子的 x 坐标计算是关键:pad + (w - pad * 1.5) * (i + 0.5) / points.length - barW / 2。这里 (i + 0.5) / points.length 计算每个数据点的中心位置(加 0.5 使柱子居中在数据点位置),然后减去 barW / 2 使柱子从中心点向左偏移半个柱宽,实现居中对齐。柱子高度 barH 通过 (points[i].volume / maxV) * (h - pad * 2) 计算——成交量除以最大值得到归一化比例,然后乘以可用高度。柱子的 y 坐标为 h - pad - barH,即从画布底部向上偏移柱子高度,确保柱子从底部开始向上生长。
柱子使用金色(#FFD700)填充,与折线图的红色形成色彩区分,使两种图表在视觉上各有特色。日期标签使用 9px 字体(比折线图小 1px),并通过 x - 8 偏移使文本相对于柱子居中。
五、页面组件状态管理
5.1 组件声明与状态定义
@Entry
@Component
struct MainPage {
@State currentTab: number = 0
@State bottomTab: number = 0
@State activeModal: number = 0
@State floatEmojis: FloatEmoji[] = buildFloatEmojis()
@State flashSec: number = 3 * 3600 + 42 * 60 + 18
@Entry 装饰器标记 MainPage 为应用的入口组件,它是整个页面的根组件。@Component 装饰器将其标记为 ArkTS 组件,使其能够使用声明式 UI 的各种特性。这两个装饰器的组合是 HarmonyOS ArkTS API 26 中定义页面入口组件的标准模式。
@State 装饰器是 ArkTS 声明式 UI 的核心机制之一,它将组件内部的变量标记为"响应式状态"。当 @State 变量的值发生变化时,框架会自动检测哪些 UI 部分依赖于这个变量,并精确地重新渲染这些部分——而不是重新渲染整个组件。这种细粒度的状态追踪和差分更新机制是声明式 UI 性能的基础。
currentTab 状态控制内容区域的 Tab 切换(0-5 对应热卖、鉴定、行情、收藏、排行、我的六个 Tab),bottomTab 状态控制底部导航栏的激活状态。这两个状态虽然都控制导航相关功能,但被分离为两个独立的状态变量,这是因为内容 Tab 栏和底部导航栏是两个独立的 UI 区域,它们在某些场景下可能不同步——例如底部导航栏有 5 个按钮,而内容 Tab 栏有 6 个标签,"排行"Tab 只在内容栏中存在而不在底部导航栏中。
activeModal 状态控制弹框的显示——0 表示无弹框,1 表示鉴定提交弹框,2 表示挂售弹框,3 表示尺码偏好弹框,4 表示删除收藏弹框。使用单一状态变量控制所有弹框(而非为每个弹框使用独立的布尔状态)是一个重要的设计决策,它天然地保证了同一时间只有一个弹框处于显示状态,避免了多个弹框叠加的问题。
floatEmojis 状态的初始值通过 buildFloatEmojis() 函数生成,这是一个在组件初始化时执行一次的函数调用。flashSec 状态的初始值是一个计算表达式 3 * 3600 + 42 * 60 + 18,结果为 13338 秒(即 3 小时 42 分 18 秒),这种使用表达式而非硬编码数字的方式使初始值的含义一目了然。
5.2 挂售弹框状态组
// 挂售弹框状态
@State selectedSize: number = 42
@State askPrice: string = ''
@State selectedCondition: string = '全新未拆'
@State autoAccept: boolean = true
@State listingTargetId: number = 2
挂售弹框的状态组包含了用户在挂售流程中需要设置的所有参数。selectedSize 是用户选择的尺码(默认 42,因为这是最常见的男鞋尺码),askPrice 是用户输入的挂售价格(初始为空字符串,引导用户输入),selectedCondition 是成色选择(默认"全新未拆",因为新鞋是最常见的挂售场景),autoAccept 是闪电直售开关(默认开启,降低用户的操作门槛),listingTargetId 是当前挂售目标商品的 ID(默认 2,即 Travis Scott x AJ1)。
这组状态的设计体现了"表单状态管理"的最佳实践——每个表单字段都有独立的状态变量,表单的初始值合理地预设为最常见的选项。当用户打开挂售弹框时,表单已经预填了合理的默认值,用户只需确认或微调即可提交,这大大降低了用户的操作成本。
5.3 其他弹框状态组
// 鉴定弹框状态
@State selectedChannel: string = '得物'
@State authPrice: string = ''
// 尺码偏好弹框状态
@State prefSizes: number[] = [41, 42, 43]
@State prefBrands: string[] = ['Jordan', 'Nike']
@State notifyToggle: boolean = true
// 删除收藏弹框状态
@State delTargetId: number = 1
鉴定弹框的状态组较为简单——selectedChannel 是购买渠道选择(默认"得物",因为这是球鞋交易的主要平台),authPrice 是购入价格输入。尺码偏好弹框的状态组包含多选的尺码数组 prefSizes(默认 [41, 42, 43])和多选的品牌数组 prefBrands(默认 [‘Jordan’, ‘Nike’]),以及通知开关 notifyToggle。删除收藏弹框只需要 delTargetId 来标识要删除的收藏项。
prefSizes 和 prefBrands 使用数组而非单个值,这是因为它们是多选控件。在 HarmonyOS ArkTS API 24 中,@State 修饰的数组变量在被重新赋值时(如 this.prefSizes = next)会触发 UI 更新,但直接修改数组元素(如 this.prefSizes.push(v))不会触发更新——这就是为什么后面的 togglePrefSize 函数会创建新数组而非原地修改的原因。
5.4 私有变量与 Canvas 上下文
private floatTimerId: number = -1
private tickTimerId: number = -1
private lineCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))
private barCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true))
private 修饰的变量不使用 @State 装饰器,这意味着它们的变化不会触发 UI 重新渲染。floatTimerId 和 tickTimerId 用于存储 setInterval 返回的定时器 ID,以便在组件销毁时清除定时器。这两个变量不需要触发 UI 更新,因此使用 private 而非 @State 是正确的选择。
lineCtx 和 barCtx 是两个 CanvasRenderingContext2D 实例,分别用于折线图和柱状图的绘制。new CanvasRenderingContext2D(new RenderingContextSettings(true)) 的参数 true 表示启用抗锯齿,这在图表绘制中非常重要——没有抗锯齿,线条和圆形的边缘会出现锯齿,影响视觉质量。这两个上下文实例在组件创建时初始化一次,然后在 Canvas 组件的 onReady 回调中被传入 drawLineChart 和 drawBarChart 函数进行绘制。将 Canvas 上下文定义为成员变量而非局部变量,确保了上下文的生命周期与组件一致,避免了重复创建的开销。
六、组件生命周期管理
6.1 aboutToAppear:定时器初始化
aboutToAppear(): void {
this.floatTimerId = setInterval(() => {
this.floatEmojis = advanceFloat(this.floatEmojis)
}, 60)
this.tickTimerId = setInterval(() => {
if (this.flashSec > 0) {
this.flashSec = this.flashSec - 1
} else {
this.flashSec = 3 * 3600 + 42 * 60 + 18
}
}, 1000)
}
aboutToAppear 是 ArkTS 组件的生命周期回调函数,在组件即将出现在屏幕上之前被调用。这个函数是初始化定时器的理想位置——此时组件的状态已经初始化完成,但 UI 尚未渲染,定时器的启动不会与首次渲染产生竞争条件。
两个定时器的设置体现了不同动画频率的精确控制。漂浮表情定时器每 60 毫秒执行一次,调用 advanceFloat 函数推进表情位置并更新 floatEmojis 状态。60 毫秒的间隔约为 16.7 帧/秒的更新频率,这对于背景装饰性动画来说已经足够流畅,同时不会占用过多的 CPU 资源。倒计时定时器每 1000 毫秒(1 秒)执行一次,将 flashSec 递减 1。当 flashSec 递减到 0 时,重置为初始值 3 * 3600 + 42 * 60 + 18,实现了循环倒计时。
这两个定时器的回调函数都使用了箭头函数 () => { ... },这确保了函数内部的 this 指向组件实例。如果使用普通函数,this 将指向定时器的回调上下文(通常是 global 或 window),导致 this.floatEmojis 和 this.flashSec 的访问失败。这是 JavaScript/TypeScript 中一个经典的 this 绑定问题,在 ArkTS 中同样需要注意。
6.2 aboutToDisappear:资源清理
aboutToDisappear(): void {
if (this.floatTimerId >= 0) {
clearInterval(this.floatTimerId)
}
if (this.tickTimerId >= 0) {
clearInterval(this.tickTimerId)
}
}
aboutToDisappear 是与 aboutToAppear 对应的生命周期回调,在组件即将从屏幕上消失时被调用。这个函数负责清理 aboutToAppear 中创建的资源——主要是清除两个定时器。
clearInterval 调用前的 >= 0 检查是一个防御性编程措施。虽然 aboutToAppear 总是会设置定时器 ID,但在某些异常情况下(如组件创建过程中出错),定时器 ID 可能仍为初始值 -1。在这种情况下调用 clearInterval(-1) 虽然不会报错,但也没有意义,因此通过条件检查避免了无效调用。
这种"创建-清理"的对称设计是资源管理的黄金法则。如果不清理定时器,当组件被销毁后定时器仍然会继续执行,导致对已销毁组件的状态进行更新,这会引发内存泄漏和潜在的运行时错误。在 HarmonyOS 6.1.1 系统中,未清理的定时器可能导致页面退出后仍然占用 CPU 资源,影响设备的续航表现。
七、顶部 Header 组件
7.1 PageHeader 构建器
@Builder PageHeader() {
Column() {
Row() {
Text('👟').fontSize(26)
Column() {
Text('球鞋引力').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('SNEAKER GRAVITY').fontSize(9).fontColor(COLORS.textDim).letterSpacing(1.5)
}.alignItems(HorizontalAlign.Start).margin({ left: 8 })
Row() {
Text('🔔').fontSize(20)
}
.width(36).height(36).borderRadius(18).backgroundColor(COLORS.cardLight)
.justifyContent(FlexAlign.Center).margin({ left: 10 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 16, right: 16, top: 10, bottom: 10 })
PageHeader 是顶部标题栏的构建器函数,它由三部分组成:品牌标识行、搜索栏和热门标签滚动栏。@Builder 装饰器将这个函数标记为 UI 构建器,使其可以在 build 方法中像组件一样被调用(如 this.PageHeader())。
品牌标识行使用 Row 容器实现水平布局,通过 justifyContent(FlexAlign.SpaceBetween) 使左侧的品牌标识和右侧的通知按钮分别贴向两端。左侧的品牌标识由一个球鞋 emoji 和一个 Column 组成——Column 中包含中文标题"球鞋引力"(18px 加粗白色)和英文副标题"SNEAKER GRAVITY"(9px 深灰色,字间距 1.5)。中英文双标题的设计既保留了中文的亲和力,又通过英文增加了国际化感。letterSpacing(1.5) 为英文字母之间添加了 1.5 的间距,使小字号文本更加清晰可读。
右侧的通知按钮是一个 36x36 的圆形容器(borderRadius(18)),背景色为 cardLight,内部居中放置一个铃铛 emoji。这种"圆形按钮"的设计是移动端 UI 的常见模式,通过固定的尺寸和圆角确保视觉一致性。
7.2 搜索栏设计
// 搜索栏
Row() {
Text('🔍').fontSize(16)
Text('搜索球鞋 / 品牌 / 货号').fontSize(13).fontColor(COLORS.textDim).margin({ left: 8 })
Row() {
Text('拍照').fontSize(11).fontColor(COLORS.textSecondary)
}
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10).backgroundColor(COLORS.bg)
.margin({ left: 8 })
}
.width('100%').height(40).borderRadius(20)
.backgroundColor(COLORS.card)
.padding({ left: 14, right: 14 })
.margin({ top: 6 })
搜索栏的设计模拟了一个输入框的视觉效果,但实际上由静态 Text 组件组成——这在展示型页面中是合理的,因为实际搜索功能需要跳转到独立的搜索页面。搜索栏的容器是一个 40 高、圆角 20(完全圆角)的 Row,背景色为 card,内部包含搜索图标、占位文本和一个"拍照"功能按钮。
"拍照"按钮的设计是一个亮点——它被嵌套在搜索栏内部,使用更深的背景色(COLORS.bg)和更小的圆角(10),形成视觉上的"按钮中的按钮"效果。这种设计在电商应用中很常见,因为它将"搜索"和"拍照搜物"两个功能紧凑地组合在同一个区域,节省了屏幕空间。
7.3 热门标签横向滚动
// 热门标签横向滚动
Scroll() {
Row() {
ForEach(TREND_TAGS, (tag: string) => {
Text(tag)
.fontSize(11).fontColor(COLORS.textSecondary)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor(COLORS.card)
.margin({ right: 8 })
}, (tag: string) => tag)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
热门标签区域使用 Scroll 组件实现横向滚动,这是 HarmonyOS ArkTS API 24 中实现水平列表滚动的标准方式。scrollable(ScrollDirection.Horizontal) 设置滚动方向为水平,scrollBar(BarState.Off) 隐藏滚动条,使界面更加简洁。
ForEach 遍历 TREND_TAGS 数组,为每个标签生成一个 Text 组件。每个标签的样式是 11px 字号、中灰色文字、12 的圆角和 card 背景色,通过 margin({ right: 8 }) 在标签之间添加 8 的间距。ForEach 的第三个参数是键值生成函数 (tag: string) => tag,它使用标签文本本身作为唯一键,确保列表更新时能够正确地进行差分渲染。
7.4 渐变背景效果
.width('100%')
.padding({ left: 0, right: 0, top: 0, bottom: 12 })
.linearGradient({
angle: 160,
colors: [['#1F1F1F', 0.0], ['#0A0A0A', 1.0]]
})
PageHeader 最外层 Column 使用 linearGradient 属性实现了一个线性渐变背景。渐变角度为 160 度(接近从上到下但略微倾斜),颜色从 #1F1F1F(较亮的深灰)渐变到 #0A0A0A(纯黑)。这种渐变效果使顶部标题栏与页面内容区域之间产生自然的过渡,避免了硬边界的突兀感。
在 HarmonyOS ArkTS API 24 中,linearGradient 接受一个包含 angle 和 colors 数组的对象。colors 数组中的每个元素是一个 [color, position] 元组,position 是 0 到 1 之间的值,表示颜色在渐变路径上的位置。[['#1F1F1F', 0.0], ['#0A0A0A', 1.0]] 表示从起点(0.0)的 #1F1F1F 渐变到终点(1.0)的 #0A0A0A。
八、内容 Tab 栏组件
8.1 TabsBar 构建器
@Builder TabsBar() {
Row() {
ForEach(['热卖', '鉴定', '行情', '收藏', '排行', '我的'], (label: string, idx: number) => {
Column() {
Text(label)
.fontSize(this.currentTab === idx ? 15 : 14)
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentTab === idx ? COLORS.textPrimary : COLORS.textSecondary)
Column()
.width(this.currentTab === idx ? 18 : 0)
.height(3)
.borderRadius(2)
.backgroundColor(COLORS.accent)
.margin({ top: 4 })
}
.padding({ left: 8, right: 8, top: 8, bottom: 6 })
.onClick(() => {
this.currentTab = idx
})
}, (label: string) => label)
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.padding({ top: 4 })
Column()
.width('100%')
.height(0.5)
.backgroundColor(COLORS.border)
}
TabsBar 是内容区域的 Tab 导航栏,包含六个标签:热卖、鉴定、行情、收藏、排行、我的。每个标签由一个 Column 组成,包含标签文本和一个下划线指示器。
标签的样式根据 this.currentTab === idx 的判断结果动态变化——激活状态的标签字号为 15、加粗、白色文字,非激活状态的标签字号为 14、常规粗细、中灰色文字。这种"大小 + 粗细 + 颜色"三重差异化的设计使激活标签在视觉上更加突出,用户一眼就能识别当前所在的 Tab。
下划线指示器是 Tab 栏的视觉焦点。激活标签的下划线宽度为 18、高度为 3、圆角为 2、颜色为强调红色(COLORS.accent)。非激活标签的下划线宽度为 0(即不可见)。这种通过宽度变化(而非 visibility 或 opacity)来控制指示器显示/隐藏的方式,在 ArkTS 中能够获得更好的动画过渡效果——如果使用了 if/else 条件渲染,指示器会突然出现/消失,而通过宽度变化则可以配合隐式动画实现平滑过渡。
Tab 栏底部有一个 0.5 高的分隔线,使用 border 颜色,在视觉上将 Tab 栏与内容区域分隔开来。这种极细的分隔线是移动端 UI 的常见设计,既提供了视觉边界,又不会过于抢眼。
onClick 回调将 this.currentTab 设置为被点击标签的索引,这会触发内容区域的条件渲染逻辑——build 方法中根据 currentTab 的值显示对应的内容构建器。
九、热卖页面组件
9.1 TabHot 大 Hero 卡片
@Builder TabHot() {
Scroll() {
Column() {
// 大 Hero 卡
Column() {
Column() {
Text('🔥 FLASH DROP').fontSize(10).fontColor(COLORS.gold).letterSpacing(2)
Text('👟').fontSize(84)
Text('Travis Scott x AJ1').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 6 })
Text('Mocha 倒钩 · 编号限定 500 双').fontSize(11).fontColor('#D8C8C8').margin({ top: 4 })
}
.width('100%')
.padding({ top: 20, bottom: 20 })
.justifyContent(FlexAlign.Center)
.borderRadius({ topLeft: 16, topRight: 16 })
.backgroundColor('#3B2F2F')
热卖页面(TabHot)的顶部是一个大型 Hero 卡片,用于展示限时抢购商品。这个卡片的设计目标是第一时间抓住用户的注意力,因此使用了大字号、特殊背景色和醒目的文案。
Hero 卡片的上半部分是一个带有深棕色背景(#3B2F2F,与 Travis Scott x AJ1 的 colorDeep 一致)的区域,内部居中排列了四个元素:顶部标签"🔥 FLASH DROP"(10px 金色,字间距 2,传达紧迫感)、大号球鞋 emoji(84px,作为视觉焦点)、商品名称"Travis Scott x AJ1"(18px 加粗白色)和副标题"Mocha 倒钩 · 编号限定 500 双"(11px,使用 #D8C8C8 暖灰色与棕色背景协调)。
letterSpacing(2) 为"FLASH DROP"的字母之间添加了 2 的间距,使大写字母的排列更加舒展,增强了"品牌标签"的视觉效果。borderRadius({ topLeft: 16, topRight: 16 }) 只设置顶部两个圆角,使卡片顶部呈圆角而底部保持直角,与卡片下半部分形成自然的衔接。
9.2 Hero 卡片的价格与倒计时
Row() {
Column() {
Text('当前市价').fontSize(10).fontColor(COLORS.textSecondary)
Text(formatPrice(8999)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start)
Column() {
Text('距结束').fontSize(10).fontColor(COLORS.textSecondary)
Row() {
Text(countdownHours(this.flashSec)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.padding({ left: 6, right: 6, top: 3, bottom: 3 }).borderRadius(4).backgroundColor(COLORS.cardLight)
Text(':').fontSize(14).fontColor(COLORS.accent).margin({ left: 3, right: 3 })
Text(countdownMinutes(this.flashSec)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.padding({ left: 6, right: 6, top: 3, bottom: 3 }).borderRadius(4).backgroundColor(COLORS.cardLight)
Text(':').fontSize(14).fontColor(COLORS.accent).margin({ left: 3, right: 3 })
Text(countdownSeconds(this.flashSec)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.padding({ left: 6, right: 6, top: 3, bottom: 3 }).borderRadius(4).backgroundColor(COLORS.cardLight)
}.margin({ top: 4 })
}.alignItems(HorizontalAlign.End)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(14)
Hero 卡片的下半部分是一个 Row,左右两端分别显示当前市价和倒计时。这种"价格 + 时间"的双信息布局是限时抢购场景的经典设计——价格传达价值信息,倒计时制造紧迫感,两者共同促进用户的购买决策。
倒计时的 UI 实现是一个亮点。时、分、秒三个数字各自被包裹在一个带有 cardLight 背景色和 4 圆角的 Text 组件中,形成了"数字块"的视觉效果。三个数字块之间用红色冒号分隔,红色冒号与数字块的深色背景形成对比,使整个倒计时看起来像一个专业的计时器。
每个数字块的 padding 设置为 { left: 6, right: 6, top: 3, bottom: 3 },确保数字在块内有足够的留白。Text(countdownHours(this.flashSec)) 的调用方式使每秒的 flashSec 状态更新都会触发这三个 Text 组件的内容刷新,这是 @State 响应式机制的直接体现。
9.3 Hero 卡片的抢购按钮
Button() {
Text('立即抢购').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
}
.width('90%').height(42).borderRadius(21)
.backgroundColor(COLORS.accent)
.margin({ top: 4, bottom: 14 })
.onClick(() => {
this.listingTargetId = 2
this.activeModal = 2
})
"立即抢购"按钮使用 Button 组件包裹一个 Text 组件。按钮宽度为 90%(居中显示),高度 42,圆角 21(完全圆角),背景色为强调红色。onClick 回调设置了两个状态——listingTargetId = 2(将挂售目标设为 Travis Scott x AJ1)和 activeModal = 2(显示挂售弹框),实现了从"抢购入口"到"挂售弹框"的跳转流程。
9.4 商品 Feed 列表
// 商品 Feed 标题
Row() {
Text('热门球鞋').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('查看全部 >').fontSize(11).fontColor(COLORS.textSecondary)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 16, bottom: 4 })
// 商品 Feed 大卡
ForEach(SNEAKERS, (s: Sneaker) => {
Column() {
Row() {
Column() {
Text(s.emoji).fontSize(40)
}
.width(90).height(90)
.borderRadius(14)
.backgroundColor(s.colorDeep)
.justifyContent(FlexAlign.Center)
Column() {
Text(s.brand).fontSize(10).fontColor(COLORS.accent)
Text(s.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 2 })
Text(s.series + ' · ' + s.releaseDate).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
Row() {
ForEach(s.tags, (tag: string) => {
Text(tag).fontSize(9).fontColor(COLORS.gold)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(COLORS.bg)
.margin({ right: 4 })
}, (tag: string) => tag)
}.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
商品 Feed 列表是热卖页面的主体内容,使用 ForEach 遍历 SNEAKERS 数组,为每款球鞋生成一个商品卡片。每个卡片的上半部分是一个 Row,左侧是 90x90 的球鞋图标区域(背景色为 colorDeep),右侧是商品信息列。
商品信息列的设计层次分明:品牌名(10px 红色)作为顶部标签,商品全名(14px 加粗白色)作为主标题,系列名 + 发售日期(10px 中灰色)作为副标题,标签数组(9px 金色徽章)作为底部装饰。maxLines(1) 和 textOverflow({ overflow: TextOverflow.Ellipsis }) 确保商品名过长时只显示一行并以省略号结尾,防止单个卡片占用过多垂直空间。
layoutWeight(1) 是 ArkTS 布局系统中的一个重要属性,它类似于 CSS 的 flex: 1。在商品卡片的 Row 中,左侧的图标区域有固定宽度 90,右侧的信息列使用 layoutWeight(1) 填充剩余空间。这种"固定 + 弹性"的组合布局确保了图标区域大小一致,而信息区域能够自适应不同屏幕宽度。
9.5 商品卡片的价格与操作区
Row() {
Column() {
Text(formatPrice(s.price)).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('发售价 ' + formatPrice(s.retailPrice)).fontSize(9).fontColor(COLORS.textDim).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start)
Row() {
Text(trendSymbol(s.trend)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(trendColor(s.trend))
Text((s.changePercent > 0 ? '+' : '') + s.changePercent.toFixed(1) + '%')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor(trendColor(s.trend))
if (s.hot) {
Text('HOT').fontSize(9).fontColor(COLORS.textPrimary)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(8).backgroundColor(COLORS.accent)
.margin({ left: 8 })
}
}
Button() {
Text('买入').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
}
.width(64).height(30).borderRadius(15)
.backgroundColor(COLORS.cardLight)
.margin({ left: 10 })
.onClick(() => {
this.listingTargetId = s.id
this.activeModal = 2
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Bottom)
.margin({ top: 10 })
商品卡片的下半部分是价格和操作区域。左侧显示当前市价(18px 加粗白色)和发售价(9px 深灰色),中间显示趋势箭头和百分比变化(13px,颜色由 trendColor 函数决定),右侧是"买入"按钮。
alignItems(VerticalAlign.Bottom) 使这一行的所有子元素在垂直方向上底部对齐,这样不同高度的内容(如价格列有两行文字,而趋势行只有一行)会以底部为基准对齐,视觉上更加协调。
"买入"按钮的 onClick 回调将 listingTargetId 设置为当前商品的 ID,然后显示挂售弹框(activeModal = 2)。这里的 s.id 来自 ForEach 的闭包捕获,确保每个按钮都能正确设置对应商品的 ID。
if (s.hot) 条件渲染只在 s.hot 为 true 时显示"HOT"徽章。这是 ArkTS 声明式 UI 中条件渲染的基本用法——当条件为 false 时,对应的 UI 节点不会被创建,也不会占用布局空间。
十、鉴定页面组件
10.1 鉴定流程时间线
@Builder TabAuth() {
Scroll() {
Column() {
// 鉴定流程时间线
Column() {
Text('鉴定流程').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Row() {
Column() {
Text('1').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.width(36).height(36).borderRadius(18)
.backgroundColor(COLORS.accent).textAlign(TextAlign.Center)
Text('提交鉴定').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 6 })
Text('拍照上传球鞋').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.width('30%').alignItems(HorizontalAlign.Center)
Column() {
Text('2').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.width(36).height(36).borderRadius(18)
.backgroundColor(COLORS.cardLight).textAlign(TextAlign.Center)
Text('专业检测').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 6 })
Text('平台鉴定师').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.width('30%').alignItems(HorizontalAlign.Center)
Column() {
Text('3').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.width(36).height(36).borderRadius(18)
.backgroundColor(COLORS.cardLight).textAlign(TextAlign.Center)
Text('出具报告').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 6 })
Text('24小时内').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.width('30%').alignItems(HorizontalAlign.Center)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 14 })
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
鉴定页面的顶部是一个三步流程时间线,向用户展示鉴定的完整流程。三个步骤(提交鉴定、专业检测、出具报告)被横向排列在一个 Row 中,每个步骤占 30% 的宽度,居中对齐。
每个步骤的编号(1、2、3)被放在一个 36x36 的圆形容器中。第一个步骤的圆形使用强调红色背景(COLORS.accent),表示当前所在步骤;第二和第三个步骤使用 cardLight 背景,表示尚未到达的步骤。这种通过颜色区分步骤状态的设计是进度指示器的常见模式。
每个步骤圆形下方有两行文字——步骤名称(13px 加粗白色)和步骤说明(10px 中灰色)。这种"编号 + 标题 + 描述"的三层信息结构使每个步骤的含义清晰明了。
10.2 鉴定提交按钮
Button() {
Text('📷 提交鉴定').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
}
.width('100%').height(46).borderRadius(23)
.backgroundColor(COLORS.accent)
.margin({ top: 12 })
.onClick(() => {
this.activeModal = 1
})
"提交鉴定"按钮使用全宽设计(width('100%')),高度 46,圆角 23(完全圆角),强调红色背景。按钮文本中包含了相机 emoji “📷”,直观地暗示了提交鉴定需要拍照的操作。onClick 回调将 activeModal 设置为 1,显示鉴定提交弹框。
10.3 鉴定记录列表
// 鉴定记录
Row() {
Text('最近鉴定').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('共 23 次 · 通过率 91%').fontSize(10).fontColor(COLORS.textSecondary)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 16, bottom: 4 })
ForEach(AUTH_RECORDS, (r: AuthRecord) => {
Column() {
Row() {
Column() {
Text(r.brand.charAt(0)).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
.width(44).height(44).borderRadius(12)
.backgroundColor(COLORS.cardLight).textAlign(TextAlign.Center)
}
Column() {
Text(r.sneakerName).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(r.submitDate + ' · ' + r.channel + ' · ' + formatPrice(r.price))
.fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 3 })
Text(r.result).fontSize(10).fontColor(COLORS.textDim).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(r.status).fontSize(11).fontWeight(FontWeight.Bold).fontColor(statusColor(r.status))
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(COLORS.bg)
}
}
.width('100%')
}
.width('100%')
.borderRadius(14)
.backgroundColor(COLORS.card)
.padding(12)
.margin({ top: 8 })
}, (r: AuthRecord) => r.id.toString())
鉴定记录列表使用 ForEach 遍历 AUTH_RECORDS 数组,每条记录显示为一个卡片。每个卡片的布局是"左图标 + 中信息 + 右状态"的三栏结构。
左侧的图标区域使用品牌名的首字母(r.brand.charAt(0))作为显示内容,放在一个 44x44 的圆角方形容器中。这种"首字母头像"的设计在缺少真实图片资源时是一个优雅的替代方案,它比使用通用图标更具辨识度。
中间的信息区域包含三行文字:球鞋名称(13px 加粗白色)、日期 + 渠道 + 价格(10px 中灰色,用"·"分隔)和鉴定结果(10px 深灰色)。三行文字的信息密度较高,但通过字号和颜色的差异化使信息层级清晰。
右侧的状态标签使用 statusColor 函数获取对应的颜色,以 11px 加粗显示,带有 bg 背景色和 10 的圆角。状态标签的设计使其在卡片中作为一个独立的视觉元素存在,用户可以通过颜色快速扫描鉴定结果。
十一、行情页面组件
11.1 引力指数卡
@Builder TabMarket() {
Scroll() {
Column() {
// 指数卡
Row() {
Column() {
Text('引力指数').fontSize(11).fontColor(COLORS.textSecondary)
Text('1286.5').fontSize(26).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 4 })
}.alignItems(HorizontalAlign.Start)
Column() {
Row() {
Text('↑').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
Text('+2.34%').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
}
Text('较昨日 +29.4 点').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 3 })
}.alignItems(HorizontalAlign.End)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(16)
.margin({ top: 10 })
行情页面的顶部是"引力指数"卡片,这是平台的综合市场指数,类似于股票市场的上证指数。卡片左侧显示指数名称和当前值(26px 加粗白色,是页面中最大的数字字号),右侧显示涨跌幅(16px 绿色加粗)和涨跌点数(10px 中灰色)。
这个卡片的设计模仿了金融行情应用的指数展示——大字号数值是视觉焦点,涨跌幅以颜色(绿色表示上涨)传达趋势信息,涨跌点数提供精确的数值参考。justifyContent(FlexAlign.SpaceBetween) 使左右两部分分别贴向卡片两端,形成了"标签 + 数值"的经典金融数据布局。
11.2 走势折线图区域
// 走势折线图
Column() {
Row() {
Text('AJ1 芝加哥 · 7日走势').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('2199').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Canvas(this.lineCtx)
.width('100%')
.height(190)
.margin({ top: 8 })
.onReady(() => {
drawLineChart(this.lineCtx, PRICE_TREND)
})
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
走势折线图区域是行情页面的核心组件之一。它由标题行和 Canvas 组件组成。标题行左侧显示商品名和时间范围,右侧显示当前价格(强调红色)。
Canvas(this.lineCtx) 组件接收之前在组件成员变量中创建的 lineCtx 作为绘制上下文。onReady 回调在 Canvas 组件准备就绪后被调用,此时执行 drawLineChart(this.lineCtx, PRICE_TREND) 进行图表绘制。这种"Canvas + onReady + 纯函数"的架构是 HarmonyOS ArkTS API 24 中实现自定义图形绘制的标准模式——Canvas 组件负责提供绘制表面和上下文管理,纯函数负责具体的绘制逻辑,onReady 回调负责在正确的时机触发绘制。
11.3 涨幅榜与跌幅榜
// 涨幅榜 / 跌幅榜
Row() {
Column() {
Row() {
Text('📈 涨幅榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
}.width('100%')
ForEach([SNEAKERS[7], SNEAKERS[12], SNEAKERS[9], SNEAKERS[0], SNEAKERS[5]], (s: Sneaker) => {
Row() {
Text(s.emoji).fontSize(16)
Text(s.name).fontSize(11).fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1).margin({ left: 8 })
Text('+' + s.changePercent.toFixed(1) + '%').fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
}
.width('100%')
.padding({ top: 7, bottom: 7 })
}, (s: Sneaker) => 'up' + s.id.toString())
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column()
.width(0.5)
.height(200)
.backgroundColor(COLORS.border)
.margin({ left: 10, right: 10 })
Column() {
Row() {
Text('📉 跌幅榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.danger)
}.width('100%')
ForEach([SNEAKERS[3], SNEAKERS[13], SNEAKERS[8], SNEAKERS[6], SNEAKERS[2]], (s: Sneaker) => {
Row() {
Text(s.emoji).fontSize(16)
Text(s.name).fontSize(11).fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1).margin({ left: 8 })
Text(s.changePercent.toFixed(1) + '%').fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.danger)
}
.width('100%')
.padding({ top: 7, bottom: 7 })
}, (s: Sneaker) => 'down' + s.id.toString())
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
.alignItems(VerticalAlign.Top)
涨幅榜和跌幅榜被并排排列在一个 Row 中,中间用一个 0.5 宽、200 高的分隔线隔开。两个榜单各占 layoutWeight(1) 的等宽空间,alignItems(VerticalAlign.Top) 确保两个列从顶部开始对齐。
涨幅榜通过 [SNEAKERS[7], SNEAKERS[12], SNEAKERS[9], SNEAKERS[0], SNEAKERS[5]] 手动指定了五款涨幅最高的球鞋(按 changePercent 降序排列),跌幅榜通过 [SNEAKERS[3], SNEAKERS[13], SNEAKERS[8], SNEAKERS[6], SNEAKERS[2]] 指定了五款跌幅最大的球鞋。这种手动指定索引的方式虽然在灵活性上不如动态排序,但在静态数据场景下更加直观,且避免了运行时排序的计算开销。
ForEach 的键值生成函数使用了 'up' + s.id.toString() 和 'down' + s.id.toString() 的前缀区分,这是因为同一款球鞋可能同时出现在涨幅榜和跌幅榜的 ForEach 中(虽然实际上不会,但键值唯一性是 ForEach 的要求),添加前缀确保了键值的唯一性。
11.4 成交量柱状图
// 成交量柱状图
Column() {
Row() {
Text('全站成交量 · 7日').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('今日 3560 双').fontSize(11).fontColor(COLORS.gold)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Canvas(this.barCtx)
.width('100%')
.height(150)
.margin({ top: 8 })
.onReady(() => {
drawBarChart(this.barCtx, VOLUME_BARS)
})
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
成交量柱状图区域的结构与折线图区域类似,但使用了 barCtx 上下文和 drawBarChart 函数。Canvas 高度为 150(比折线图的 190 矮),这是因为柱状图的视觉信息密度较低,不需要太多垂直空间。标题行右侧的"今日 3560 双"使用金色,与柱状图的金色柱子形成呼应。
十二、收藏页面组件
12.1 CollectionCard 子构建器
@Builder CollectionCard(item: CollectionItem) {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
Text(item.emoji).fontSize(44)
}
.width('100%').height(100)
.justifyContent(FlexAlign.Center)
.borderRadius({ topLeft: 14, topRight: 14 })
.backgroundColor(item.color)
if (item.liked) {
Text('❤️').fontSize(14).margin(8)
} else {
Text('🤍').fontSize(14).margin(8)
}
}
.width('100%')
CollectionCard 是一个接收参数的 @Builder 函数,它定义了单个收藏卡片的 UI 结构。这种参数化构建器是 ArkTS 中实现 UI 组件复用的关键机制——通过将卡片 UI 抽取为独立的构建器,收藏页面可以在双列布局中两次调用 this.CollectionCard(pair[0]) 和 this.CollectionCard(pair[1]) 来渲染每对收藏项。
卡片顶部的球鞋展示区域使用 Stack 容器实现层叠布局。Stack({ alignContent: Alignment.TopEnd }) 设置子元素的对齐方式为右上角,使得爱心 emoji 被放置在展示区域的右上角。展示区域本身是一个 100 高的 Column,背景色为球鞋的 color 值,内部居中放置一个 44px 的球鞋 emoji。
爱心图标使用条件渲染——item.liked 为 true 时显示红心 emoji(❤️),为 false 时显示空心 emoji(🤍)。这种通过数据驱动图标变化的实现方式是声明式 UI 的典型应用。
12.2 收藏卡片的详情区域
Column() {
Text(item.brand).fontSize(9).fontColor(COLORS.accent)
Text(item.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 2 })
Text('尺码 US ' + item.size + ' (EU 26cm)').fontSize(9).fontColor(COLORS.textSecondary).margin({ top: 2 })
Row() {
Column() {
Text('入手 ' + formatPrice(item.acquiredPrice)).fontSize(9).fontColor(COLORS.textDim)
Text('现价 ' + formatPrice(item.currentPrice)).fontSize(9).fontColor(COLORS.textSecondary).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text((item.profit > 0 ? '+' : '') + formatPrice(item.profit))
.fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(item.profit >= 0 ? COLORS.success : COLORS.danger)
Text((item.profitPercent > 0 ? '+' : '') + item.profitPercent.toFixed(1) + '%')
.fontSize(9).fontColor(item.profitPercent >= 0 ? COLORS.success : COLORS.danger).margin({ top: 1 })
}.alignItems(HorizontalAlign.End)
}
.width('100%')
.margin({ top: 6 })
Button() {
Text('移除收藏').fontSize(10).fontColor(COLORS.textSecondary)
}
.width('100%').height(26).borderRadius(13)
.backgroundColor(COLORS.cardLight)
.margin({ top: 8 })
.onClick(() => {
this.delTargetId = item.id
this.activeModal = 4
})
}
.width('100%')
.padding(10)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.borderRadius(14)
.backgroundColor(COLORS.card)
.clip(true)
}
收藏卡片的详情区域包含了丰富的信息:品牌名、商品名、尺码、入手价、现价和盈亏数据。盈亏数据的颜色通过三元运算符动态决定——盈利为绿色(COLORS.success),亏损为红色(COLORS.danger),这是金融数据展示的标准色彩约定。
"移除收藏"按钮位于卡片底部,使用全宽设计和 cardLight 背景色(而非强调红色,因为"移除"是一个次要操作,不应过于醒目)。onClick 回调设置 delTargetId 为当前收藏项的 ID,然后显示删除确认弹框(activeModal = 4)。
.clip(true) 属性是卡片实现的关键——它启用了裁剪,使得顶部球鞋展示区域的圆角(borderRadius({ topLeft: 14, topRight: 14 }))能够正确显示。如果不设置 clip(true),内部 Column 的方形背景色会溢出到外层 Column 的圆角范围之外,导致圆角失效。
12.3 收藏概览与双列布局
@Builder TabCollection() {
Scroll() {
Column() {
// 收藏概览
Row() {
Column() {
Text('13').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('在藏').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}.layoutWeight(1)
Column() {
Text('¥42,606').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('总市值').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}.layoutWeight(1)
Column() {
Text('+¥2,347').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
Text('总浮盈').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}.layoutWeight(1)
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
.justifyContent(FlexAlign.Center)
收藏页面顶部是一个三栏概览卡片,分别显示在藏数量(13)、总市值(¥42,606)和总浮盈(+¥2,347)。三个数据项各占 layoutWeight(1) 的等宽空间,justifyContent(FlexAlign.Center) 使它们均匀分布。总浮盈使用绿色,与商品卡片中的盈利颜色保持一致。
12.4 尺码偏好入口
// 尺码偏好入口
Row() {
Text('📏').fontSize(20)
Column() {
Text('尺码偏好').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('常穿 41-43 · 关注 2 个品牌').fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('编辑 >').fontSize(11).fontColor(COLORS.accent)
}
.width('100%')
.borderRadius(14)
.backgroundColor(COLORS.card)
.padding(12)
.margin({ top: 10 })
.onClick(() => {
this.activeModal = 3
})
尺码偏好入口是一个可点击的 Row,点击后显示尺码偏好编辑弹框(activeModal = 3)。这个入口的设计是"图标 + 信息 + 操作"的典型列表项布局——左侧的尺码 emoji 作为视觉标识,中间的标题和说明提供信息,右侧的"编辑 >"用强调红色引导用户操作。
12.5 双列网格布局
Text('我的鞋柜').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.width('100%').margin({ top: 16, bottom: 4 })
ForEach(chunkPairs(COLLECTIONS), (pair: CollectionItem[]) => {
Row() {
Column() {
this.CollectionCard(pair[0])
}.layoutWeight(1)
Column()
.width(10)
.height(10)
if (pair.length > 1) {
Column() {
this.CollectionCard(pair[1])
}.layoutWeight(1)
} else {
Column().layoutWeight(1)
}
}
.width('100%')
.margin({ top: 10 })
}, (pair: CollectionItem[]) => pair[0].id.toString() + '_' + pair.length.toString())
收藏列表的双列网格布局通过 chunkPairs 函数和 ForEach 的配合实现。chunkPairs(COLLECTIONS) 将 13 个收藏项分组为 7 个"对"(6 对 + 1 单),每个"对"被渲染为一个包含两个 Column(各占 layoutWeight(1))的 Row。
当"对"只有一个元素时(即最后一个奇数项),第二个 Column 位置被一个空的 Column().layoutWeight(1) 占位,确保布局对齐。这种处理方式确保了即使最后一行只有一个卡片,它也会占据左半列的位置,右半列保持空白,而不会出现卡片拉伸到全宽或左移的情况。
两个 Column 之间有一个 10 宽、10 高的间隔 Column,它在视觉上为两个卡片之间提供了间距。ForEach 的键值生成函数使用 pair[0].id.toString() + '_' + pair.length.toString(),将第一个元素的 ID 和数组长度的组合作为唯一键,确保了键值的唯一性。
十三、排行页面组件
13.1 前三名领奖台
@Builder TabRank() {
Scroll() {
Column() {
Text('交易达人榜 · 第35周').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.width('100%').margin({ top: 10 })
// 前三名领奖台
Row() {
Column() {
Text('🥈').fontSize(40).margin({ top: 8 })
Text('球鞋猎手Max').fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.maxLines(1).margin({ top: 4 })
Text('🎯').fontSize(16).margin({ top: 2 })
Text(formatVolume(RANKINGS[1].volume)).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor('#C0C0C0').margin({ top: 2 })
}
.width('30%')
.height(130)
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor(COLORS.cardLight)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
.padding({ bottom: 8 })
Column() {
Text('👑').fontSize(14)
Text('🥇').fontSize(52).margin({ top: 2 })
Text('SneakerKing_钉子').fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
.maxLines(1).margin({ top: 4 })
Text(formatVolume(RANKINGS[0].volume)).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(COLORS.gold).margin({ top: 2 })
}
.width('34%')
.height(160)
.borderRadius({ topLeft: 14, topRight: 14 })
.backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
.padding({ bottom: 8 })
.margin({ left: 4, right: 4 })
Column() {
Text('🥉').fontSize(36).margin({ top: 8 })
Text('东城老K').fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.maxLines(1).margin({ top: 4 })
Text('🧢').fontSize(16).margin({ top: 2 })
Text(formatVolume(RANKINGS[2].volume)).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor('#CD7F32').margin({ top: 2 })
}
.width('30%')
.height(115)
.borderRadius({ topLeft: 12, topRight: 12 })
.backgroundColor(COLORS.cardLight)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
.padding({ bottom: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 12 })
排行榜页面的视觉焦点是前三名领奖台,它采用了经典的"银-金-铜"布局——亚军在左(130 高,银色 #C0C0C0 文字),冠军在中(160 高,金色文字),季军在右(115 高,铜色 #CD7F32 文字)。三个柱子的高度差异(130/160/115)模拟了真实领奖台的高低排列,冠军最高、季军最矮。
冠军柱使用了 card 背景色(比亚军的 cardLight 更亮),并在顶部添加了皇冠 emoji,进一步突出第一名的特殊地位。冠军的金色 emoji(🥇)使用 52 的字号,比亚军(40)和季军(36)更大。所有三个柱子都使用 justifyContent(FlexAlign.End) 使内容底部对齐,borderRadius 只设置顶部圆角,模拟真实奖台的形状。
13.2 排名列表
// 排名列表
ForEach(RANKINGS, (r: RankItem, idx: number) => {
Row() {
Text(idx < 3 ? ' ' : idx.toString())
.fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? COLORS.gold : COLORS.textDim)
.width(28)
.textAlign(TextAlign.Center)
Text(r.avatar).fontSize(24)
.width(40).height(40).borderRadius(20)
.backgroundColor(COLORS.cardLight)
.textAlign(TextAlign.Center)
Column() {
Text(r.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(r.badge).fontSize(8).fontColor(COLORS.accent)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(6).backgroundColor(COLORS.bg)
Text(r.tradeCount.toString() + ' 笔成交').fontSize(9).fontColor(COLORS.textSecondary)
.margin({ left: 5 })
}.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(formatVolume(r.volume)).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('盈利 ' + formatVolume(r.profit)).fontSize(9).fontColor(COLORS.success).margin({ top: 2 })
}.alignItems(HorizontalAlign.End)
}
.width('100%')
.borderRadius(14)
.backgroundColor(COLORS.card)
.padding(10)
.margin({ top: 8 })
.alignItems(VerticalAlign.Center)
}, (r: RankItem) => r.id.toString())
排名列表使用 ForEach 遍历 RANKINGS 数组,每条记录显示为一个卡片。ForEach 的第二个参数 (r: RankItem, idx: number) 提供了当前项的索引,用于显示排名序号。
排名序号的显示逻辑值得分析:idx < 3 ? ' ' : idx.toString()——前三名显示空格(因为前三名已经在领奖台区域展示),第四名及以后显示数字序号。颜色也做了区分——前三名显示金色(虽然实际显示为空格,但颜色设置确保了一致性),第四名及以后显示深灰色。这种设计避免了前三名在领奖台和列表中的重复展示,使列表更紧凑。
每个列表项的布局是"序号 + 头像 + 信息 + 数据"的四栏结构。头像使用 24px 的 emoji,放在一个 40x40 的圆形容器中。信息区域包含用户名和徽章 + 成交笔数。数据区域显示交易额(12px 加粗白色)和盈利额(9px 绿色)。
十四、我的页面组件
14.1 个人资料卡
@Builder TabMine() {
Scroll() {
Column() {
// 个人资料卡
Row() {
Text(USER_PROFILE.avatar).fontSize(36)
.width(64).height(64).borderRadius(32)
.backgroundColor(COLORS.cardLight)
.textAlign(TextAlign.Center)
Column() {
Row() {
Text(USER_PROFILE.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
if (USER_PROFILE.verified) {
Text('✔ 认证').fontSize(8).fontColor(COLORS.textPrimary)
.padding({ left: 5, right: 5, top: 2, bottom: 2 })
.borderRadius(6).backgroundColor(COLORS.accent)
.margin({ left: 6 })
}
}
Text(USER_PROFILE.level).fontSize(10).fontColor(COLORS.gold).margin({ top: 4 })
Row() {
Text('关注 ' + USER_PROFILE.following.toString()).fontSize(10).fontColor(COLORS.textSecondary)
Text('粉丝 ' + USER_PROFILE.followers.toString()).fontSize(10).fontColor(COLORS.textSecondary)
.margin({ left: 12 })
Text('成交 ' + USER_PROFILE.trades.toString()).fontSize(10).fontColor(COLORS.textSecondary)
.margin({ left: 12 })
}.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
"我的"页面顶部是个人资料卡,采用"左头像 + 右信息"的布局。头像使用 36px 的 emoji,放在一个 64x64 的圆形容器中(borderRadius(32) 实现完全圆形)。
信息区域包含三层信息:用户名(16px 加粗白色)和认证标识(条件渲染,8px 白色文字 + 强调红色背景)、等级(10px 金色)、社交数据(关注/粉丝/成交,10px 中灰色)。认证标识使用 if (USER_PROFILE.verified) 条件渲染,只有当 verified 为 true 时才显示"✔ 认证"徽章。这种基于数据驱动 UI 显示的方式是声明式编程的核心优势。
14.2 钱包卡片
// 钱包卡
Column() {
Text('钱包余额 (CNY)').fontSize(10).fontColor(COLORS.textSecondary)
Text(formatPrice(WALLET.balance)).fontSize(28).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.margin({ top: 6 })
Row() {
Column() {
Text('本月收入').fontSize(9).fontColor(COLORS.textSecondary)
Text(formatPrice(WALLET.income)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('本月支出').fontSize(9).fontColor(COLORS.textSecondary)
Text(formatPrice(WALLET.expense)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.danger)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('冻结金额').fontSize(9).fontColor(COLORS.textSecondary)
Text(formatPrice(WALLET.frozen)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%')
.margin({ top: 14 })
Button() {
Text('提现').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
}
.width('100%').height(40).borderRadius(20)
.backgroundColor(COLORS.cardLight)
.margin({ top: 14 })
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(16)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
钱包卡片的设计分为三个层次:余额、收支明细和提现按钮。余额使用 28px 加粗白色——这是整个"我的"页面中最大的字号,突出了钱包余额的核心信息地位。收支明细使用三栏布局,收入为绿色、支出为红色、冻结为金色,三种颜色的语义化设计使用户能够快速识别资金的性质。
提现按钮使用 cardLight 背景色而非强调红色,这是因为"提现"是一个常规操作而非核心转化操作。在 UI 设计中,按钮的颜色应该与其重要性相匹配——如果所有按钮都使用强调色,用户的视觉焦点会被分散,无法识别哪个操作最重要。
14.3 交易统计与在售列表
// 交易统计
Row() {
ForEach(TRANS_STATS, (st: StatBlock) => {
Column() {
Text(st.label).fontSize(9).fontColor(COLORS.textSecondary)
Text(st.value).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).margin({ top: 3 })
Text(st.sub).fontSize(8).fontColor(COLORS.textDim).margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (st: StatBlock) => st.label)
}
.width('100%')
.borderRadius(16)
.backgroundColor(COLORS.card)
.padding(14)
.margin({ top: 10 })
交易统计区域使用 ForEach 遍历 TRANS_STATS 数组,生成四个等宽的统计块。每个统计块包含标签(9px 中灰色)、值(15px 加粗白色)和子说明(8px 深灰色),三层信息的字号和颜色差异化使信息层级清晰。使用 layoutWeight(1) 和 alignItems(HorizontalAlign.Center) 确保四个统计块等宽且内容居中。
// 我的在售
Row() {
Text('我的在售 (5)').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('管理 >').fontSize(11).fontColor(COLORS.textSecondary)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 16, bottom: 4 })
ForEach(COLLECTIONS, (item: CollectionItem) => {
if (item.id <= 5) {
Row() {
Text(item.emoji).fontSize(28)
.width(52).height(52).borderRadius(12)
.backgroundColor(COLORS.cardLight)
.textAlign(TextAlign.Center)
Column() {
Text(item.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('US ' + item.size + ' · ' + item.brand).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 2 })
Text('挂售价 ' + formatPrice(item.currentPrice + 200)).fontSize(10).fontColor(COLORS.gold).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text('在售').fontSize(9).fontColor(COLORS.success)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8).backgroundColor(COLORS.bg)
Text('12人想要').fontSize(8).fontColor(COLORS.textDim).margin({ top: 3 })
}.alignItems(HorizontalAlign.End)
}
.width('100%')
.borderRadius(14)
.backgroundColor(COLORS.card)
.padding(10)
.margin({ top: 8 })
}
}, (item: CollectionItem) => 'mine' + item.id.toString())
"我的在售"列表使用 ForEach 遍历 COLLECTIONS 数组,但通过 if (item.id <= 5) 条件只显示前 5 个收藏项。这种"遍历全部但条件渲染部分"的模式在 ArkTS 中很常见,它比先过滤数组再遍历的方式更符合声明式 UI 的思维方式。每个在售项的挂售价通过 item.currentPrice + 200 计算——在当前估值基础上加 200 元作为挂售价,这是一个简单的定价策略模拟。
十五、弹框系统
15.1 弹框工具方法
togglePrefSize(v: number): void {
if (this.prefSizes.indexOf(v) >= 0) {
const next: number[] = []
for (let i = 0; i < this.prefSizes.length; i++) {
if (this.prefSizes[i] !== v) {
next.push(this.prefSizes[i])
}
}
this.prefSizes = next
} else {
const next: number[] = this.prefSizes.slice()
next.push(v)
this.prefSizes = next
}
}
togglePrefBrand(b: string): void {
if (this.prefBrands.indexOf(b) >= 0) {
const next: string[] = []
for (let i = 0; i < this.prefBrands.length; i++) {
if (this.prefBrands[i] !== b) {
next.push(this.prefBrands[i])
}
}
this.prefBrands = next
} else {
const next: string[] = this.prefBrands.slice()
next.push(b)
this.prefBrands = next
}
}
togglePrefSize 和 togglePrefBrand 是两个多选切换方法,分别用于尺码偏好和品牌偏好的多选交互。这两个方法的逻辑相同:如果值已在数组中,则移除它;如果值不在数组中,则添加它。
这两个方法的实现有一个关键细节——它们都创建了新数组而非修改原数组。当移除值时,方法遍历原数组并将不等于目标值的元素放入新数组;当添加值时,方法使用 slice() 复制原数组,然后 push 新值。这种"创建新数组"的方式是 ArkTS @State 的要求——直接修改数组(如 splice 或 push)不会触发 UI 更新,必须将整个数组重新赋值给 @State 变量才能触发响应式更新。
15.2 弹框覆盖层与手柄
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.6)').onClick(() => {
onClose()
})
}
@Builder SheetHandle() {
Column()
.width(40).height(4).borderRadius(2)
.backgroundColor(COLORS.border)
.margin({ top: 10, bottom: 12 })
}
modalOverlay 是弹框的半透明遮罩层构建器,它接收一个 onClose 回调函数作为参数。这个构建器展示了 ArkTS 中构建器参数化的高级用法——通过将关闭逻辑作为回调函数传入,同一个 modalOverlay 可以被不同的弹框复用,每个弹框传入自己的关闭逻辑。遮罩层使用 rgba(0,0,0,0.6) 设置 60% 透明度的黑色背景,点击遮罩层会触发 onClose 回调关闭弹框。
SheetHandle 是底部弹框顶部的拖拽手柄,一个 40 宽、4 高、圆角 2 的灰色小条。这个视觉元素是 iOS 和 Material Design 中底部弹框的标准组件,它向用户传达了"这是可滑动的底部面板"的交互暗示。
15.3 鉴定提交弹框
@Builder ModalAuthSubmit() {
Stack({ alignContent: Alignment.Bottom }) {
this.modalOverlay(() => {
this.activeModal = 0
})
Column() {
this.SheetHandle()
Text('提交鉴定').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
.width('100%')
}
.width('100%')
.height('100%')
}
删除弹框的按钮组采用"取消 + 确认"的双按钮设计。取消按钮使用 cardLight 背景和中灰色文字,确认按钮使用危险红色背景和白色文字。两个按钮各占 layoutWeight(1) 的等宽空间,中间通过 margin 的 right: 10 和 left: 10 形成 20 的间距。两个按钮的 onClick 回调都将 activeModal 设置为 0,关闭弹框。在实际应用中,确认按钮还需要执行实际的删除逻辑(如从 COLLECTIONS 数组中移除对应项),但在这个展示型页面中,仅关闭弹框。
十六、漂浮表情层
16.1 FloatLayer 构建
@Builder FloatLayer() {
Column() {
ForEach(this.floatEmojis, (f: FloatEmoji) => {
Text(f.emoji)
.fontSize(floatSize(f.seed))
.opacity(floatOpacity(f.y))
.position({ x: (floatX(f.seed) + floatWobble(f.seed) * 1.5) + '%', y: f.y + '%' })
}, (f: FloatEmoji) => f.seed.toString() + f.y.toString())
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.None)
}
FloatLayer 是漂浮表情动画层的构建器,它使用 ForEach 遍历 this.floatEmojis 状态数组,为每个表情生成一个 Text 组件。每个表情的视觉属性通过之前定义的纯函数计算——floatSize(f.seed) 计算字体大小,floatOpacity(f.y) 计算透明度,floatX(f.seed) + floatWobble(f.seed) * 1.5 计算水平位置(百分比),f.y 计算垂直位置(百分比)。
position 属性使用百分比单位定位,使表情的位置相对于 FloatLayer 容器的尺寸。x 的计算结合了基础位置 floatX(f.seed) 和摆动偏移 floatWobble(f.seed) * 1.5,其中 floatWobble 返回 +1 或 -1,乘以 1.5 后产生微小的左右偏移,增加动画的自然感。
hitTestBehavior(HitTestMode.None) 是这个组件的关键设置——它禁用了漂浮表情层的触摸事件处理,使触摸事件能够穿透到下方的页面内容。如果没有这个设置,漂浮表情层会覆盖整个页面,拦截所有触摸事件,导致页面内容无法交互。HitTestMode.None 告诉框架这个组件不参与命中测试,触摸事件会自动传递给下方的组件。
ForEach 的键值生成函数 f.seed.toString() + f.y.toString() 将种子值和 y 位置组合作为键。由于 y 值每 60 毫秒变化一次,键值也会随之变化,这确保了 ForEach 在每次状态更新时都能正确识别变化并重新渲染。虽然频繁的键值变化可能导致 ForEach 的差分更新效率降低(因为几乎每个元素的键都变了),但在 12 个元素的小列表中,这种性能影响可以忽略。
十七、底部导航栏
17.1 BottomBar 构建
@Builder BottomBar() {
Column() {
Column()
.width('100%')
.height(0.5)
.backgroundColor(COLORS.border)
Row() {
Column() {
Text('🏠').fontSize(20).opacity(this.bottomTab === 0 ? 1 : 0.45)
Text('首页').fontSize(9)
.fontColor(this.bottomTab === 0 ? COLORS.textPrimary : COLORS.textDim)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 0
this.currentTab = 0
})
Column() {
Text('🔍').fontSize(20).opacity(this.bottomTab === 1 ? 1 : 0.45)
Text('鉴定').fontSize(9)
.fontColor(this.bottomTab === 1 ? COLORS.textPrimary : COLORS.textDim)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 1
this.currentTab = 1
})
底部导航栏使用 Column 容器,顶部有 0.5 高的分隔线,下方是一个 Row 包含 5 个导航按钮。每个按钮由一个 emoji 图标和文字标签组成,使用 Column 垂直排列。
激活状态和非激活状态的视觉差异通过两个属性实现:emoji 的 opacity(激活为 1,非激活为 0.45)和标签文字的 fontColor(激活为白色 textPrimary,非激活为深灰色 textDim)。这种"透明度 + 颜色"的双重差异化使激活状态更加突出,同时非激活状态通过降低透明度保持可见但不抢眼。
每个按钮的 onClick 回调同时设置 bottomTab 和 currentTab 两个状态。bottomTab 控制底部导航栏自身的激活状态,currentTab 控制内容区域的显示。这两个状态的同步设置确保了底部导航与内容区域的一致性——点击底部"首页"按钮时,底部导航高亮"首页"且内容区域显示热卖页面。
17.2 底部导航的 Tab 映射
Column() {
Text('💸').fontSize(20).opacity(this.bottomTab === 2 ? 1 : 0.45)
Text('交易').fontSize(9)
.fontColor(this.bottomTab === 2 ? COLORS.textPrimary : COLORS.textDim)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 2
this.currentTab = 2
})
Column() {
Text('⭐').fontSize(20).opacity(this.bottomTab === 3 ? 1 : 0.45)
Text('收藏').fontSize(9)
.fontColor(this.bottomTab === 3 ? COLORS.textPrimary : COLORS.textDim)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 3
this.currentTab = 3
})
Column() {
Text('👤').fontSize(20).opacity(this.bottomTab === 4 ? 1 : 0.45)
Text('我的').fontSize(9)
.fontColor(this.bottomTab === 4 ? COLORS.textPrimary : COLORS.textDim)
.margin({ top: 2 })
}
.layoutWeight(1)
.onClick(() => {
this.bottomTab = 4
this.currentTab = 5
})
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.backgroundColor(COLORS.card)
}
.width('100%')
}
底部导航栏的五个按钮分别映射到不同的内容 Tab:首页(bottomTab=0, currentTab=0)、鉴定(bottomTab=1, currentTab=1)、交易(bottomTab=2, currentTab=2)、收藏(bottomTab=3, currentTab=3)、我的(bottomTab=4, currentTab=5)。
特别值得注意的是"我的"按钮的映射——bottomTab = 4 但 currentTab = 5。这是因为底部导航有 5 个按钮(索引 0-4),而内容 Tab 栏有 6 个标签(索引 0-5),"排行"Tab(索引 4)只在内容栏中存在而不在底部导航栏中。当用户点击"我的"时,底部导航高亮第 5 个按钮(索引 4),但内容区域显示第 6 个 Tab(索引 5,即"我的"页面)。
底部导航栏的高度为 56,使用 card 背景色,justifyContent(FlexAlign.Center) 使 5 个按钮在水平方向上均匀分布。每个按钮使用 layoutWeight(1) 等宽占据可用空间。
十八、主构建方法
18.1 build 方法与 Stack 布局
build() {
Stack() {
Column() {
this.PageHeader()
this.TabsBar()
Column() {
if (this.currentTab === 0) {
this.TabHot()
} else if (this.currentTab === 1) {
this.TabAuth()
} else if (this.currentTab === 2) {
this.TabMarket()
} else if (this.currentTab === 3) {
this.TabCollection()
} else if (this.currentTab === 4) {
this.TabRank()
} else {
this.TabMine()
}
}
.width('100%')
.layoutWeight(1)
this.BottomBar()
}
.width('100%')
.height('100%')
this.FloatLayer()
if (this.activeModal === 1) {
this.ModalAuthSubmit()
}
if (this.activeModal === 2) {
this.ModalListing()
}
if (this.activeModal === 3) {
this.ModalSizePref()
}
if (this.activeModal === 4) {
this.ModalDelete()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}
build 方法是整个页面的组装中心,它使用 Stack 容器将页面内容、漂浮表情层和弹框层三个层次叠加在一起。Stack 的层叠顺序是从下到上——后添加的子元素覆盖在先添加的子元素之上。
第一层是页面主内容,使用 Column 垂直排列三个部分:顶部 PageHeader、内容 TabsBar 和中间的内容区域、底部 BottomBar。内容区域使用 if/else if/else 条件语句根据 currentTab 的值选择对应的 Tab 构建器进行渲染。layoutWeight(1) 使内容区域占据 PageHeader 和 BottomBar 之间的所有剩余空间。
第二层是 FloatLayer,覆盖在整个页面上方。由于设置了 hitTestBehavior(HitTestMode.None),漂浮表情层不会拦截触摸事件,用户可以正常与下方的页面内容交互。
第三层是弹框层,使用四个独立的 if 语句(而非 if/else if)根据 activeModal 的值决定显示哪个弹框。使用独立 if 而非 if/else if 的原因是为了灵活性——虽然当前设计确保了同一时间只有一个弹框显示,但使用独立 if 允许未来扩展为多弹框叠加的场景。
18.2 页面渲染流程
从上述时序图可以清晰地看到页面渲染的三种触发路径。第一种是用户交互触发——用户点击 Tab 标签或按钮,状态系统更新对应的 @State 变量,触发 build 方法重新执行,条件语句选择对应的内容构建器,渲染引擎进行差分更新。第二种是弹框显示触发——用户点击操作按钮,设置弹框状态和目标 ID,build 方法中的弹框条件判断触发对应弹框的渲染。第三种是定时器触发——漂浮表情定时器和倒计时定时器定期更新状态,触发 build 方法重新执行,ForEach 更新表情位置或倒计时数字。
这三种触发路径共同构成了页面的完整交互模型。基于 HarmonyOS API 24 的状态管理系统确保了每次状态变化都能精确地触发最小范围的 UI 更新——例如,flashSec 的每秒更新只会影响倒计时 Text 组件的内容,不会重新渲染整个页面;floatEmojis 的每 60 毫秒更新只会影响 FloatLayer 中的 ForEach 元素,不会波及页面内容区域。
十九、关键技术选型与设计模式分析
19.1 纯函数架构的设计优势
在本文分析的球鞋交易平台中,纯函数架构是最核心的设计决策之一。所有的业务逻辑——从趋势颜色计算到图表绘制——都被实现为不依赖组件状态的纯函数。这种设计选择带来了多方面的技术优势。
首先是可测试性。纯函数的输出完全由输入决定,不依赖任何外部状态或副作用,因此可以独立于 UI 进行单元测试。例如,trendColor('up') 总是返回 COLORS.success,formatPrice(3899) 总是返回 '¥3,899',这些函数的行为是确定性的,测试用例编写简单直观。在基于 HarmonyOS API 24 的开发中,这种可测试性尤为重要,因为 ArkTS 组件的 UI 逻辑通常难以直接进行单元测试,将逻辑抽离为纯函数后可以绕过这一限制。
其次是性能可预测性。纯函数没有副作用,不会修改任何外部状态,因此可以安全地进行缓存和并行执行。在图表绘制场景中,drawLineChart 和 drawBarChart 函数接收 Canvas 上下文和数据点数组,在函数内部完成所有绘制操作,不会修改组件状态。这种"输入到输出"的单向数据流使性能分析更加直观——函数的执行时间只与输入数据规模相关,不受组件状态变化的影响。
最后是代码复用性。纯函数可以在不同的组件中被调用,无需担心状态冲突。formatPrice 函数在热卖页面的商品卡片、收藏页面的盈亏显示、我的页面的钱包余额等数十处被调用,每次调用都返回正确格式化的字符串。如果价格格式化逻辑散落在各个组件中,不仅代码重复,而且格式不一致的风险极高。
19.2 @State 与响应式更新机制
ArkTS 的 @State 装饰器是声明式 UI 响应式更新的核心机制。当 @State 变量的值发生变化时,框架会自动检测依赖于该变量的 UI 部分,并精确地重新渲染这些部分。在球鞋交易平台中,这种机制体现在多个层面。
currentTab 状态的更新触发内容区域的条件渲染切换。当用户从"热卖"Tab 切换到"鉴定"Tab 时,currentTab 从 0 变为 1,build 方法中的 if/else if/else 链会重新执行,旧的 TabHot 构建器被卸载,新的 TabAuth 构建器被挂载。这个过程是增量的——只有内容区域被重新渲染,顶部的 PageHeader 和 TabsBar 不会受到影响。
floatEmojis 状态的更新驱动漂浮表情动画。每 60 毫秒,advanceFloat 函数返回一个新的 FloatEmoji[] 数组,赋值给 floatEmojis 后触发 FloatLayer 中 ForEach 的重新渲染。由于 ForEach 的键值包含了 y 位置,每个表情元素的键都发生了变化,ForEach 会为每个表情创建新的 UI 节点。虽然这在理论上比原地更新属性的开销更大,但在 12 个元素的小列表中,性能差异可以忽略不计。
flashSec 状态的更新驱动倒计时显示。每秒递减一次的 flashSec 触发 TabHot 中倒计时 Text 组件的内容更新。由于 flashSec 只被倒计时相关的 Text 组件引用,更新范围被精确限制在这几个组件上,不会导致整个 TabHot 的重新渲染。这种细粒度的状态依赖追踪是 HarmonyOS ArkTS API 24 渲染优化的关键。
19.3 单一弹框状态的设计考量
使用单一的 activeModal 状态变量控制所有弹框(而非为每个弹框使用独立的布尔状态)是一个值得深入分析的设计决策。这种设计有几个显著优势。
第一,它天然地保证了同一时间只有一个弹框处于显示状态。当 activeModal 的值为 2 时,只有挂售弹框显示,其他弹框的条件判断(activeModal === 1、activeModal === 3、activeModal === 4)都为 false。这种"互斥"特性不需要额外的逻辑来保证——数值类型的天然互斥性自动实现了这一约束。
第二,它简化了状态管理。如果有四个独立的布尔状态(如 showAuthModal、showListingModal、showSizePrefModal、showDeleteModal),在打开一个弹框时需要先将其他三个设为 false,这种手动互斥管理容易出错。使用单一数值状态,设置 activeModal = 2 自动关闭了所有其他弹框。
第三,它便于扩展。未来如果需要添加新的弹框类型,只需分配一个新的数值(如 activeModal = 5),在 build 方法中添加对应的 if 判断即可,不需要修改现有的状态定义。这种扩展方式符合开闭原则——对扩展开放,对修改封闭。
19.4 Canvas 绘图与第三方图表库的对比
在行情页面中,折线图和柱状图都是通过 Canvas 2D API 手动绘制的,而非使用第三方图表库。这个技术选型值得深入分析其利弊。
手动绘制的最大优势是包体积控制。第三方图表库(如 MPAndroidChart、Charts 等)通常引入数十 KB 甚至上百 KB 的代码量,而手动绘制只需要几十行代码。对于球鞋交易平台这样的应用,图表功能相对简单(折线图 + 柱状图),引入完整的图表库是不必要的开销。基于 HarmonyOS 6.1.1 系统的 Canvas 加速渲染能力,手动绘制的图表在性能上完全不逊色于第三方库。
另一个优势是定制灵活性。第三方图表库通常提供一套预设的样式和交互模式,深度定制往往需要覆盖大量默认配置,代码复杂度不亚于手动绘制。手动绘制则可以精确控制每一个像素——从网格线的颜色和位置,到数据点的半径和填充色,再到标签的字体和偏移量,都可以自由调整。
手动绘制的劣势在于开发成本和维护复杂度。绘制一个完整的图表需要处理坐标映射、刻度计算、抗锯齿、文本对齐等多个细节,这些在第三方库中通常是开箱即用的。但在本案例中,由于图表样式固定且数据量小(7 个数据点),手动绘制的开发成本完全可控。
二十、技术对比表格
以下表格对本文涉及的关键技术选型进行了横向对比,帮助读者理解不同方案之间的权衡。
20.1 声明式 UI 与命令式 UI 对比
| 对比维度 | 声明式 UI(ArkTS) | 命令式 UI(传统 Android View) |
|---|---|---|
| 编程模型 | 状态驱动视图,描述 UI 在任何状态下的样子 | 手动操作视图树,调用方法更新 UI |
| 状态管理 | @State 自动追踪依赖,精确更新 |
手动调用 setText、setVisibility 等 |
| 代码量 | 较少,UI 结构与状态绑定一体化 | 较多,需要 findViewById + 设置监听器 |
| 可读性 | 高,UI 结构一目了然 | 中等,逻辑与视图分离但代码分散 |
| 性能 | 框架自动优化差分更新 | 取决于开发者手动优化程度 |
| 学习曲线 | 需理解响应式编程思维 | 直观的命令式思维 |
| 调试难度 | 状态变化自动追踪,调试工具支持 | 需手动添加日志追踪 UI 更新 |
20.2 数据驱动渲染方式对比
| 渲染方式 | 本案例实现 | 传统实现 | 优势分析 |
|---|---|---|---|
| Tab 切换 | if/else if 条件渲染 + @State |
Viewpager + Fragment |
代码集中,状态管理简单 |
| 列表渲染 | ForEach + 纯函数格式化 |
RecyclerView + Adapter |
无需 Adapter 类,数据到 UI 直接映射 |
| 弹框管理 | 单一 activeModal 数值状态 |
多个 Dialog 实例管理 | 天然互斥,状态简洁 |
| 动画驱动 | @State + setInterval + 纯函数 |
ValueAnimator + 回调 |
数据驱动,动画逻辑可测试 |
| 图表绘制 | Canvas + 纯函数 | 第三方图表库 | 包体积小,定制灵活 |
20.3 状态管理策略对比
| 状态类型 | 本案例方案 | 替代方案 | 选择理由 |
|---|---|---|---|
| Tab 切换 | @State currentTab: number |
独立布尔变量组 | 数值天然互斥,代码简洁 |
| 弹框控制 | @State activeModal: number |
多个 @State boolean |
互斥保证,扩展方便 |
| 表单输入 | @State askPrice: string |
双向绑定对象 | 简单直接,符合 ArkTS 惯例 |
| 多选列表 | @State prefSizes: number[] + toggle 方法 |
Set 数据结构 |
数组兼容 ForEach,序列化方便 |
| 动画状态 | @State floatEmojis: FloatEmoji[] |
@Animatable 装饰器 |
12 元素小列表,纯函数更新足够 |
| 倒计时 | @State flashSec: number + setInterval |
CountDownTimer |
ArkTS 原生定时器,生命周期可控 |
20.4 纯函数与组件内方法对比
| 对比维度 | 纯函数(本案例) | 组件内方法 | 静态工具类 |
|---|---|---|---|
| 状态依赖 | 无,纯输入输出 | 可访问 this 状态 |
无,但需静态调用 |
| 可测试性 | 极高,输入输出确定 | 需模拟组件上下文 | 高,但受静态限制 |
| 复用性 | 全局可用 | 仅限当前组件 | 全局可用 |
| 可维护性 | 高,职责单一 | 中等,可能混合逻辑 | 高,但过度使用导致类爆炸 |
| 性能 | 无额外开销 | 可能有 this 绑定开销 |
无额外开销 |
| 适用场景 | 格式化、映射、计算 | 需要访问状态的逻辑 | 通用工具函数 |
二十一、总结与展望
21.1 架构设计的核心思想回顾
通过对整个球鞋引力平台的逐行代码分析,我们可以清晰地看到基于 HarmonyOS API 24 的 ArkTS 声明式开发所倡导的核心架构思想——“数据驱动、函数分离、组件组合”。这个三词概括了整个平台从接口定义到 UI 渲染的完整链路设计。数据驱动意味着 UI 是状态的函数映像,开发者只需关注"状态是什么"和"UI 应该长什么样",而由框架负责状态变化到 UI 更新的高效映射。函数分离意味着所有不依赖状态的逻辑都被抽取为纯函数,这些函数是可测试、可复用、可缓存的业务逻辑原子。组件组合意味着复杂的 UI 被分解为一系列 @Builder 构建器,每个构建器负责一个独立的 UI 区域,最终在 build 方法中组合成完整的页面。
这种架构思想的深层价值在于它将"复杂性"分散到了三个独立的维度——数据复杂度、逻辑复杂度和 UI 复杂度——而非将它们纠缠在一起。当需求变更时,开发者可以精确地定位到需要修改的维度,而不会产生连锁影响。例如,修改价格格式化逻辑只需修改 formatPrice 函数,不需要触碰任何组件代码;添加新的 Tab 页面只需定义新的 @Builder 函数并在 build 方法中添加条件分支,不需要修改数据层或纯函数层。这种"关注点分离"的架构原则在基于 HarmonyOS 6.1.1 的开发实践中是确保代码长期可维护的基石。
21.2 声明式 UI 的工程价值
HarmonyOS ArkTS API 24 所采用的声明式 UI 编程范式,在工程实践中展现出了显著的生产力提升。与传统的命令式 UI 开发相比,声明式 UI 将开发者从繁琐的视图操作中解放出来——不需要 findViewById、不需要手动调用 setText、不需要管理视图树的结构变化。开发者只需要定义状态和 UI 的映射关系,框架自动处理状态变化到 UI 更新的转换。在球鞋交易平台中,14 款商品、13 条鉴定记录、13 个收藏项、13 位排行用户的数据全部通过 ForEach 自动渲染为列表,每个列表项的 UI 结构只需定义一次,数据变化时自动更新。
声明式 UI 的另一个工程价值在于它天然地促进了代码的一致性。由于 UI 结构通过统一的声明式语法描述,不同开发者编写的代码风格趋于一致,代码审查时更容易发现偏差。在本案例中,所有卡片组件都遵循相同的结构模式——Column 容器 + 圆角 + card 背景色 + 内边距,这种一致性不是通过文档约束而是通过声明式语法自然形成的。基于 HarmonyOS API 24 的开发团队可以借此建立统一的 UI 组件规范,提升整体代码质量。
21.3 纯函数架构的可维护性优势
纯函数架构在本案例中的表现令人印象深刻。所有的业务逻辑——趋势颜色映射、价格格式化、成交量格式化、数据分块、数据查找、动画位置计算、倒计时计算、图表绘制——都被实现为不依赖组件状态的纯函数。这种架构使得业务逻辑的修改变得安全而精确——修改 formatPrice 函数的实现只会影响所有调用它的地方,不会有意外的副作用。纯函数的确定性特性也使得调试更加容易——给定相同的输入,函数总是返回相同的输出,开发者可以通过控制输入来精确定位问题。
在团队协作中,纯函数架构的价值更加突出。不同的开发者可以并行开发 UI 组件和纯函数,只要接口约定不变,两者可以独立推进。纯函数的单元测试不需要模拟组件环境,测试覆盖率可以轻松达到高水平。在基于 HarmonyOS 6.1.1 的开发流程中,这种可测试性尤为重要——它使团队能够建立持续集成的质量门禁,确保每次代码提交不会引入回归缺陷。
21.4 Canvas 绘图的性能与灵活性
本案例中 Canvas 2D 绘图的实践表明,在不依赖第三方图表库的情况下,基于 HarmonyOS API 24 的 Canvas API 完全能够实现生产级别的数据可视化。折线图和柱状图的绘制函数各约 40 行代码,包含了网格线、数据连线、数据点、柱体和标签的完整绘制逻辑。这种自给自足的绘图方式在包体积控制和定制灵活性方面具有显著优势——整个图表功能不引入任何外部依赖,且每个视觉元素都可以精确控制。
Canvas 绘图的性能在 HarmonyOS 6.1.1 系统中得到了良好的优化。RenderingContextSettings(true) 启用了抗锯齿,使线条和形状的边缘平滑;Canvas 组件的 onReady 回调确保了在正确的时机进行绘制,避免了在组件尚未准备好时绘制导致的空白或错误。在数据量较小(7 个数据点)的场景下,绘制几乎是瞬时完成的,不会造成可感知的延迟。
21.5 弹框系统的状态管理哲学
本案例的弹框系统设计体现了一种"最小状态"的管理哲学。使用单一的 activeModal 数值变量控制四个弹框的显示,这种设计在状态空间上是最优的——只需要一个变量就能表达"无弹框"和"四种弹框"五种状态。如果使用四个独立的布尔变量,状态空间将达到 16 种(2 的 4 次方),其中大部分组合是无效的(如同时显示两个弹框),需要额外的逻辑来排除这些无效状态。
这种"最小状态"哲学的深层含义是:状态应该精确地表达业务语义,而不是过度建模。球鞋交易平台的业务语义是"同一时间最多显示一个弹框",单一数值变量精确地表达了这一语义,不多也不少。这种精确性在代码维护中价值巨大——当新开发者阅读 activeModal 变量时,立即就能理解"弹框是互斥的"这一业务约束,而不需要通过多个布尔变量的互斥逻辑来推断。
21.6 HarmonyOS ArkTS API 24 的开发生态展望
基于 HarmonyOS API 24 的 ArkTS 声明式开发框架正在快速成熟。从本案例的实践来看,ArkTS 已经能够支撑起具有生产级复杂度的单文件页面应用——包括多 Tab 导航、多弹框交互、Canvas 图表绘制、定时器动画、条件渲染和列表渲染等核心能力。HarmonyOS 6.1.1 系统的底层优化(如 Canvas 加速渲染、细粒度状态追踪、ForEach 差分更新)为这些能力的性能表现提供了坚实保障。
展望未来,随着 HarmonyOS 生态的进一步发展,ArkTS 开发框架有望在以下几个方面继续演进。首先是组件库的丰富——目前 ArkUI 提供的基础组件已经覆盖了大部分常见场景,但在高级组件(如日期选择器、级联选择器、富文本编辑器)方面仍有补充空间。其次是开发工具的完善——热重载、可视化预览、性能分析工具的进一步优化将显著提升开发效率。最后是生态社区的建设——更多的开源组件、最佳实践文档和示例项目将降低开发者的学习曲线,加速 HarmonyOS 应用生态的繁荣。基于 HarmonyOS API 24 的开发实践表明,这个生态系统正在朝着正确的方向快速发展,我们有理由对其未来保持乐观。
21.7 组件化设计的可扩展性分析
本案例的组件化设计通过 @Builder 构建器实现了 UI 逻辑的模块化分离。每个 Tab 页面(TabHot、TabAuth、TabMarket、TabCollection、TabRank、TabMine)和每个弹框(ModalAuthSubmit、ModalListing、ModalSizePref、ModalDelete)都被定义为一个独立的构建器函数,这种设计使每个功能模块的代码自包含且边界清晰。当需要添加新的功能页面时,开发者只需定义一个新的 @Builder 函数并在 build 方法中添加对应的条件分支,完全不需要修改现有的构建器代码。这种"插件式"的扩展模式使应用的功能演进变得安全而可控。
参数化构建器(如 CollectionCard(item: CollectionItem) 和 modalOverlay(onClose: () => void))进一步提升了组件复用性。CollectionCard 通过接收参数实现了同一 UI 模板在不同数据项上的复用——收藏页面的双列网格中,同一个构建器被调用两次,每次传入不同的 CollectionItem 数据。modalOverlay 通过接收回调函数实现了关闭逻辑的参数化,使四个弹框可以复用同一个遮罩层构建器。这种参数化复用模式在基于 HarmonyOS API 24 的开发中是减少代码重复的有效手段。
21.8 性能优化的实践总结
在性能优化方面,本案例实践了多种策略。首先是定时器频率的精确控制——漂浮表情动画使用 60 毫秒间隔(约 16.7 FPS),倒计时使用 1000 毫秒间隔,两者的频率差异反映了动画重要性的不同。漂浮表情是装饰性动画,较低帧率不影响核心体验;倒计时是功能性显示,每秒更新一次即可。这种"按需分配帧率"的策略避免了不必要的 CPU 消耗,在 HarmonyOS 6.1.1 系统的电池管理框架下能够有效延长续航。
其次是 ForEach 键值策略的选择。在本案例中,不同场景使用了不同的键值生成策略。商品列表使用 s.id.toString() 作为键,确保了列表更新时的元素复用;漂浮表情使用 f.seed.toString() + f.y.toString() 作为键,虽然频繁变化但确保了位置更新的正确性;多选列表使用包含数组长度的复合键,确保了选中状态变化的正确响应。这种"因场景制宜"的键值策略是 ArkTS 列表渲染优化的核心技巧。
最后是资源清理的规范性。aboutToDisappear 生命周期回调中清理了两个定时器,避免了组件销毁后的定时器泄漏。hitTestBehavior(HitTestMode.None) 禁用了漂浮表情层的触摸事件处理,避免了不必要的命中测试开销。这些细节性的优化虽然在单次操作中影响微小,但在长时间运行的应用中累积起来能够显著提升整体性能表现。
21.9 用户体验设计的技术考量
从用户体验角度审视本案例的技术实现,可以发现多处精心设计的交互细节。倒计时使用独立的数字块加冒号分隔的显示方式,而非简单的文本拼接,这种设计确保了数字宽度固定,避免了数字变化时的文本抖动。收藏卡片的盈亏数据使用颜色语义化(盈利绿色、亏损红色),使用户在快速浏览时无需阅读数字就能感知盈亏方向。
弹框的设计也体现了用户体验的考量。底部弹框(鉴定提交、挂售)使用 Stack({ alignContent: Alignment.Bottom }) 从底部弹出,符合移动端单手操作的习惯——弹框内容在屏幕下半部分,用户拇指可以轻松触达。居中弹框(尺码偏好、删除确认)使用 Stack({ alignContent: Alignment.Center }) 在屏幕中央显示,适合需要用户集中注意力的操作。删除弹框中展示收藏项的财务详情(入手价、现价、盈亏),使用户在删除前能够评估损失,这种"决策辅助"设计体现了对用户利益的尊重。
21.10 从展示型页面到生产应用的演进路径
本案例作为一个展示型页面,数据全部为静态常量。从展示型页面演进到生产应用,需要逐步引入数据获取、状态持久化和网络通信等能力。首先是数据层的替换——将 SNEAKERS、AUTH_RECORDS 等静态常量替换为通过 HTTP 请求从后端 API 获取的动态数据,HarmonyOS 提供的 @ohos.net.http 模块可以用于发起网络请求。其次是状态持久化——用户的尺码偏好、收藏列表等状态需要通过 @ohos.data.preferences 或数据库进行本地持久化,确保应用重启后状态不丢失。
在架构层面,生产应用可能需要引入全局状态管理(如 AppStorage)来共享跨组件的状态,以及引入路由管理来处理页面间的导航跳转。但本案例所确立的"数据驱动、函数分离、组件组合"的核心架构原则在演进过程中不会改变——这些原则是基于 HarmonyOS API 24 的开发的通用最佳实践,无论应用规模如何扩展都适用。通过遵循这些原则,开发者可以构建出既具有展示型页面的简洁性,又具备生产应用的健壮性的高品质 HarmonyOS 应用。
二十二、补充技术深度分析
22.1 接口设计的类型安全体系
在本案例中,11 个 TypeScript 接口共同构成了整个应用的类型安全体系。这些接口不仅定义了数据的结构,更在编译期提供了全面的类型检查保障。当开发者在组件中使用 Sneaker 类型的变量时,TypeScript 编译器会确保所有属性访问都是合法的——如果尝试访问不存在的属性 s.discount,编译器会立即报错。这种编译期的类型安全保障在大型项目中价值巨大,它能够在代码提交前捕获大量潜在的类型错误,减少运行时异常的发生。
接口之间的关联设计也值得关注。Sneaker 接口定义了商品的完整信息,而 CollectionItem 接口定义了用户收藏的球鞋信息。两者有部分字段重叠(如 name、brand、emoji、color),但 CollectionItem 增加了用户相关的字段(如 size、acquiredPrice、profit)。这种"核心数据 + 扩展数据"的接口设计模式使数据模型能够适应不同的业务场景——商品列表使用 Sneaker 接口,收藏列表使用 CollectionItem 接口,两者各司其职而不冗余。基于 HarmonyOS API 24 的 TypeScript 类型系统为这种接口设计提供了完整的编译期支持。
22.2 数据流与单向数据绑定
本案例的数据流遵循严格的单向数据绑定模式——数据从状态变量流向 UI,UI 的变化通过事件回调反向更新状态。这种单向数据流确保了数据变化的可追踪性:当 currentTab 从 0 变为 1 时,开发者可以确定是某个 onClick 回调执行了 this.currentTab = 1,而不需要排查是否有其他地方间接修改了这个状态。
在挂售弹框中,这种单向数据流体现得尤为清晰。用户在尺码选择器中点击某个尺码,onClick 回调执行 this.selectedSize = sz,状态变化触发 UI 更新——选中尺码的标签变为强调红色背景。用户在价格输入框中输入数字,onChange 回调执行 this.askPrice = v,状态变化触发输入框内容同步。这种"事件 -> 状态更新 -> UI 重渲染"的循环是声明式 UI 的基本运作模式,基于 HarmonyOS API 24 的 ArkTS 框架对这一模式提供了完整的支持。
22.3 条件渲染的性能特性
if/else 条件渲染在 ArkTS 中具有特殊的性能特性。当条件从 true 变为 false 时,条件块内的 UI 子树会被完全卸载——所有组件实例被销毁,相关的状态和事件监听器被清理。当条件重新变为 true 时,UI 子树会被重新创建。这种"全量卸载/重建"的模式在 Tab 切换场景中是合理的——因为不同 Tab 的内容完全不同,复用组件实例没有意义。
但在某些场景下,频繁的条件切换可能导致性能问题。如果条件块内的组件树很大(如包含大量 ForEach 列表),每次切换都会带来明显的重建开销。在这种情况下,可以考虑使用 Visibility 属性替代 if/else——将组件的 visibility 设置为 Visibility.None 可以隐藏组件但不销毁,再次显示时无需重建。不过在本案例中,Tab 切换使用 if/else 是合理的选择,因为每个 Tab 的内容相对独立,且用户不会频繁切换。
22.4 漂浮表情动画的帧率与性能平衡
漂浮表情动画使用 60 毫秒的定时器间隔,对应的帧率约为 16.7 FPS。这个帧率选择是在视觉效果和性能消耗之间的平衡。如果使用 16 毫秒间隔(约 60 FPS),动画会更加流畅,但 CPU 占用会显著增加——每秒 60 次的状态更新和 ForEach 重渲染在低端设备上可能导致卡顿。如果使用 100 毫秒间隔(10 FPS),动画会显得不够流畅,表情的移动会有明显的跳跃感。
60 毫秒间隔是一个经过实践验证的合理选择。在 HarmonyOS 6.1.1 系统中,这个帧率下的动画看起来仍然流畅(因为表情的移动速度很慢,每帧只移动不到 1% 的位置),同时 CPU 占用保持在较低水平。advanceFloat 函数创建了全新的数组而非原地修改,这在 ArkTS 的状态管理系统中是最安全的更新方式——确保框架能够检测到引用变化并触发重新渲染。
22.5 颜色系统的语义化设计
本案例的颜色系统(COLORS 常量)采用了语义化命名而非描述性命名。accent 而非 red,success 而非 green,textPrimary 而非 white——这种命名方式使颜色的用途而非外观成为命名的核心。当未来需要将强调色从红色改为蓝色时,只需修改 COLORS.accent 的值,而不需要在代码中搜索替换所有 red 字符串。
语义化颜色命名还促进了颜色使用的一致性。在球鞋交易平台中,所有需要用户注意的交互元素(按钮、激活标签、Tab 下划线)都使用 COLORS.accent,所有正面状态(盈利、通过、上涨)都使用 COLORS.success,所有负面状态(亏损、不通过、下跌)都使用 COLORS.danger。这种一致的语义-颜色映射使用户在整个应用中形成稳定的色彩认知,降低了认知负荷。基于 HarmonyOS API 24 的设计系统建设中,语义化颜色命名是建立可维护设计系统的第一步。
22.6 列表渲染的键值生成策略
ForEach 组件的键值生成函数(第三个参数)在 ArkTS 中扮演着至关重要的角色。键值决定了列表更新时框架如何匹配旧元素和新元素——如果键值相同,框架会复用已有的 UI 节点并更新其属性;如果键值不同,框架会销毁旧节点并创建新节点。因此,键值生成策略直接影响列表渲染的性能和正确性。
在本案例中,不同列表使用了不同的键值策略。商品列表使用 s.id.toString() 作为键——id 是商品的唯一标识,不会变化,这确保了列表更新时商品卡片的复用。鉴定记录列表使用 r.id.toString() 作为键,原理相同。但漂浮表情列表使用了 f.seed.toString() + f.y.toString() 作为键——将 y 位置包含在键中,使得每次位置变化都产生新的键值,框架会为每个表情创建新的 UI 节点而非更新属性。这种策略在动画场景中确保了位置更新的正确性,但牺牲了节点复用的性能优势。
多选列表的键值策略最为精妙。尺码偏好弹框中,尺码选择器使用 'pref' + sz.toString() + this.prefSizes.length.toString() 作为键——将数组长度包含在键中。当用户选中或取消选中一个尺码时,prefSizes 数组长度变化,所有尺码标签的键值都会变化,触发完整的列表重建。虽然这看起来效率不高,但它确保了选中状态的视觉更新正确性——如果使用 sz.toString() 作为键,ForEach 会复用已有节点,但由于 indexOf 检查的 backgroundColor 和 fontColor 依赖于 prefSizes 的内容变化,这些属性需要被更新。通过改变键值强制重建,确保了所有属性都被正确重新计算和应用。
22.7 生命周期回调的工程意义
aboutToAppear 和 aboutToDisappear 生命周期回调在组件的完整生命周期中只各执行一次,这使它们成为资源初始化和清理的理想位置。在球鞋交易平台中,aboutToAppear 负责启动两个定时器(漂浮表情和倒计时),aboutToDisappear 负责清除这两个定时器。这种对称的资源管理模式确保了组件在整个生命周期中不会出现资源泄漏。
在基于 HarmonyOS 6.1.1 的开发中,生命周期回调的执行时机是确定的——aboutToAppear 在组件创建后、UI 渲染前执行,aboutToDisappear 在组件销毁前执行。这种确定性使开发者可以安全地在 aboutToAppear 中访问 @State 变量(此时已初始化完成),并确保 aboutToDisappear 中的清理逻辑在组件仍然可用时执行。如果将定时器启动放在 build 方法中,由于 build 可能被多次调用(每次状态变化时),会导致定时器被重复创建,最终造成严重的内存泄漏和性能问题。
22.8 Stack 布局与层级管理
build 方法中的 Stack 容器是整个页面层级管理的核心。Stack 的子元素按照添加顺序从下到上层叠——第一个子元素(页面主内容)在最底层,最后一个子元素(弹框层)在最顶层。这种层级设计确保了弹框能够覆盖在页面内容之上,而漂浮表情层位于页面内容和弹框之间。
Stack 的 alignContent 默认值为 Alignment.Center,这意味着所有子元素默认居中对齐。但本案例中的页面主内容使用 Column 作为 Stack 的第一个子元素,并通过 .width('100%').height('100%') 占满整个 Stack 空间,因此居中对齐对其没有影响。漂浮表情层同样占满整个空间。弹框层通过各自的 Stack 容器内部的 alignContent 设置(Alignment.Bottom 或 Alignment.Center)控制弹框在屏幕中的位置。
这种三层 Stack 架构是移动端应用页面的经典模式——内容层、装饰层和弹框层各司其职,通过 Stack 的层叠特性自然组合。hitTestBehavior(HitTestMode.None) 的设置确保了装饰层(漂浮表情)不会拦截内容层的触摸事件,而弹框层的遮罩层会拦截内容层的触摸事件(通过 onClick 关闭弹框),这正是期望的交互行为。
22.9 数据冗余与预计算的权衡
本案例在数据设计中多次使用了冗余字段和预计算值,这些设计决策体现了性能与数据一致性之间的权衡。CollectionItem 接口中的 profit 和 profitPercent 字段就是典型的预计算冗余——它们可以通过 currentPrice - acquiredPrice 和 ((currentPrice - acquiredPrice) / acquiredPrice) * 100 实时计算得出,但选择了预计算并存储在数据中。
这种预计算策略在列表渲染场景中具有明显的性能优势。收藏页面使用 chunkPairs 将 13 个收藏项分为 7 对进行双列渲染,每个收藏卡片都需要显示盈亏金额和百分比。如果每次渲染都执行计算,13 个卡片需要 26 次浮点运算和字符串格式化。预计算后,这些值直接存储在数据中,渲染时只需读取并显示,零计算开销。在基于 HarmonyOS API 24 的渲染优化中,减少 ForEach 内部的计算量是提升列表滚动流畅度的关键策略之一。
Sneaker 接口中的 color 和 colorDeep 字段也是一种冗余设计——colorDeep 本可以通过算法从 color 推导(如降低亮度),但选择了手动指定。这是因为不同球鞋的"深色变体"可能需要不同的色彩调整策略(有些需要更暗,有些需要偏色),手动指定比算法推导更加精确和可控。
22.10 文本溢出处理的一致性策略
在本案例中,几乎所有可能溢出的文本都使用了 maxLines(1) 和 textOverflow({ overflow: TextOverflow.Ellipsis }) 的组合。这一策略确保了长文本不会破坏布局——商品名称、用户名、球鞋系列名等文本字段的长度不可预测(如"sacai x Nike LDWaffle"有 22 个字符,"AF1"只有 3 个字符),通过统一的溢出处理策略,所有文本在超出可用宽度时都会以省略号结尾。
这种一致性策略在基于 HarmonyOS API 24 的开发中尤为重要。如果不设置 maxLines,长文本可能换行显示,导致卡片高度不一致,破坏列表的视觉对齐。如果设置了 maxLines(1) 但不设置 textOverflow,文本会被直接截断,用户无法知道文本被截断。省略号(…)的视觉暗示使用户意识到文本不完整,可以在需要时点击查看详情。这种细节性的 UI 一致性是高品质移动应用的标志。
22.11 按钮设计的层级体系
本案例中的按钮设计建立了一个清晰的视觉层级体系。最高层级是主操作按钮——“立即抢购”、“提交鉴定”、“确认挂售”、"确认移除"等——使用 COLORS.accent(强调红色)或 COLORS.danger(危险红色)背景,全宽设计,46 高度,23 圆角(完全圆角)。这些按钮是页面上最醒目的交互元素,引导用户完成核心转化操作。
第二层级是次要操作按钮——“买入”、“提现”、"保存"等——使用 COLORS.cardLight(深灰色)背景,较小尺寸,15 圆角。这些按钮虽然可点击但不是当前页面的核心操作,通过降低视觉权重引导用户关注主操作。
第三层级是功能按钮——“移除收藏”、“管理 >”、"编辑 >"等——使用 COLORS.cardLight 背景和更小的字号(10px),或直接使用文字链接(无背景色)。这些按钮提供辅助功能,视觉权重最低,不干扰核心操作流程。
这种三级按钮层级体系使页面的交互引导清晰有序——用户首先注意到红色主操作按钮,其次是灰色次要按钮,最后是低权重的功能链接。基于 HarmonyOS API 24 的 UI 设计中,建立明确的按钮视觉层级是提升用户体验和转化率的有效手段。
22.12 移动端布局的弹性设计
本案例的布局系统大量使用了 layoutWeight、百分比宽度和 FlexAlign 对齐方式,构建了一个高度弹性的响应式布局。layoutWeight(1) 使元素按比例分配剩余空间,百分比宽度(如 '100%')使元素跟随容器尺寸变化,FlexAlign.SpaceBetween 和 FlexAlign.SpaceAround 提供了灵活的间距分配策略。
这种弹性布局设计确保了页面在不同屏幕尺寸的设备上都能正确显示。在 HarmonyOS 6.1.1 系统的多种设备形态(手机、平板、折叠屏)下,弹性布局能够自动适应可用空间的变化。例如,商品卡片的 Row 布局中,左侧图标区域使用固定宽度 90,右侧信息区域使用 layoutWeight(1) 填充剩余空间——在窄屏设备上信息区域变窄,文本通过省略号截断;在宽屏设备上信息区域变宽,文本有更多展示空间。
justifyContent(FlexAlign.SpaceBetween) 在多个场景中被使用——价格与趋势的左右分布、标题与"查看全部"的左右分布、标签与状态徽章的左右分布。这种对齐方式使两个元素在容器中分别贴向两端,中间的空间随容器宽度自动调整,在不同屏幕尺寸下都能保持合理的间距。基于 HarmonyOS API 24 的 Flex 布局系统为这种弹性设计提供了完整的支持。
22.13 交互反馈与状态视觉化
本案例在交互反馈设计上采用了多种视觉化手段,使用户的操作能够得到即时的视觉确认。Tab 切换时,激活标签的字号从 14 变为 15、粗细从 Normal 变为 Bold、颜色从中灰变为白色,同时下划线从 0 宽变为 18 宽。这种多重视觉属性的同时变化确保了用户能够明确感知到当前所在的 Tab 位置。底部导航栏的激活状态通过透明度(1 vs 0.45)和文字颜色的双重变化来表达,这种设计在各种光线条件下都具有良好的可辨识性。
在表单交互中,选中状态的视觉反馈同样精心设计。尺码选择器、渠道选择器、成色选择器中的选中项都使用 COLORS.accent(强调红色)背景和白色文字,未选中项使用 COLORS.bg(深色)背景和中灰色文字。这种"红 vs 深"的高对比度色彩差异使用户能够一眼识别当前选择。Toggle 开关通过系统原生的滑动动画提供操作反馈,无需额外的视觉提示。
弹框的关闭交互也体现了细致的考量。底部弹框的遮罩层可以通过点击任意区域关闭,这通过 modalOverlay 构建器的 onClick 回调实现。居中弹框同样支持点击遮罩层关闭。这种"点击外部关闭"的交互模式是移动端弹框的标准设计,用户无需寻找关闭按钮即可快速退出弹框。弹框内的"取消"和"确认"按钮提供了更明确的关闭路径,满足需要明确操作意图的场景。
22.14 数据展示的信息密度控制
在数据展示方面,本案例通过字号、颜色和间距的精心设计,在不同信息密度之间取得了平衡。商品卡片的信息密度较高——一个卡片中包含品牌名、商品名、系列名、发售日期、2 个标签、当前价、发售价、趋势箭头、百分比变化、HOT 徽章和买入按钮共 11 个信息元素。这些元素通过三级字号体系(18px 价格、14px 商品名、10-11px 辅助信息)和三级颜色体系(白色主信息、红色强调信息、灰色辅助信息)实现了清晰的视觉层级,使用户能够在密集信息中快速找到关注的内容。
鉴定记录卡片的信息密度同样较高——品牌首字母、球鞋名、日期、渠道、价格、鉴定结果和状态标签共 7 个信息元素。通过将信息分为"左图标 + 中三行信息 + 右状态"的三栏结构,信息在水平方向上得到了合理分配,避免了垂直堆叠导致的卡片过高问题。
排行榜列表的信息密度相对适中——排名序号、头像、用户名、徽章、成交笔数、交易额和盈利额共 7 个信息元素。前三名通过领奖台的特殊布局获得了更大的展示空间,第四名及以后通过紧凑的列表布局在有限空间内展示更多信息。这种"重点突出 + 紧凑列表"的组合模式在排行榜类页面中非常常见。
22.15 结语:技术深度与工程实践的结合
通过对球鞋引力平台的完整代码分析,我们看到了基于 HarmonyOS API 24 的 ArkTS 声明式开发在实际项目中的深度应用。从接口定义到数据建模,从纯函数架构到组件化设计,从 Canvas 绘图到弹框状态管理,每一个技术点都展现了声明式 UI 编程范式的优势和特点。HarmonyOS 6.1.1 系统的底层优化为这些技术实现提供了坚实的性能保障,使开发者能够专注于业务逻辑和用户体验的设计,而不用担心框架层面的性能瓶颈。
这篇分析的价值不仅在于解读了一个具体的代码实现,更在于它展示了如何将通用的软件工程原则(单一职责、关注点分离、开闭原则、最小状态空间等)应用到基于 HarmonyOS API 24 的具体开发实践中。这些原则是跨越技术栈的通用智慧,无论使用何种框架或语言,它们都能指导开发者构建出高质量、可维护的软件系统。希望本文能够为正在或即将基于 HarmonyOS 进行应用开发的开发者提供有价值的参考和启发。
二十三、附录:关键技术点速查
23.1 核心装饰器使用速查
本案例中使用的 ArkTS 核心装饰器包括 @Entry(标记入口组件)、@Component(标记 ArkTS 组件)、@State(标记响应式状态变量)、@Builder(标记 UI 构建器函数)。这些装饰器是 HarmonyOS ArkTS API 24 声明式 UI 开发的基础设施,每一个都承担着特定的框架职责。@Entry 确保组件被识别为页面入口,@Component 赋予组件声明式 UI 的各种能力,@State 建立状态与视图的响应式绑定,@Builder 将 UI 片段封装为可复用的构建单元。
23.2 布局组件使用速查
本案例中使用的布局组件包括 Column(垂直布局)、Row(水平布局)、Stack(层叠布局)、Scroll(滚动容器)。Column 和 Row 是最常用的布局容器,通过 justifyContent 和 alignItems 控制子元素的主轴和交叉轴对齐。Stack 用于实现层叠效果,通过 alignContent 控制子元素的对齐方式。Scroll 提供滚动能力,通过 scrollable 设置滚动方向,通过 scrollBar 控制滚动条显示。
23.3 交互组件使用速查
本案例中使用的交互组件包括 Button(按钮)、Text(文本)、TextInput(输入框)、Toggle(开关)、Canvas(画布)。Button 通过 onClick 处理点击事件,内部可包裹 Text 组件实现自定义文本。TextInput 通过 onChange 监听输入变化,通过 type 设置输入类型。Toggle 通过 onChange 监听开关状态变化。Canvas 通过 onReady 回调在准备就绪后执行绘制操作。
23.4 纯函数使用速查
本案例中定义了约 20 个纯函数,涵盖了趋势颜色映射(trendColor、trendSymbol、statusColor)、价格格式化(formatPrice、formatVolume)、数据处理(chunkPairs、getSneakerById、getCollectionById)、动画计算(floatSize、floatX、floatOpacity、floatWobble、advanceFloat、buildFloatEmojis)、倒计时计算(padTwo、countdownHours、countdownMinutes、countdownSeconds)和图表绘制(drawLineChart、drawBarChart)等多个类别。这些纯函数共同构成了应用的业务逻辑核心,它们的独立性和可测试性是整个架构设计的基石。
23.5 最终总结
综上所述,基于 HarmonyOS API 24 的 ArkTS 声明式开发框架为移动应用开发提供了一套完整而高效的技术方案。本文通过对球鞋引力平台的逐行代码分析,全面展示了从接口定义、数据建模、纯函数架构到组件化构建、Canvas 绘图、弹框系统、动画管理和状态控制的完整技术实践。每一个技术决策背后都有明确的设计考量和权衡分析,这些实践经验对于希望在 HarmonyOS 6.1.1 平台上构建高质量移动应用的开发者具有直接的参考价值。声明式 UI 编程范式不仅提升了开发效率,更通过状态驱动的响应式机制确保了应用的性能和可维护性,它是移动应用开发未来的重要方向,而 HarmonyOS ArkTS API 24 正是这一方向上的优秀实践。
23.6 开发者实践建议
对于即将开始基于 HarmonyOS API 24 项目的开发者,本文提供了以下实践建议。第一,在项目初期建立清晰的接口定义体系——所有数据模型都应通过 TypeScript 接口定义,确保类型安全贯穿整个开发过程。第二,将业务逻辑抽取为纯函数——任何不依赖组件状态的逻辑都应成为独立的纯函数,这不仅提升了可测试性,更使代码职责清晰。第三,使用单一数值状态管理互斥 UI——如弹框、Tab 切换等场景使用一个数值变量而非多个布尔变量,天然保证互斥性。第四,Canvas 绘图优先使用纯函数——将绘制逻辑与组件状态分离,通过 onReady 回调触发绘制,确保绘制逻辑的可测试性和复用性。
第五,合理规划定时器频率——装饰性动画使用较低帧率(如 60ms 间隔),功能性更新使用自然频率(如倒计时 1000ms 间隔),避免不必要的 CPU 消耗。第六,在 aboutToDisappear 中清理所有资源——定时器、事件监听器等资源必须在组件销毁时清理,防止内存泄漏。第七,使用 hitTestBehavior(HitTestMode.None) 处理覆盖层——任何覆盖在交互内容之上的装饰层都应禁用命中测试,确保触摸事件能够穿透到下方。第八,为 ForEach 选择合适的键值策略——稳定数据使用 ID 作为键,频繁变化的数据将变化维度编入键值,多选列表将数组长度编入键值。这些实践建议来自于本案例的代码分析,它们在基于 HarmonyOS 6.1.1 的实际开发中已经得到了验证。
23.7 技术演进趋势展望
展望 HarmonyOS 生态的技术演进趋势,我们可以预见几个方向的发展。首先是 ArkTS 语言能力的持续增强——未来的版本可能会引入更丰富的类型系统特性(如条件类型、映射类型)和更强大的异步编程支持(如更完善的 Promise/async-await 生态)。其次是 ArkUI 组件库的扩展——更多高级组件(如高级图表、富文本编辑器、复杂表单组件)的引入将进一步降低开发成本。第三是开发工具链的完善——热重载、可视化布局编辑器、性能分析工具的成熟将显著提升开发体验。
第四是跨设备能力的深化——HarmonyOS 的分布式架构为跨设备应用开发提供了独特优势,未来的 ArkTS 可能会提供更完善的分布式 UI 组件和状态同步机制。第五是性能优化的持续投入——框架层面的渲染优化、内存管理和启动速度提升将继续迭代,为应用提供更好的运行时性能。基于 HarmonyOS API 24 的开发实践是这一演进过程中的重要一步,它为开发者奠定了坚实的技术基础,也为未来的技术升级预留了充足的扩展空间。开发者应该持续关注 HarmonyOS 的版本更新和生态发展,及时学习和应用新的技术能力,保持技术栈的现代化。
23.8 学习路径与资源建议
对于希望深入学习 HarmonyOS ArkTS API 24 开发的开发者,建议按照以下路径循序渐进。首先,掌握 TypeScript 语言基础——ArkTS 是 TypeScript 的超集,理解接口、泛型、类型推断等 TypeScript 特性是学习 ArkTS 的前提。其次,学习声明式 UI 的基本概念——理解状态驱动视图的核心思想,掌握 @State、@Builder、@Component 等装饰器的使用方法。第三,实践基础布局组件——通过 Column、Row、Stack 等容器组件练习各种布局组合,理解 layoutWeight、justifyContent、alignItems 等布局属性的作用。
第四,掌握列表渲染和条件渲染——ForEach 和 if/else 是声明式 UI 中最常用的控制流结构,理解键值生成策略和条件渲染的性能特性。第五,学习 Canvas 绘图——通过 CanvasRenderingContext2D 和 RenderingContextSettings 实现自定义图形绘制,理解坐标映射和绘制流程。第六,实践状态管理——通过复杂的状态组合(如本案例的多弹框管理、多选列表管理)深入理解 @State 的响应式机制。第七,研究生命周期管理——通过 aboutToAppear 和 aboutToDisappear 掌握资源初始化和清理的最佳实践。
23.9 致谢与版权说明
本文基于一个完整的 HarmonyOS ArkTS 单文件页面源码进行技术分析,所有代码片段和分析文字均为原创。文章中涉及的 HarmonyOS、ArkTS、ArkUI 等技术名称属于华为技术有限公司的商标或技术品牌。本文的技术分析基于 HarmonyOS 6.1.1 系统版本和 ArkTS API 24 的公开技术文档,随着系统版本和 API 的更新,部分技术细节可能会发生变化。读者在实际开发中应以华为官方发布的最新技术文档为准。感谢 HarmonyOS 开发者社区提供的丰富技术资源和讨论氛围,这些资源为本文的写作提供了重要的参考和启发。希望本文能够为 HarmonyOS 开发者社区贡献一份有价值的技术资料,推动 HarmonyOS 应用生态的持续发展和繁荣。
23.10 结语
技术博客的价值在于将代码背后的思考过程显性化,使读者不仅能理解"怎么做",更能理解"为什么这样做"。本文通过对球鞋引力平台的完整技术剖析,展示了基于 HarmonyOS API 24 的 ArkTS 声明式开发在实践中如何运用接口定义、纯函数架构、组件化设计、Canvas 绘图和状态管理等技术手段构建一个具有生产级复杂度的移动应用页面。每一个技术选择都有其背后的设计考量和权衡分析,这些思考过程是技术文章最核心的价值所在。
HarmonyOS 6.1.1 系统为开发者提供了强大的底层能力和优秀的运行时性能,而 HarmonyOS ArkTS API 24 则为开发者提供了高效的声明式 UI 开发工具。两者的结合使开发者能够在保证应用性能的同时大幅提升开发效率。随着 HarmonyOS 生态的持续发展和完善,我们有理由相信这一技术栈将在移动应用开发领域扮演越来越重要的角色。希望本文的分析能够帮助开发者更好地理解和运用 HarmonyOS 的开发能力,共同推动 HarmonyOS 应用生态的繁荣发展。在未来的技术探索之路上,让我们携手前行,用代码创造更美好的数字体验。
23.11 读者反馈与交流
本文力求详尽和准确,但由于技术深度和篇幅限制,难免存在疏漏或可商榷之处。欢迎读者在阅读后提出反馈意见,包括但不限于技术细节的修正、分析深度的建议、补充内容的提议等。技术写作是一个持续改进的过程,读者的反馈是提升文章质量的重要驱动力。基于 HarmonyOS API 24 的开发实践仍在不断演进中,本文所呈现的分析和观点也会随着技术发展而更新迭代。
对于希望进一步交流的开发者,建议关注 HarmonyOS 官方开发者社区和论坛,那里有丰富的技术文档、示例代码和开发者讨论。同时,也鼓励开发者在实践中验证本文所述的技术观点,通过实际编码来深化对 ArkTS 声明式开发的理解。技术的真正掌握来自于实践——阅读本文后,建议读者尝试基于 HarmonyOS 6.1.1 系统和 ArkTS API 24 构建自己的第一个声明式 UI 页面,将本文的理论分析转化为实际的编码能力。只有在实践中遇到问题、解决问题,才能真正内化这些技术知识,成为一名合格的 HarmonyOS 应用开发者。
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// ============================================================
// 球鞋引力 Sneaker Gravity
// HarmonyOS ArkTS (API 24) 单文件页面
// 京东风格球鞋交易平台 · 黑白极简街头风
// ============================================================
this.FloatLayer()
if (this.activeModal === 1) {
this.ModalAuthSubmit()
}
if (this.activeModal === 2) {
this.ModalListing()
}
if (this.activeModal === 3) {
this.ModalSizePref()
}
if (this.activeModal === 4) {
this.ModalDelete()
}
}
.width('100%')
.height('100%')
.backgroundColor(COLORS.bg)
}
}

23.12 总结:
最后,让我们对全文的核心技术要点进行一次系统性回顾。本文从 HarmonyOS ArkTS API 24 的声明式 UI 编程范式出发,深入分析了球鞋引力交易平台的完整源码实现。在接口设计层面,我们学习了如何通过 TypeScript 接口建立类型安全的数据模型体系,包括 ColorPalette、Sneaker、AuthRecord 等十一个接口的设计思路。在数据层,我们分析了十四款球鞋商品、十三条鉴定记录、十三个收藏项和十三位排行用户的数据结构设计。在纯函数层,我们深入解读了趋势颜色映射、价格格式化、数据分块、动画位置计算、倒计时计算和 Canvas 图表绘制等约二十个纯函数的实现原理。
在组件层,我们逐段分析了六个 Tab 页面构建器(热卖、鉴定、行情、收藏、排行、我的)、四个弹框构建器(鉴定提交、挂售、尺码偏好、删除收藏)以及漂浮表情层和底部导航的代码实现。在状态管理层面,我们深入探讨了 @State 装饰器的响应式机制、单一弹框状态的设计哲学和多选列表的状态更新策略。在性能优化层面,我们分析了定时器频率选择、ForEach 键值策略、hitTestBehavior 命中测试控制和资源清理规范等优化手段。这些技术要点共同构成了基于 HarmonyOS API 24 的 ArkTS 开发的完整知识体系,为开发者提供了从理论到实践的全面参考。
更多推荐



所有评论(0)