基于HarmonyOS API 24与ArkTS声明式UI范式的球鞋定制工坊全栈实战:HarmonyOS 6.1.1单文件架构深度解析、Canvas数据可视化、动画交互与赛博风暗色主题设计全维度技术指南

一、技术背景与架构概览

1.1 HarmonyOS技术栈全景

在这里插入图片描述

HarmonyOS作为华为自研的分布式操作系统,历经多个版本的迭代演进,在HarmonyOS 6.1.1版本中迎来了声明式开发范式的全面成熟。基于HarmonyOS ArkTS API 24的开发框架,为开发者提供了从UI构建到状态管理、从动画系统到图形绘制的一站式解决方案。

ArkTS语言在TypeScript的基础上进行了深度扩展,引入了装饰器语法、自定义组件、状态管理等核心特性。这种声明式的UI开发范式,使得开发者可以通过描述"UI应该是什么样子"来构建界面,而无需关心底层的DOM操作和渲染细节。

在HarmonyOS 6.1.1中,ArkUI框架进一步优化了渲染性能,增强了组件化开发能力,并且提供了更加丰富的内置组件和API。基于HarmonyOS API 24的应用可以充分利用这些新特性,构建出流畅、美观、功能丰富的原生应用。

1.2 声明式UI范式的核心思想

在这里插入图片描述

声明式UI范式的核心在于"数据驱动视图"。当状态发生变化时,框架会自动重新计算并更新相关的UI部分,而开发者只需要关注状态的定义和状态与UI之间的映射关系。

这种范式与传统的命令式UI开发有着本质的区别。在命令式开发中,开发者需要手动获取DOM元素、修改属性、监听事件,每一步都需要明确的指令。而在声明式开发中,开发者只需要描述UI在不同状态下的样子,状态变化时的更新由框架自动完成。

ArkTS通过@State@Prop@Link等装饰器来管理组件状态,通过@Component装饰器来定义自定义组件,通过build()方法来描述UI结构。这种设计使得代码更加简洁、可读性更强,同时也更容易维护和测试。

1.3 单文件架构的设计理念

在这里插入图片描述

本文所分析的球鞋定制工坊应用采用了单文件架构的设计模式。整个应用的所有代码——包括枚举定义、接口声明、数据常量、辅助函数、入口组件以及六个功能Tab组件——全部集中在一个源文件中。

这种单文件架构在小型应用或原型开发阶段具有显著的优势。首先,它减少了文件跳转的开销,开发者可以在一个文件中浏览完整的应用逻辑。其次,它降低了模块间的耦合复杂度,所有的类型和数据都在同一作用域内可用。最后,它便于快速原型验证和代码分享。

当然,单文件架构也有其局限性。当应用规模扩大到一定程度时,单一文件会变得过于庞大,不利于团队协作和代码维护。但对于中小型应用或教学演示场景,单文件架构不失为一种高效的开发方式。

1.4 赛博风暗色主题的视觉设计

在这里插入图片描述

本应用采用了"炭黑 + 暗夜紫 + 电光绿"的赛博朋克风格配色方案。主背景色为深炭黑色#16161A,卡片背景为稍浅的深灰色#1F1F26,主色调为暗夜紫#7C4DFF,强调色为电光绿#B6FF00

这种配色方案在暗色模式下具有强烈的视觉冲击力。紫色作为主色调传达出科技感和未来感,而电光绿作为强调色则提供了清晰的视觉焦点和交互反馈。文字颜色分为三个层级:主文字色#F2F2F7、次文字色#A8A8B3、辅助文字色#6E6E7A,确保了在暗色背景下的可读性。

此外,主题中还定义了危险色#FF4D5E和警告色#FFB300,用于状态标识和操作反馈。完整的配色系统通过一个统一的调色板对象进行管理,便于全局引用和主题切换。

1.5 整体架构层次图

在这里插入图片描述

应用入口层 SneakerLabApp

顶部导航栏

内容展示层 Stack

底部Tab栏

首页Tab SNHomeTab

球鞋Tab SNShoeTab

配色Tab SNColorTab

护理Tab SNCareTab

客制Tab SNCustomTab

我的Tab SNMineTab

鞋型仪表盘

限量发售横滑

本周热销榜

功能导航宫格

分类筛选

球鞋Grid

尺码对比条

人气柱状图

编辑参数弹框

喷漆调色台

潮流色卡Grid

色系分布饼图

配色热度排行

配色方案弹框

护理好物榜

护理用量分布

护理详情弹框

发起定制入口

客制方案列表

工艺单量统计

新增定制弹框

会员卡

月消费统计

我的订单

我的收藏

删除确认弹框

数据层

枚举定义

接口类型

主题配置

数据常量

辅助函数

二、数据层与类型系统深度解析

在这里插入图片描述

2.1 枚举类型的设计与应用

枚举类型是ArkTS中一种重要的类型定义方式,它允许我们为一组相关的常量赋予有意义的名称,从而提高代码的可读性和可维护性。

在本应用中,定义了一个名为SNTab的枚举类型,用于表示应用的六个功能Tab页。这个枚举包含了HomeShoesColorCareCustomMine六个成员,分别对应首页、球鞋、配色、护理、客制和我的六个页面。

使用枚举类型而非字符串字面量来表示Tab状态有诸多好处。首先,它提供了类型安全保障,编译器可以检查枚举值的合法性,避免了拼写错误导致的bug。其次,它使得代码更加自文档化,看到SNTab.Home就能立即明白其含义,而不需要猜测"home"或"Home"或"HOME"哪种写法是正确的。最后,枚举值支持IDE的自动补全,提升了开发效率。

2.2 接口定义与数据模型

在这里插入图片描述

接口是TypeScript和ArkTS中定义对象形状的核心机制。本应用共定义了十个接口,每个接口都精确描述了一类数据对象的结构和类型。

球鞋数据接口定义了球鞋的完整属性,包括唯一标识id、名称name、品类cat、现价price、原价oldPrice、热度值heat、主色调colorHex、是否限量limited以及标签tag。这些字段覆盖了商品展示所需的全部信息,从基本信息到营销属性,从视觉特征到状态标识。

配色项接口则相对简洁,包含id、名称、色值、系列和热度五个字段。这个接口主要用于配色选择和色系展示场景,重点在于颜色信息的表达。

护理产品接口的设计更加细致,除了基本的id、名称、价格外,还包含了功效描述、功效指数、使用占比、产品容量和适用材质等专业属性。这些属性支撑了护理产品的排行榜展示、用量分布统计和详情展示等功能。

定制方案接口描述了定制服务的核心属性,包括工艺类型、工期天数、价格、人气值和方案描述。其中工期和人气都通过进度条的形式进行可视化展示,使用户能够直观地了解每个方案的特点。

订单项接口记录了订单的关键信息,包括商品名称、订单状态、下单日期、价格和主色调。订单状态通过不同的颜色进行区分,已发货和已完成使用电光绿、定制中使用暗夜紫、待付款使用警告黄、其他状态使用辅助灰色。

收藏项接口则包含了商品名称、价格、备注和主色调四个核心字段。备注字段允许用户为每个收藏添加个性化的说明,增加了收藏功能的实用性。

消费记录接口用于月消费统计,只包含月份和金额两个字段。这个简单的数据结构支撑了柱状图的可视化展示。

功能项接口定义了首页功能导航宫格的数据结构,包含图标、名称和跳转目标Tab三个字段。这种设计使得功能导航可以完全由数据驱动,便于后续的功能扩展和配置化管理。

工艺统计接口用于定制工艺的单量统计,包含工艺名称和单量两个字段。这个数据驱动了工艺单量柱状图的渲染。

最后,调色板接口定义了完整的主题配色系统,包含背景色、卡片色、深卡片色、分割线色、主色调、深主色调、强调色以及三个层级的文字色,还有危险色和警告色。这种系统化的配色定义确保了整个应用视觉风格的一致性。

2.3 主题配置的系统化设计

主题配置是一个应用视觉系统的基石。本应用通过一个常量对象集中管理所有的颜色配置,这种设计模式被称为"设计令牌"(Design Tokens)。

设计令牌的核心理念是将设计决策(颜色、间距、字体等)抽象为可复用的变量,而不是在代码中硬编码具体的数值。这样做的好处是显而易见的:当需要调整配色方案时,只需要修改一处定义,整个应用的视觉风格就会统一更新。

在本应用中,调色板包含了十三种颜色变量,每种颜色都有明确的语义含义。背景色bg是整个应用的底色,卡片色card用于内容容器,深卡片色cardDeep用于卡片内的次级容器或需要视觉分层的区域,分割线色line用于边框和分隔符。

主色调primary是应用的品牌色,用于主要按钮、选中状态、重要强调等场景。深主色调primaryDeep则用于渐变的深色端或需要降低饱和度的场景。强调色accent是视觉焦点色,用于最重要的操作按钮、高亮文字、特殊标记等需要吸引用户注意力的地方。

文字颜色分为三个层级:主文字色textMain用于标题和重要内容,次文字色textSub用于正文和辅助信息,辅助文字色textDim用于次要说明和禁用状态。这种三级文字色体系确保了信息层级的清晰表达。

此外,还定义了危险色danger和警告色warn两种状态色,分别用于错误、删除等危险操作和提醒、待处理等警告状态。

2.4 数据常量的组织与管理

本应用定义了大量的数据常量,包括球鞋列表、配色列表、护理产品列表、定制方案列表、订单列表、收藏列表、消费记录、尺码数据、库存数据、首页功能项和工艺统计数据。

这些数据常量以数组的形式组织,每个数组元素都是一个符合对应接口定义的对象。数据常量的命名采用了全大写下划线分隔的命名约定,这是一种常见的常量命名规范,有助于在代码中快速识别常量值。

数据驱动是声明式UI的核心思想之一。所有的列表展示、图表渲染、功能导航都由这些数据常量驱动。当数据发生变化时,UI会自动更新。在实际的生产应用中,这些数据通常来自后端API接口,但在原型或演示阶段,使用本地数据常量可以快速搭建完整的UI界面。

球鞋数据数组包含了十款不同风格的球鞋,涵盖了跑步、篮球、休闲、滑板等多个品类,价格区间从799元到1899元不等,热度值从65到96分布。每款球鞋都有独特的主色调和标签,丰富了展示内容的多样性。

配色列表包含八种潮流配色,分布在紫系、荧光、暗色、粉系、蓝系、暖系、中性等多个色系中。每种配色都有对应的热度值,用于热度排行和饼图统计。

护理产品数组包含八款护理产品,从清洁到防护、从除臭到滋养,覆盖了球鞋护理的各个方面。功效指数从62到95不等,使用占比从8%到46%分布,这些数据为排行榜和用量分布提供了支撑。

定制方案数组包含六种定制工艺,从喷绘到刺绣、从激光到编织,每种方案有不同的工期、价格和人气值。这些数据支撑了客制方案列表和工艺统计图表。

订单列表包含六条订单记录,覆盖了已发货、定制中、已完成、待付款、已取消等多种订单状态。不同状态通过不同的颜色进行区分,提供了清晰的状态可视化。

收藏列表包含六条收藏记录,每条都有用户自定义的备注信息。收藏功能是电商应用中的常见功能,有助于用户管理感兴趣的商品。

消费记录数组包含六个月的消费数据,用于月消费统计柱状图。数据呈现波动上升的趋势,最高消费出现在6月份。

尺码数据和库存数据是两个平行数组,分别存储了欧洲码的尺码列表和对应尺码的库存数量。这种平行数组的设计在处理简单的关联数据时非常简洁高效。

首页功能项数组定义了八个功能入口,每个入口都有对应的图标、名称和跳转目标Tab。这种数据驱动的功能导航设计使得功能配置非常灵活。

工艺统计数组包含六种工艺的单量数据,用于工艺单量柱状图的展示。喷绘工艺的单量最高,达到156单。

2.5 辅助函数的实现与设计模式

辅助函数是数据层的重要组成部分,它们封装了常用的数据处理逻辑,使得组件代码更加简洁清晰。本应用共实现了十二个辅助函数,涵盖了数据过滤、排序、统计、格式化等多个方面。

球鞋过滤函数接收一个品类参数,返回该品类下的所有球鞋。当参数为"全部"时,返回所有球鞋。这个函数使用了简单的for循环和条件判断,虽然没有使用数组的filter方法,但逻辑清晰易懂。

限量球鞋筛选函数返回所有标记为限量的球鞋。这个函数的实现方式与过滤函数类似,都是通过遍历数组并根据条件收集符合条件的元素。

热销球鞋排序函数返回按热度降序排列的球鞋列表。值得注意的是,这个函数首先创建了一个原数组的浅拷贝,然后对拷贝数组进行排序,而不是直接修改原数组。这种不可变数据的处理方式是函数式编程的重要原则,它避免了副作用,使得数据变化更加可预测。

Top N球鞋函数在热销排序的基础上,截取前N个元素。这个函数复用了热销排序函数的结果,体现了代码复用的设计思想。

Top配色函数返回按热度降序排列的配色列表。实现方式与热销球鞋函数类似,都是先拷贝再排序。

Top护理产品函数返回按功效降序排列的护理产品列表。同样采用了先拷贝后排序的不可变数据处理方式。

总热度计算函数遍历配色列表,累加所有配色的热度值,返回总热度。这个值用于饼图的角度计算。

前序热度和函数计算指定索引之前(不包含当前索引)所有配色的热度之和。这个函数是饼图绘制的关键辅助函数,用于计算每个扇形的起始角度。

最大消费额函数遍历消费记录数组,找出最大的消费金额。这个值用于柱状图的高度比例计算。

最大工艺单量函数遍历工艺统计数组,找出最大的单量值。同样用于柱状图的比例计算。

最大库存函数遍历库存数组,找出最大库存量。用于尺码库存对比条的比例计算。

状态颜色函数根据订单状态返回对应的颜色值。已发货和已完成返回强调色,定制中返回主色调,待付款返回警告色,其他状态返回辅助文字色。这个函数封装了状态到颜色的映射逻辑,使得状态颜色的管理更加集中。

2.6 数据流向图

数据常量层

辅助函数层

组件层

SN_SHOES 球鞋数据

SN_COLORS_LIST 配色数据

SN_CARES 护理数据

SN_CUSTOMS 定制数据

SN_ORDERS 订单数据

SN_FAVORITES 收藏数据

SN_SPENDS 消费数据

snFilterShoes 品类过滤

snHotShoes 热度排序

snTopShoes TopN截取

snTopColors 配色排序

snTopCares 功效排序

snTotalHeat 总热度计算

snMaxSpend 最大值计算

snStatusColor 状态颜色

首页Tab

球鞋Tab

配色Tab

护理Tab

客制Tab

我的Tab

三、入口组件与导航架构分析

3.1 入口组件的结构设计

入口组件是整个应用的根组件,它承载了应用的整体布局和导航架构。在ArkTS中,入口组件通过@Entry装饰器标记,表示这是页面的入口点。

入口组件使用@Component装饰器定义,结构体名为SneakerLabApp。组件内部使用@State装饰器定义了一个状态变量activeTab,用于跟踪当前激活的Tab页,初始值为首页。

@State装饰器是ArkTS中最基础的状态管理装饰器。被@State标记的变量会被框架追踪,当变量值发生变化时,框架会自动重新渲染依赖该状态的UI部分。这种响应式的状态管理是声明式UI的核心机制。

入口组件的build()方法描述了应用的整体布局。整个应用采用垂直方向的三栏式布局:顶部是头部导航栏,中间是内容展示区,底部是Tab导航栏。这种布局结构是移动端应用的经典布局模式,符合用户的使用习惯。

3.2 顶部头部栏的实现

顶部头部栏采用水平方向的Row布局,包含三个部分:左侧的品牌标识区、中间的弹性空间、右侧的功能图标区。

品牌标识区使用Column垂直布局,上方是"SNEAK LAB"的品牌名称,使用了较大的字号、粗体和字间距,营造出潮流品牌的视觉效果。下方是副标题"球鞋定制工坊 · CUSTOM YOUR SOLE",使用了较小的字号和电光绿配色,与品牌名形成视觉层次。

功能图标区包含两个图标:通知图标和消息图标。通知图标使用Stack叠加布局,在铃铛图标上叠加了一个红色的小圆点,表示有未读通知。这种红点提示是移动应用中常见的通知提醒方式。消息图标则是一个简单的信封形状。

整个头部栏有底部边框,用于与内容区进行视觉分隔。背景色与应用背景色一致,保持了视觉的连贯性。

3.3 内容区与Tab切换逻辑

内容区是应用的核心展示区域,使用Stack堆叠布局。Stack布局的特点是子组件会堆叠在一起,后添加的组件会覆盖在先添加的组件之上。在本应用中,六个Tab页的内容都被放置在同一个Stack中,通过条件渲染来控制哪个Tab页显示。

条件渲染使用了if...else if...else的链式判断结构。根据activeTab状态变量的值,决定渲染哪个Tab组件。当activeTab为首页时,渲染首页Tab组件;为球鞋时,渲染球鞋Tab组件,以此类推。

这种条件渲染的Tab切换方式有其优缺点。优点是实现简单直观,每个Tab组件的生命周期与Tab的显示状态同步。缺点是每次切换Tab都会销毁和重建组件,导致组件状态丢失,而且对于复杂的Tab页,切换时可能会有性能开销。

首页Tab组件接收一个onGoTab回调函数参数,用于从首页跳转到其他Tab页。这种通过回调函数进行父子组件通信的方式,是ArkTS中组件间通信的常用模式。

3.4 底部Tab栏的设计与实现

底部Tab栏是应用的主导航入口,采用水平方向的Row布局,包含六个Tab项,分别对应首页、球鞋、配色、护理、客制和我的六个页面。

每个Tab项使用Column垂直布局,上方是图标,下方是文字。图标使用特殊的Unicode字符来表示,这是一种轻量级的图标方案,不需要额外的图标资源文件。文字使用较小的字号,确保Tab栏的紧凑性。

Tab项的选中状态通过颜色来区分。选中的Tab使用电光绿作为图标和文字的颜色,未选中的Tab使用辅助灰色。这种颜色对比清晰地表达了当前的激活状态。

每个Tab项都绑定了点击事件,点击时将activeTab状态设置为对应的枚举值。由于activeTab@State状态变量,修改它会触发UI的重新渲染,从而切换到对应的Tab页。

Tab栏的高度为58像素,背景色为卡片色,顶部有一条分割线。这些设计细节与整体的视觉风格保持一致。

3.5 页面导航流程图

应用启动

点击球鞋Tab/功能入口

点击配色Tab/功能入口

点击护理Tab/功能入口

点击客制Tab/功能入口

点击我的Tab/功能入口

点击首页Tab

点击配色Tab

点击护理Tab

点击客制Tab

点击我的Tab

点击首页Tab

点击球鞋Tab

点击护理Tab

点击客制Tab

点击我的Tab

点击首页Tab

点击球鞋Tab

点击配色Tab

点击客制Tab

点击我的Tab

点击首页Tab

点击球鞋Tab

点击配色Tab

点击护理Tab

点击我的Tab

点击首页Tab

点击球鞋Tab

点击配色Tab

点击护理Tab

点击客制Tab

点击编辑参数

保存/关闭

点击方案选择

应用/取消

点击产品

加入清单/关闭

点击发起定制

提交/关闭

点击删除收藏

确认删除/取消

Home

Shoes

Color

Care

Custom

Mine

EditModal

SchemeModal

CareDetailModal

AddCustomModal

DeleteConfirmModal

四、首页Tab深度解析

4.1 首页整体结构

首页是用户进入应用后看到的第一个页面,承担着内容展示、功能导航和氛围营造的重要作用。本应用的首页采用了纵向滚动的布局结构,从上到下依次排列了鞋型仪表盘、限量发售横滑、本周热销榜和功能导航宫格四个模块。

首页组件定义了一个状态变量snLaceY,用于控制鞋带摆动动画的位置。这个状态变量通过setInterval定时器周期性地更新,从而驱动鞋带的上下摆动效果。

aboutToAppear()aboutToDisappear()是组件的生命周期钩子函数。aboutToAppear()在组件即将显示时调用,在这里启动了定时器。aboutToDisappear()在组件即将消失时调用,在这里清除定时器,避免内存泄漏。

这种生命周期管理对于包含定时器或订阅的组件来说至关重要。如果不在组件销毁时清除定时器,定时器会继续运行,导致内存泄漏和性能问题。ArkTS提供了完善的生命周期钩子函数,帮助开发者正确管理组件的生命周期。

4.2 鞋型仪表盘与鞋带动画

鞋型仪表盘是首页最上方的一个大卡片模块,用于展示平台的核心数据指标。这个模块分为上下两部分:上方是鞋带摆动动画区域,下方是三个数据指标。

鞋带摆动动画是本应用的一个视觉亮点。它通过模拟鞋带的上下摆动效果,营造出一种动态的、有生命力的视觉体验。这个动画完全使用声明式的方式实现,没有使用任何动画API,而是通过状态变量驱动UI变化来实现动画效果。

具体实现方式是:定义一个Y轴偏移量的状态变量,通过定时器周期性地增加这个偏移量,然后使用取模运算让偏移量在0到40之间循环。每个鞋带条使用translate变换属性,根据这个偏移量计算出对应的Y轴位移。

为了让鞋带摆动看起来更加自然,不同位置的鞋带条使用了不同的相位偏移。第一排的五个鞋带条分别有32、24、16、8、0的相位差,第二排的四个鞋带条分别有28、20、12、4的相位差。这种相位差使得鞋带条的摆动不是同步的,而是呈现出波浪式的递进效果,更加接近真实鞋带的摆动状态。

动画区域的背景使用了从深紫色到黑色的线性渐变,营造出科技感和深度感。左上角还有"LACE SYSTEM"的文字水印,增强了仪表盘的专业感。

数据指标区域展示了三个核心指标:今日热度指数96.2、定制单进行中128、本周新品6。每个指标使用垂直布局,上方是数值,下方是说明文字。指标之间使用竖线分隔,形成了清晰的视觉分区。

三个指标分别使用了不同的颜色:热度指数使用电光绿强调色,定制单使用暗夜紫主色调,新品数使用主文字色。这种颜色分配既符合各指标的语义,又形成了丰富的视觉层次。

4.3 限量发售横滑模块

限量发售模块展示了平台上的限量款球鞋,采用了横向滚动的展示方式。这种横滑布局在移动端非常常见,可以在有限的屏幕空间内展示更多的内容,同时也符合用户的滑动操作习惯。

横滑区域使用了Scroll组件配合水平方向的Row布局来实现。Scroll组件的scrollable属性设置为ScrollDirection.Horizontal,表示只允许水平方向滚动。scrollBar属性设置为BarState.Off,隐藏了滚动条,使界面更加简洁。

每个限量球鞋卡片使用垂直布局,从上到下依次是渐变色块、球鞋名称和价格。渐变色块使用了球鞋的主色调从左上角向右下角渐变到深卡片色,这种渐变效果使得色块更加有层次感和质感。

在色块的左上角叠加了一个"LIMITED"标签,使用电光绿背景和深色文字,突出了限量的稀缺属性。标签使用了圆角矩形的形状,与整体的圆角设计语言保持一致。

球鞋名称使用了单行省略的处理方式,当名称过长时会显示省略号,避免了布局错乱。价格使用了电光绿加粗的样式,突出了价格信息的重要性。

限量球鞋数据通过snLimitedShoes()辅助函数获取,这个函数筛选出所有标记为限量的球鞋。使用ForEach组件遍历数据数组,动态生成对应的卡片组件。ForEach的第二个参数是键生成函数,用于为每个列表项生成唯一的键,帮助框架高效地更新列表。

4.4 本周热销榜模块

本周热销榜展示了热度排名前五的球鞋,采用了排行榜的形式。排行榜是电商应用中常见的内容组织方式,可以激发用户的从众心理和购买欲望。

排行榜的每条记录包含排名序号、球鞋图片、球鞋信息和价格四个部分。排名序号使用了不同的颜色来区分前三名:第一名使用警告色(金色),第二名使用主文字色(银色),第三名及之后使用辅助文字色(铜色/普通)。这种颜色区分强化了排名的荣誉属性。

球鞋图片使用了渐变色块来模拟,同样采用了从球鞋主色调到深卡片色的线性渐变。球鞋信息区域包含球鞋名称、品类标签和热度值,以及一个热度进度条。

品类标签使用了紫色背景和紫色文字的胶囊形状,与主色调保持一致。热度值使用了辅助文字色,作为次要信息展示。热度进度条使用了从主色调到强调色的线性渐变,视觉效果非常炫酷。

进度条的实现使用了Stack堆叠布局:底层是灰色的背景条,上层是彩色的进度条。进度条的宽度通过热度值的百分比来控制,热度值越高,进度条越长。这种进度条的实现方式简单而有效,是UI开发中的常用技巧。

价格信息使用了电光绿加粗的样式,与整体的价格展示风格保持一致。

热销榜数据通过snTopShoes(5)辅助函数获取,这个函数先按热度排序,然后截取前五名。这种函数组合的方式体现了单一职责原则,每个函数只做一件事,然后通过组合来实现复杂的功能。

4.5 功能导航宫格模块

功能导航宫格是首页的底部模块,提供了八个功能入口的快捷访问。宫格布局使用Grid组件实现,设置为四列等宽布局。

Grid组件是ArkTS中用于网格布局的强大组件。通过columnsTemplate属性可以定义列的数量和宽度,columnsGaprowsGap属性则分别定义列间距和行间距。与使用Row和Column嵌套相比,Grid组件在处理规则网格布局时更加简洁和高效。

每个功能入口使用垂直布局,上方是图标,下方是功能名称。图标使用特殊的Unicode字符表示,颜色为电光绿强调色。功能名称使用辅助文字色,字号较小。

功能入口点击后会调用onGoTab回调函数,跳转到对应的Tab页。这个回调函数是从父组件传入的,通过这种方式,子组件可以通知父组件进行状态变更,而不需要直接持有父组件的引用。

这种"数据驱动 + 回调通信"的设计模式是声明式UI开发中的最佳实践。子组件只负责展示数据和触发事件,真正的状态管理和业务逻辑由父组件或更高层级来处理。这种分层设计使得组件更加独立、可复用性更强。

4.6 首页渲染流程图

组件初始化

初始化snLaceY状态

aboutToAppear生命周期

启动setInterval定时器

每50ms更新snLaceY值

触发UI重新渲染

计算每条鞋带的Y轴偏移

应用translate变换

ApplyTrigger

渲染鞋型仪表盘

渲染限量发售横滑

渲染本周热销榜

渲染功能导航宫格

鞋带摆动动画

三个数据指标

横向Scroll容器

ForEach遍历限量球鞋

生成球鞋卡片

ForEach遍历Top5球鞋

生成排行榜项

排名序号

球鞋信息

热度进度条

Grid四列布局

ForEach遍历功能项

生成功能入口

点击功能入口

调用onGoTab回调

父组件activeTab变更

Tab页切换

组件即将消失

清除定时器

释放资源

五、球鞋Tab深度解析

5.1 球鞋Tab整体架构

球鞋Tab是应用的商品展示核心页面,用户可以在这里浏览所有球鞋、按品类筛选、查看尺码库存和人气排行。球鞋Tab的整体布局采用了纵向滚动的结构,从上到下依次是分类筛选栏、球鞋双列Grid、尺码对比条和人气柱状图。

球鞋Tab定义了四个状态变量:shoeCat用于记录当前选中的品类,初始值为"全部";showEdit用于控制编辑参数弹框的显示状态;editShoe用于存储当前编辑的球鞋对象;editTargeteditHeat分别用于存储编辑弹框中的目标价格和热度阈值。

这种状态设计体现了"状态提升"的思想。编辑弹框的显示状态和编辑数据都由父组件管理,弹框本身只是一个纯展示的@Builder方法。这种设计使得状态管理更加集中,数据流更加清晰。

5.2 分类筛选栏的实现

分类筛选栏是球鞋Tab的顶部组件,提供了全部、跑步、篮球、休闲、滑板五个品类的筛选功能。筛选栏采用水平方向的Row布局,五个分类标签并排排列。

每个分类标签是一个Text组件,通过不同的背景色和文字色来区分选中和未选中状态。选中的标签使用电光绿背景和深色文字,未选中的标签使用卡片色背景和辅助文字色。这种胶囊形状的标签设计在移动端筛选场景中非常常见。

点击分类标签时,将shoeCat状态变量设置为对应的品类名称。由于shoeCat@State状态变量,修改它会触发UI的重新渲染,从而更新标签的选中状态和下方的球鞋列表。

分类筛选的实现逻辑非常简洁:所有可选的分类值直接硬编码在模板中,而不是通过数据驱动。对于分类数量较少且相对固定的场景,这种硬编码的方式更加简单直接。如果分类数量较多或需要动态变化,则应该考虑使用数据驱动的方式。

5.3 双列球鞋Grid布局

球鞋展示区采用了双列Grid布局,这是电商应用中最常见的商品展示方式之一。双列布局在信息密度和可读性之间取得了良好的平衡,既能够展示足够多的商品,又不会因为过于拥挤而影响浏览体验。

Grid组件通过columnsTemplate('1fr 1fr')设置为两列等宽布局,columnsGaprowsGap分别设置列间距和行间距。Grid的高度通过计算得出:球鞋数量除以2向上取整,再乘以每个卡片的高度(252像素)。这种动态高度计算确保了Grid容器能够完整容纳所有内容。

每个球鞋卡片使用垂直布局,从上到下依次是:渐变色块区域、球鞋名称、品类和热度信息、价格信息、编辑参数按钮。

渐变色块区域是卡片的视觉主体,使用了球鞋主色调到深色的线性渐变。在色块的右上角叠加了一个标签,显示球鞋的标签信息(如新品、热卖、经典等)。标签使用警告色背景和深色文字,起到了醒目的标记作用。

球鞋名称使用了单行省略的处理方式,确保名称不会因为过长而破坏布局。品类和热度信息使用了较小的字号,品类文字使用主色调,中间用一个点号分隔,热度使用辅助文字色。

价格区域包含现价和原价两部分。现价使用电光绿加粗的样式,是视觉焦点。原价使用辅助文字色并添加了删除线,用于对比展示折扣力度。

编辑参数按钮使用了深卡片色背景和辅助文字色,点击后会弹出编辑参数弹框。这个按钮的存在使得用户可以直接从商品列表进入参数编辑,提升了操作效率。

球鞋数据通过snFilterShoes(this.shoeCat)辅助函数获取,这个函数根据当前选中的品类返回对应的球鞋列表。当品类发生变化时,这个函数的返回值也会变化,从而触发Grid的重新渲染。

值得注意的是ForEach的键生成函数中包含了this.shoeCat。这是因为当品类切换时,Grid中的数据会完全变化,如果键只包含球鞋id,框架可能会错误地复用旧的组件实例。将品类信息加入键中,可以确保品类切换时组件会被正确地重建。

5.4 尺码对比条模块

尺码对比条模块展示了不同尺码的库存对比情况,帮助用户了解各尺码的供需关系。这个模块使用了横向进度条的形式,每个尺码对应一条进度条,进度条的长度表示库存的多少。

进度条的实现使用了Stack堆叠布局:底层是深灰色的背景条,上层是渐变色的进度条。进度条的宽度通过当前尺码库存除以最大库存再乘以100%来计算。渐变色从主色调到强调色,与整体的渐变设计语言保持一致。

每个尺码行包含三个部分:尺码数字、进度条、库存件数。尺码数字左对齐,库存件数右对齐,进度条占据中间的剩余空间。这种三栏式布局是列表展示的经典模式。

尺码数据和库存数据使用了两个平行数组SN_SIZESSN_SIZE_STOCK,通过索引来对应。这种设计在数据量较小且关系简单时非常简洁,但在复杂场景下可能不如对象数组直观。

最大库存值通过snMaxSizeStock()辅助函数计算,这个函数遍历库存数组找出最大值。由于最大值是固定的,每次渲染时都会重新计算,这在数据量较小时不会有性能问题,但如果数据量很大,可以考虑使用缓存来优化。

5.5 人气柱状图模块

人气柱状图模块以柱状图的形式展示了全系列球鞋的热度排名。柱状图是数据可视化的基础图表类型,能够直观地展示数据之间的大小对比。

柱状图的实现完全使用基础组件构建,没有使用专门的图表库。每根柱子是一个Column组件,高度通过热度值乘以一个系数(1.05)来计算。前三名的柱子使用电光绿强调色,其余使用深主色调,通过颜色区分突出了前三名的特殊地位。

每根柱子的顶部显示具体的热度值,底部显示排名序号。柱子从底部向上生长,通过justifyContent(FlexAlign.End)实现底部对齐的效果。

整个柱状图区域使用Row布局,通过justifyContent(FlexAlign.SpaceBetween)让柱子均匀分布。Row的高度固定为160像素,确保所有柱子在同一基线上对齐。

人气数据通过snHotShoes()辅助函数获取,这个函数返回按热度降序排列的球鞋列表。柱状图的排列顺序与热度排序一致,最热门的球鞋排在最左边。

这种纯组件实现的柱状图虽然功能相对简单,但具有很好的灵活性和定制性。开发者可以完全控制柱子的样式、动画和交互,而不受图表库API的限制。

5.6 编辑参数弹框详解

编辑参数弹框是球鞋Tab中的一个交互组件,用户可以在这里调整目标入手价和热度提醒阈值。弹框使用@Builder装饰器定义为一个构建方法,通过条件渲染来控制显示和隐藏。

弹框采用了居中弹出的样式,背景是半透明的黑色遮罩层,中间是弹框内容。遮罩层使用#A6000000的颜色值,其中A6是alpha通道的十六进制值,约等于65%的不透明度。弹框内容使用卡片色背景,带有圆角和边框。

弹框的内容分为四个部分:标题区、价格滑块、热度滑块、操作按钮区。

标题区显示弹框标题"编辑定制参数"和当前编辑的球鞋名称,底部有一条分割线。这种标题区的设计是弹框组件的标准模式。

价格滑块区域包含标签行和滑块两部分。标签行左侧是"目标入手价"的说明文字,右侧是当前的价格数值,使用电光绿加粗突出显示。滑块使用Slider组件,最小值600,最大值2200,步长50,样式为OutSet(滑块在轨道外侧)。

滑块的选中轨道颜色为主色调,未选中轨道颜色为分割线色,滑块按钮颜色为强调色。这种配色方案使得滑块的当前位置清晰可见。滑块的值变化通过onChange事件监听,更新到对应的状态变量中。

热度滑块区域的结构与价格滑块类似,区别在于颜色配置:选中轨道颜色为强调色,滑块按钮颜色为主色调。这种颜色互换的设计形成了有趣的视觉对比。

操作按钮区包含"重置"和"保存参数"两个按钮。重置按钮使用深卡片色背景和辅助文字色,点击后将价格和热度恢复为默认值。保存按钮使用主色调背景和深色文字,点击后关闭弹框。

弹框的关闭只改变了showEdit状态,并没有真正保存数据。在实际应用中,这里应该调用保存接口或更新数据,但在原型阶段,简单的关闭就足够了。

5.7 状态管理状态图

组件初始化

shoeCat = 全部

showEdit = false

点击跑步

点击篮球

点击休闲

点击滑板

点击全部

点击篮球

点击休闲

点击滑板

点击全部

点击跑步

点击休闲

点击滑板

点击全部

点击跑步

点击篮球

点击滑板

点击全部

点击跑步

点击篮球

点击休闲

点击球鞋卡片

点击保存/点击遮罩

Init

ShowAll

HideEdit

ShowRun

ShowBasket

ShowCasual

ShowSkate

ShowEdit

editTarget=1500, editHeat=70

拖动价格滑块

拖动热度滑块

拖动热度滑块

拖动价格滑块

点击重置

点击重置

恢复默认值

DefaultValues

AdjustPrice

AdjustHeat

ResetValues

六、配色Tab深度解析

6.1 配色Tab整体架构

配色Tab是应用的色彩探索页面,用户可以在这里浏览各种潮流配色、查看色系分布、选择配色方案。配色Tab的整体布局从上到下依次是喷漆调色台、潮流色卡Grid、色系分布饼图和配色热度排行。

配色Tab定义了四个状态变量:snSprayY用于控制喷漆粒子动画的位置;showScheme用于控制配色方案弹框的显示状态;selScheme用于记录弹框中选中的配色方案索引;mainColorIdx用于记录当前选中的主色调索引。

除了状态变量,配色Tab还定义了两个私有成员变量:sprayTimer用于存储喷漆动画的定时器ID;pieCtx是一个Canvas 2D渲染上下文对象,用于绘制饼图。

Canvas是HarmonyOS ArkTS API 24提供的强大图形绘制能力。通过Canvas API,开发者可以在画布上绘制各种图形、文字和图片,实现复杂的自定义绘制效果。在本应用中,Canvas被用于绘制色系分布饼图。

6.2 喷漆调色台动画

喷漆调色台是配色Tab的顶部视觉模块,模拟了喷漆下落的动画效果。这个动画模块营造了色彩创作的氛围,与配色主题高度契合。

喷漆动画的实现原理与首页的鞋带动画类似,都是通过定时器驱动状态变量变化,再由状态变量驱动UI变化。不同的是,喷漆动画模拟的是粒子下落的效果,而鞋带动画模拟的是摆动效果。

喷漆粒子使用Circle组件实现,共有八个不同颜色、不同大小的粒子。每个粒子有不同的水平位置和相位偏移,使得粒子下落看起来更加自然和随机。

粒子的Y轴位置通过snSprayY状态变量加上各自的相位偏移后取模计算得出。粒子的不透明度也随着Y轴位置变化,位置越低,不透明度越低,模拟了粒子逐渐消散的效果。

在喷漆粒子的下方,有一个当前色的展示区域,包含一个圆形的色样和当前色的名称、系列、色值信息。当前色由mainColorIdx状态变量控制,用户可以通过点击下方的色卡来切换当前色。

喷漆调色台的背景使用了从深紫色到黑色的线性渐变,与首页仪表盘的背景风格一致。左上角有"喷漆调色台"的标题和"喷涂中"的状态指示,右上角有一个向下的箭头图标,表示喷漆的方向。

6.3 潮流色卡Grid

潮流色卡Grid以三列网格的形式展示了所有可选的配色。每个色卡包含一个色块、配色名称和系列热度信息。

色卡的选中状态通过边框来区分。选中的色卡使用1.5像素的电光绿边框,未选中的使用1像素的分割线色边框。这种边框粗细和颜色的变化清晰地表达了选中状态。

点击色卡时,将mainColorIdx状态变量设置为对应的索引。由于mainColorIdx@State状态变量,修改它会触发UI的重新渲染,从而更新色卡的选中边框和喷漆调色台的当前色显示。

Grid组件设置为三列等宽布局,高度固定为470像素。由于配色数量固定为8个,三列布局需要三行(前两行各3个,第三行2个),因此高度可以固定计算。

"方案选择"按钮位于模块标题的右侧,点击后弹出配色方案选择弹框。这个按钮使用电光绿文字,与普通的"查看更多"链接有所区别,暗示了这是一个更重要的操作入口。

6.4 色系分布饼图与Canvas绘制

色系分布饼图是配色Tab的核心数据可视化模块,展示了各种配色的热度占比。这个饼图完全使用Canvas API绘制,展示了ArkTS中自定义绘制的强大能力。

Canvas的使用分为两步:首先创建一个Canvas渲染上下文对象,然后在组件的onReady回调中调用绘制函数。onReady回调在Canvas组件准备就绪后触发,是开始绘制的最佳时机。

饼图的绘制逻辑封装在drawPie()方法中。绘制过程如下:

首先,遍历配色列表,为每个配色绘制一个扇形。扇形的绘制使用了Canvas的路径API:先调用beginPath()开始一条新路径,然后调用moveTo()移动到圆心位置,接着调用arc()绘制圆弧,最后调用closePath()闭合路径。

arc()方法接收六个参数:圆心x坐标、圆心y坐标、半径、起始角度、结束角度。起始角度和结束角度的计算是饼图绘制的关键。总角度为2π(即360度),每个扇形的角度占比等于该配色热度占总热度的比例。

为了让饼图从12点钟方向开始(而不是默认的3点钟方向),起始角度需要减去π/2(即90度)。前序热度和通过snHeatBefore()辅助函数计算,用于确定每个扇形的起始位置。

每个扇形使用对应配色的颜色进行填充。调用fillStyle设置填充颜色,然后调用fill()方法填充路径。

绘制完所有扇形后,再绘制一个内圆,将饼图变成环形。内圆使用卡片色填充,与背景融为一体,形成了镂空的效果。内圆的边框使用背景色,线宽为3像素,增加了环形的清晰度。

饼图的右侧是图例列表,每个图例包含一个颜色方块、配色名称和占比百分比。占比通过热度值除以总热度再乘以100%计算得出,保留一位小数。

图例与饼图配合使用,用户可以直观地了解每种配色的热度占比情况。这种"图表 + 图例"的组合是数据可视化的标准模式。

6.5 配色热度排行

配色热度排行以横向进度条的形式展示了所有配色的热度排名。这个模块的设计与球鞋Tab中的尺码对比条类似,但展示的内容和样式有所不同。

每条排行记录包含配色名称、热度进度条和热度数值三部分。配色名称左对齐,热度数值右对齐,进度条占据中间空间。

进度条的实现同样使用Stack堆叠布局,底层是深卡片色背景,上层是对应配色的颜色填充。进度条的宽度直接使用热度值作为百分比(因为热度值本身就是百分制的)。

热度数据通过snTopColors()辅助函数获取,这个函数返回按热度降序排列的配色列表。排行的顺序与热度排序一致,最热的配色排在最上面。

热度排行模块与色系分布饼图形成了互补:饼图展示了各配色的相对占比,而热度排行则展示了具体的热度数值和排名顺序。两种不同的可视化方式从不同角度呈现了同一组数据。

6.6 配色方案选择弹框

配色方案选择弹框提供了更加集中的配色选择界面。与直接点击色卡相比,弹框形式的选择可以提供更多的上下文信息和操作选项。

弹框的标题区包含标题文字和关闭按钮。标题下方有一行说明文字"点击色卡设为默认喷涂主色",引导用户进行操作。

配色选择区使用三列Grid布局,每个色卡包含一个色块和配色名称。选中的色卡有电光绿边框,并且色块上叠加了一个对勾标记。对勾的颜色根据背景色的深浅进行了调整:炭晶黑(id为3)的对勾使用电光绿,其他颜色的对勾使用深色,确保了在不同背景上的可读性。

弹框底部有"取消"和"应用方案"两个按钮。取消按钮使用深卡片色背景,点击后关闭弹框但不应用选择。应用方案按钮使用电光绿背景,点击后将选中的配色方案应用为主色调,然后关闭弹框。

应用方案的逻辑是将selScheme赋值给mainColorIdx。这里有一个细节值得注意:弹框中的选择(selScheme)和实际应用的主色调(mainColorIdx)是两个独立的状态变量。用户在弹框中选择配色后,只有点击"应用方案"才会真正生效,点击"取消"则放弃选择。这种"预选-确认"的交互模式给了用户更多的控制权,避免了误操作。

6.7 Canvas绘制流程图

组件初始化

创建CanvasRenderingContext2D

build方法渲染Canvas组件

Canvas onReady回调触发

调用drawPie方法

遍历配色列表

调用beginPath开始新路径

moveTo移动到圆心

计算扇形起始角和结束角

arc绘制圆弧

closePath闭合路径

设置fillStyle为对应配色

fill填充扇形

是否还有下一个配色?

绘制内圆

beginPath开始内圆路径

arc绘制整圆

fill填充内圆卡片色

stroke绘制内圆边框

饼图绘制完成

mainColorIdx变化

更新当前色显示

重新渲染喷漆粒子

七、护理Tab深度解析

7.1 护理Tab整体架构

护理Tab展示了球鞋护理相关的产品和信息,包括护理好物排行榜和护理用量分布两个主要模块。护理Tab的设计相对简洁,但在交互细节上有其独到之处。

护理Tab定义了两个状态变量:showCare用于控制护理详情弹框的显示状态;selCare用于存储当前选中的护理产品对象。

这两个状态变量的设计模式与球鞋Tab中的编辑弹框类似:一个布尔值控制弹框的显隐,一个对象存储弹框的数据。这种模式是弹框交互的标准设计,在整个应用中被多次使用。

7.2 护理好物榜

护理好物榜是护理Tab的核心模块,以排行榜的形式展示了护理产品的功效排名。排行榜前三名有特殊的颜色标识,突出了荣誉属性。

每条排行榜记录包含排名徽章、产品信息和功效进度条三个主要部分。

排名徽章使用圆形设计,不同名次有不同的颜色:第一名使用警告色(金色),第二名使用次文字色(银色),第三名使用铜色,第四名及以后使用深卡片色。徽章中的排名数字使用对比色,确保在不同背景上的可读性。

产品信息区域包含产品名称、价格、功效描述和功效进度条。产品名称和价格在同一行,名称左对齐,价格右对齐。价格使用电光绿加粗,与整体的价格展示风格一致。

功效描述使用较小的字号和辅助文字色,作为产品的简要说明。功效进度条使用Stack布局实现,底层是灰色背景,上层是彩色进度。前三名的进度条使用电光绿强调色,其余使用主色调。这种颜色区分与排名徽章的颜色体系相呼应。

进度条下方有一行辅助信息,左侧显示功效数值,右侧显示"详情"链接。点击整条记录可以查看产品详情,"详情"链接起到了视觉提示的作用。

护理产品数据通过snTopCares()辅助函数获取,这个函数返回按功效降序排列的护理产品列表。排行榜的顺序与功效排序一致。

7.3 护理用量分布

护理用量分布模块展示了各护理产品的使用占比情况。这个模块采用了横向进度条的形式,与配色Tab中的热度排行和球鞋Tab中的尺码对比条类似,但样式有所不同。

每条用量记录包含产品名称、用量进度条和用量百分比三部分。产品名称固定宽度为76像素,确保左对齐整齐。用量百分比右对齐,使用主文字色。进度条占据中间的剩余空间。

进度条使用了从主色调到浅紫色的线性渐变,与其他模块的单色进度条形成了区分。渐变进度条在视觉上更加丰富,也与护理产品的"渐变功效"概念相呼应。

用量百分比的计算方式是使用量值乘以2。这里隐含了一个假设:使用量的最大值是50%,乘以2后得到100%的进度条宽度。这种设计使得用量数据在进度条上有更好的展示效果,避免了因为数值偏小而导致进度条过短的问题。

护理用量分布数据直接使用SN_CARES数组,没有进行排序。展示顺序与数据定义的顺序一致,即按产品ID排序。这与好物榜的按功效排序形成了对比,两个模块从不同维度展示了护理产品的信息。

7.4 护理详情弹框

护理详情弹框展示了护理产品的详细参数信息,采用了参数表的形式。与球鞋Tab的编辑弹框和配色Tab的方案弹框不同,护理详情弹框主要用于信息展示,交互操作较少。

弹框的顶部是标题区,包含一个紫色的产品图标、产品详情标题和产品名称,以及关闭按钮。产品名称使用电光绿,突出了产品标识。标题区底部有一条分割线。

参数表部分采用了多行列布局,每行包含参数名称和参数值两列。参数名称左对齐,使用辅助文字色;参数值右对齐,使用主文字色。每行底部有一条分割线,形成了清晰的表格结构。

参数表包含以下参数:产品名称、主要功效、功效指数、使用占比、产品容量、适用材质、参考价格。这些参数覆盖了护理产品的核心信息,用户可以通过这些参数全面了解产品的特点。

其中,功效指数和参考价格使用了特殊的样式突出显示。功效指数使用电光绿加粗,参考价格使用较大字号的电光绿加粗。这种视觉强调反映了这两个参数在用户购买决策中的重要性。

弹框的底部是一个"加入护理清单"按钮,使用电光绿背景和深色文字,占据全宽。点击按钮后关闭弹框,模拟了加入清单的操作。

护理详情弹框的设计体现了"信息展示为主,操作为辅"的原则。大部分空间用于展示产品信息,只有一个主要操作按钮。这种设计适用于详情查看类的弹框场景。

7.5 弹窗交互流程图

用户进入护理Tab

浏览护理好物榜

点击产品项

设置selCare和showEdit=true

显示详情弹框

渲染标题区

渲染参数表

渲染加入清单按钮

显示关闭按钮

产品名称行

主要功效行

功效指数行

使用占比行

产品容量行

适用材质行

参考价格行

点击关闭按钮

隐藏弹框

点击遮罩层

隐藏弹框

点击加入护理清单

隐藏弹框

显示添加成功提示

返回列表

向上滚动

查看护理用量分布

向下滚动

八、客制Tab深度解析

8.1 客制Tab整体架构

客制Tab是应用的定制服务页面,用户可以在这里了解定制方案、发起定制订单、查看工艺统计。客制Tab的整体布局从上到下依次是发起定制入口、热门客制方案列表和工艺单量统计柱状图。

客制Tab定义了四个状态变量:showAdd用于控制新增定制弹框的显示状态;newName用于存储输入的鞋款名称;selSize用于存储选中的尺码,初始值为41;selColorIdx用于存储选中的配色索引,初始值为0。

这四个状态变量分别对应弹框中的三个输入项和弹框的显隐控制。这种设计模式与其他Tab中的弹框设计保持一致,体现了整个应用交互模式的统一性。

8.2 发起定制入口卡

发起定制入口卡是客制Tab最上方的一个大卡片,是发起定制订单的主要入口。这个卡片采用了渐变背景和大号加号按钮的设计,视觉上非常醒目。

卡片的左侧是文字说明区域,包含主标题"发起定制"和副标题"专属鞋款 · 专属配色 · 专属工艺"。主标题使用较大的字号和粗体,副标题使用较小的字号和浅紫色。两行文字形成了清晰的视觉层次。

卡片的右侧是一个圆形的加号按钮,使用电光绿背景和深色加号图标。按钮的尺寸较大(52x52像素),具有很强的点击引导性。

整个卡片的背景使用了从深主色调到更深紫色的线性渐变,营造出高端定制的氛围。卡片的圆角为16像素,与其他卡片的圆角设计保持一致。

点击卡片的任意位置都会触发弹框显示,而不仅仅是点击加号按钮。这种大点击区域的设计提升了用户操作的便利性,符合移动端的交互设计原则。

8.3 热门客制方案列表

热门客制方案列表展示了六种定制工艺方案,采用了横向卡片的形式。每张卡片左侧是工艺图标区,右侧是方案详情区。

工艺图标区使用垂直布局,上方是工艺图标(使用Unicode字符表示),下方是工艺名称。图标区的背景使用了从深主色调到深卡片色的线性渐变,图标颜色为电光绿,工艺名称颜色为浅紫色。整个图标区有明确的视觉风格,突出了工艺属性。

方案详情区包含多个信息层级:

最上方是方案名称和价格,在同一行显示。名称左对齐,使用粗体主文字色;价格右对齐,使用电光绿粗体。这是电商卡片的标准布局方式。

名称下方是方案描述,使用较小的字号和辅助文字色。描述文字限制为单行,超出部分省略,确保卡片布局的整洁。

再往下是工期信息区域,包含工期文字和工期进度条。进度条显示当前工期占最长工期(30天)的比例。进度条的颜色根据工期长短而变化:工期超过10天使用警告色,10天以内使用主色调。这种颜色变化直观地反映了工期的长短,帮助用户快速了解方案的时间成本。

最下方是人气信息,包含人气文字和人气进度条。人气进度条使用电光绿,宽度固定为90像素。人气值以百分比的形式展示在进度条中。

方案卡片的底部有一条分割线,分隔了方案描述和详细参数。这种分区设计使得信息层次更加清晰。

定制方案数据直接使用SN_CUSTOMS数组,展示顺序与数据定义的顺序一致。

8.4 工艺单量统计柱状图

工艺单量统计柱状图展示了各种工艺的本月单量情况。这个柱状图的设计与球鞋Tab中的人气柱状图类似,但样式和数据有所不同。

每根柱子的顶部显示单量数值,底部显示工艺名称。柱子从底部向上生长,高度根据单量占最大单量的比例计算得出。

与球鞋Tab中纯色的柱子不同,工艺柱状图的柱子使用了从上到下的线性渐变,从电光绿渐变到主色调。这种渐变效果使得柱子更加有立体感和质感。

柱状图区域使用Row布局,通过justifyContent(FlexAlign.SpaceBetween)让柱子均匀分布。整个区域有14像素的内边距,确保柱子与边缘有足够的间距。

工艺统计数据使用SN_CRAFT_STATS数组,最大单量通过snMaxCraft()辅助函数计算。柱状图的总高度为150像素,柱子的最大高度为96像素(通过比例计算得出),剩余的空间用于显示顶部的数值和底部的名称。

这种纯组件实现的柱状图虽然简单,但对于展示少量分类的对比数据已经足够。如果需要更复杂的图表功能(如交互、动画、多系列等),可以考虑使用专门的图表库或基于Canvas进行更复杂的绘制。

8.5 新增定制弹框

新增定制弹框是客制Tab中的核心交互组件,用户可以在这里输入鞋款名称、选择尺码和配色,然后提交定制订单。

弹框包含三个输入区域:鞋款名称输入、尺码选择、配色选择。底部是提交按钮。

鞋款名称输入使用TextInput组件,提供了占位符文字提示。输入框使用深卡片色背景,有10像素的圆角和左右内边距。输入的文字实时更新到newName状态变量中。

尺码选择区域提供了38到44共七个尺码选项,使用横向排列的胶囊按钮形式。选中的尺码使用电光绿背景和深色文字,未选中的使用深卡片色背景和辅助文字色。点击尺码按钮更新selSize状态变量。

配色选择区域提供了八个配色选项,使用圆形色样的形式。选中的配色有2.5像素的电光绿边框,未选中的有1像素的分割线色边框。点击色样更新selColorIdx状态变量。

选中配色的下方有一行文字提示,显示当前选中的配色名称和系列。这行文字为用户提供了明确的选择反馈。

弹框的底部是"提交定制单"按钮,使用电光绿背景和深色文字,占据全宽。点击按钮后关闭弹框,模拟了提交定制单的操作。

新增定制弹框的设计体现了表单交互的最佳实践:每个输入项都有明确的标签说明,选中状态有清晰的视觉反馈,操作按钮醒目易点击。

8.6 组件依赖图

SneakerLabApp 入口组件

SNHomeTab 首页

SNShoeTab 球鞋

SNColorTab 配色

SNCareTab 护理

SNCustomTab 客制

SNMineTab 我的

编辑参数弹框

配色方案弹框

护理详情弹框

新增定制弹框

删除确认弹框

鞋带摆动动画

喷漆粒子动画

会员卡光泽动画

饼图Canvas绘制

SNShoe 球鞋接口

SNColorItem 配色接口

SNCareProduct 护理接口

SNCustomPlan 定制接口

SNOrderItem 订单接口

SNFavoriteItem 收藏接口

SNSpendItem 消费接口

SNFuncItem 功能项接口

SNCraftStat 工艺统计接口

SNColorPalette 调色板接口

snFilterShoes 过滤函数

snHotShoes 排序函数

snTopShoes TopN函数

snLimitedShoes 限量函数

snTopColors 配色排序

snTopCares 护理排序

snTotalHeat 总热度计算

snStatusColor 状态颜色

snMaxSpend 最大消费

snMaxCraft 最大单量

snMaxSizeStock 最大库存

九、我的Tab深度解析

9.1 我的Tab整体架构

我的Tab是应用的个人中心页面,展示了用户的会员信息、消费统计、订单列表和收藏列表。我的Tab是六个Tab中功能最丰富的一个,包含了多个数据展示模块和交互功能。

我的Tab定义了五个状态变量:snShine用于控制会员卡光泽动画的位置;showDel用于控制删除确认弹框的显示状态;delIdx用于记录待删除的收藏项索引;favList用于存储收藏列表数据,初始值从SN_FAVORITES常量获取。

除了状态变量,还有一个私有成员变量shineTimer用于存储光泽动画的定时器ID。

收藏列表使用@State装饰器标记,这意味着当数组内容发生变化时(如添加或删除元素),UI会自动更新。这是通过框架对数组的变异方法(如splice、push、pop等)进行拦截来实现的。

9.2 会员卡与光泽扫过动画

会员卡是我的Tab最上方的视觉焦点,展示了用户的会员等级、积分、优惠券和好评率等核心信息。会员卡最引人注目的是其光泽扫过动画效果。

光泽动画的实现原理是:在会员卡的渐变背景之上,叠加一个半透明的渐变光条,通过定时器不断改变光条的水平位置,营造出光泽在卡片上扫过的效果。

光条使用一个Column组件实现,背景是从完全透明到半透明白色再到完全透明的线性渐变。光条被旋转了16度,使得光泽看起来是斜着扫过的。光条的水平位置由snShine状态变量控制,通过translate变换属性实现位移。

定时器每40毫秒将snShine的值增加6,当光条移动到卡片右侧之外时,重置到左侧重新开始。这个循环过程创造了持续的光泽扫过效果。

会员卡的内容分为三个层级:最上方是会员标识区,中间是会员编号和等级,最下方是数据指标区。

会员标识区左侧是"SNEAK VIP"的会员品牌名,使用电光绿和字间距,营造出高端感。右侧是一个大号的星形图标,使用深紫色,作为装饰元素。

会员编号区左侧是会员编号"NO. 20260826",使用浅紫色和字间距,模仿了会员卡的钢印效果。右侧是会员等级"S7 黑金会员",使用辅助文字色。

数据指标区展示了三个核心指标:积分12860、优惠券12张、好评率96%。三个指标之间使用竖线分隔,形成清晰的分区。最右侧是一个"充值"按钮,使用电光绿背景,引导用户进行充值操作。

整个会员卡使用从主色调到深紫色再到黑色的线性渐变背景,边框为深主色调,圆角为18像素。卡片的视觉效果非常接近真实的会员卡,光泽动画则增添了动感和高级感。

9.3 月消费统计柱状图

月消费统计柱状图展示了用户近六个月的消费趋势。这个柱状图的设计与其他Tab中的柱状图类似,但展示的数据和细节有所不同。

柱状图有六根柱子,分别对应3月到8月的消费数据。每根柱子的顶部显示消费金额(以k为单位,保留一位小数),底部显示月份。柱子的高度根据消费金额占最大消费额的比例计算得出。

最高消费额的柱子使用电光绿强调色,其余柱子使用深主色调。这种颜色区分突出了消费最高的月份,帮助用户快速识别消费峰值。

消费数据使用SN_SPENDS数组,最大消费额通过snMaxSpend()辅助函数计算。柱状图的总高度为140像素,柱子的最大高度为88像素。

月消费统计是个人中心常见的数据可视化模块,帮助用户了解自己的消费习惯和趋势。通过简单的柱状图,用户可以直观地看到消费的波动情况。

9.4 我的订单列表

我的订单列表展示了用户的历史订单记录。每条订单记录包含商品缩略图、商品信息和箭头图标三部分。

商品缩略图使用渐变色块实现,从订单商品的主色调渐变到深卡片色。缩略图的尺寸为46x46像素,圆角为10像素。

商品信息区域包含两行内容:上方是商品名称和订单状态,下方是下单日期和价格。商品名称左对齐,订单状态右对齐。订单状态的颜色通过snStatusColor()辅助函数根据状态文本动态计算:已发货和已完成为电光绿,定制中为主色调,待付款为警告色,其他状态为辅助灰色。

日期和价格信息使用较小的字号和辅助文字色,作为次要信息展示。价格信息直接拼接在日期后面,用一个点号分隔。

每条记录的右侧有一个箭头图标,表示可以点击进入详情。这个箭头图标是列表项的常见视觉提示,暗示了"更多"或"进入"的含义。

订单数据使用SN_ORDERS数组,展示顺序与数据定义的顺序一致,即按下单时间倒序排列(最新的订单在最上面)。

9.5 我的收藏与删除功能

我的收藏列表展示了用户收藏的商品。与订单列表类似,每条收藏记录也包含商品色块、商品信息和操作按钮三部分。

商品色块直接使用收藏商品的主色调填充,而不是渐变色。这种纯色块的设计与订单列表的渐变色块形成了区分,也使得收藏列表看起来更加简洁。

商品信息区域包含商品名称和备注价格信息。名称使用主文字色,备注和价格使用辅助文字色。备注字段是收藏功能的特色,允许用户为每个收藏添加个性化的说明。

操作按钮是一个红色的叉号按钮,使用危险色背景(半透明)和危险色图标。点击这个按钮会弹出删除确认弹框,确认后删除对应的收藏项。

收藏列表的数据存储在favList状态变量中,这是一个@State装饰的数组。当调用splice()方法删除元素时,框架会检测到数组的变化,并自动更新UI。

删除操作的实现分为三步:首先点击删除按钮设置待删除的索引并显示确认弹框;然后在弹框中点击确认后调用removeFav()方法;最后removeFav()方法使用splice()删除数组元素,关闭弹框,重置索引。

这种"点击-确认-执行"的三步删除流程是删除操作的标准交互模式,通过确认弹框防止用户误删。

9.6 删除确认弹框

删除确认弹框是一个警告性质的弹框,用于在执行删除操作前向用户确认。这个弹框采用了红色警告主题,与其他弹框的紫色/绿色主题形成了明显的区分。

弹框的顶部是一个大号的警告图标(⚠),使用危险色,立即传达了警告的含义。警告图标下方是确认标题"确认删除该收藏?",使用粗体主文字色。

标题下方是一段说明文字,解释了删除的后果:"删除后将从收藏列表中移除,该操作不可恢复。"这段文字使用辅助文字色,居中对齐。

说明文字下方有一个提示条,显示即将删除的收藏项名称。提示条使用半透明的危险色背景和危险色文字,再次强调了删除的对象。

弹框的底部有两个按钮:“取消"和"确认删除”。取消按钮使用深卡片色背景和辅助文字色,确认删除按钮使用危险色背景和白色文字。两个按钮的颜色对比强烈,清晰地区分了"安全"和"危险"操作。

删除确认弹框的边框也使用了半透明的危险色,进一步强化了警告的视觉效果。

删除确认弹框的设计充分体现了"危险操作二次确认"的交互设计原则。通过醒目的警告图标、明确的说明文字和颜色区分的操作按钮,最大程度地降低了用户误操作的风险。

9.7 动画系统流程图

会员卡光泽动画

aboutToAppear

启动setInterval

每40ms snShine += 6

snShine > 420?

snShine = -80

保持当前值

触发重新渲染

光条translateX位移

光泽扫过效果

aboutToDisappear

clearInterval清除定时器

配色喷漆动画

aboutToAppear

启动setInterval

每40ms snSprayY += 5

snSprayY %= 160

触发重新渲染

计算每个粒子的Y和opacity

粒子下落消散效果

aboutToDisappear

clearInterval清除定时器

首页鞋带动画

aboutToAppear

启动setInterval

每50ms snLaceY += 2

snLaceY %= 40

触发重新渲染

计算每条鞋带translateY

鞋带上下摆动效果

aboutToDisappear

clearInterval清除定时器

十、核心技术模式深度总结

10.1 声明式UI的构建模式

整个应用充分体现了ArkTS声明式UI的构建模式。每个组件通过@Component装饰器定义,在build()方法中描述UI的结构和样式。状态变量通过@State装饰器标记,状态变化自动驱动UI更新。

这种声明式的开发方式与传统的命令式开发有着本质的区别。在命令式开发中,开发者需要手动获取DOM元素、设置属性、添加事件监听。而在声明式开发中,开发者只需要描述"UI在什么状态下应该是什么样子",框架负责处理状态变化到UI更新的映射。

声明式UI的优势在于代码更加简洁、可读性更强、逻辑更加清晰。开发者可以将更多的精力放在业务逻辑上,而不是繁琐的DOM操作上。同时,声明式UI也更容易进行单元测试和维护。

在本应用中,几乎所有的UI变化都是通过状态驱动的。Tab切换是通过activeTab状态驱动条件渲染实现的,弹框显示是通过showXxx布尔状态驱动的,动画效果是通过定时器更新状态变量驱动的。这种统一的状态驱动模式使得代码的可预测性大大增强。

10.2 弹框交互的统一模式

应用中共有五个弹框组件,分别是球鞋编辑弹框、配色方案弹框、护理详情弹框、新增定制弹框和删除确认弹框。虽然每个弹框的内容和功能各不相同,但它们都遵循了相同的交互模式。

统一的弹框模式包括以下几个要素:

  1. 使用@Builder装饰器定义弹框的构建方法,将弹框的UI封装为一个可复用的构建函数。

  2. 使用一个布尔类型的@State状态变量控制弹框的显示和隐藏。状态为true时显示,为false时隐藏。

  3. 使用Stack堆叠布局,通过条件渲染将弹框叠加在页面内容之上。弹框使用绝对定位铺满整个屏幕,背景是半透明的遮罩层。

  4. 弹框内容居中显示,使用卡片色背景,带有圆角和边框。

  5. 弹框通常有标题区、内容区和操作按钮区三个部分。标题区包含标题和关闭按钮,内容区展示具体的表单或信息,操作按钮区提供确认和取消等操作。

  6. 弹框的数据通过额外的状态变量传递,如编辑弹框的editShoeeditTargeteditHeat,定制弹框的newNameselSizeselColorIdx等。

这种统一的弹框模式使得整个应用的交互风格保持一致,用户在不同的页面遇到弹框时都能快速理解和操作。同时,统一的模式也降低了开发成本,新增弹框时可以遵循已有的模式快速实现。

10.3 数据可视化的多种实现方式

本应用展示了多种数据可视化的实现方式,包括进度条、柱状图、饼图等。这些可视化组件都是使用基础组件或Canvas API构建的,没有依赖第三方图表库。

进度条是最简单也是使用最多的可视化形式。应用中的进度条都采用了Stack堆叠布局的实现方式:底层是灰色的背景轨道,上层是彩色的进度条。进度条的宽度通过数据值动态计算。

进度条的样式变化丰富:有单色的(如尺码对比条),有渐变色的(如热度进度条);有细的(如人气进度条),有粗的(如尺码对比条);有横向的(大部分进度条),也有纵向的(柱状图本质上也是纵向的进度条)。

柱状图是应用中另一种常见的可视化形式。人气柱状图、工艺单量柱状图、月消费统计柱状图都是柱状图的不同变体。它们的共同特点是使用Column组件作为柱子,通过高度来表示数据值的大小。

不同柱状图的样式各有特色:人气柱状图是纯色的,前三名有颜色区分;工艺柱状图使用了渐变色;月消费柱状图最高的柱子有颜色区分。这些细节上的变化使得每个图表都有自己的视觉特点。

饼图是最复杂的一种可视化形式,使用Canvas API实现。Canvas提供了底层的绘制能力,可以实现任意形状的图形。饼图的绘制涉及路径计算、角度转换、颜色填充等多个步骤,需要一定的图形学知识。

多种数据可视化方式的综合运用,使得应用的数据展示更加丰富和直观。不同的数据类型适合不同的可视化方式:排名类数据适合排行榜+进度条,对比类数据适合柱状图,占比类数据适合饼图。

10.4 动画效果的实现策略

应用中实现了三种不同的动画效果:鞋带摆动动画、喷漆粒子动画和会员卡光泽动画。这三种动画虽然表现形式不同,但都采用了相同的实现策略——“定时器 + 状态变量 + 变换属性”。

具体来说,动画的实现步骤如下:

  1. 定义一个@State状态变量,用于存储动画的当前进度(如位移、角度等)。

  2. aboutToAppear()生命周期钩子中启动一个setInterval定时器,周期性地更新状态变量的值。

  3. build()方法中,将状态变量的值应用到组件的变换属性(如translaterotateopacity等)上。

  4. aboutToDisappear()生命周期钩子中清除定时器,防止内存泄漏。

这种基于状态驱动的动画实现方式虽然简单,但有一定的局限性。首先,动画的流畅度取决于定时器的间隔和JavaScript的执行效率,可能不如专门的动画API流畅。其次,这种方式需要手动管理定时器和状态,代码量相对较多。

在HarmonyOS ArkTS API 24中,提供了更加完善的动画系统,包括属性动画、显式动画、转场动画等。使用这些动画API可以实现更加流畅、更加丰富的动画效果,同时代码也更加简洁。

不过,对于简单的循环动画,这种"定时器+状态"的方式仍然有其优势:它不需要学习复杂的动画API,逻辑直观易懂,可以精确控制动画的每一个细节。在原型开发或简单动画场景下,这种方式是一种快速有效的选择。

10.5 状态管理的设计思想

状态管理是声明式UI开发中的核心问题。本应用采用了组件内部状态管理的模式,所有的状态都定义在各自的组件内部,通过props和回调函数进行父子组件间的通信。

具体来说,应用中的状态管理遵循以下设计原则:

  1. 状态就近原则:状态定义在使用它的最近的父组件中。如果一个状态只在某个组件内部使用,就定义在该组件内部。如果多个组件共享一个状态,就将状态提升到它们的共同父组件中。

  2. 单向数据流:数据从上往下流,事件从下往上传。父组件通过props将数据传递给子组件,子组件通过回调函数将事件通知给父组件。数据的修改权属于持有状态的组件。

  3. 状态最小化:只定义必要的状态变量,避免冗余的状态。可以通过计算得出的值不要定义为状态,而是在渲染时实时计算。

  4. 不可变数据:对于对象和数组类型的状态,优先使用不可变的方式更新(如创建新的对象/数组),避免直接修改状态对象的内部属性。虽然ArkTS也支持直接修改数组元素,但不可变数据能更好地追踪状态变化。

在本应用中,Tab切换状态(activeTab)定义在根组件中,因为它被底部Tab栏和内容区共享。各Tab内部的状态(如筛选条件、弹框显隐、输入数据等)则定义在各自的Tab组件内部。

父子组件通信方面,首页Tab通过onGoTab回调函数通知父组件切换Tab。弹框的状态由父组件管理,弹框本身只是一个纯展示的@Builder方法,通过闭包访问父组件的状态和方法。

这种轻量级的状态管理方式对于中小型应用来说是足够的。当应用规模扩大、状态共享变得更加复杂时,可以考虑使用更高级的状态管理方案,如AppStorage、PersistentStorage或第三方状态管理库。

十一、组件级技术细节深度剖析

11.1 Grid组件与列表渲染机制深度分析

Grid组件是ArkUI中用于二维网格布局的核心组件。在本应用中,Grid被广泛用于功能导航宫格、潮流色卡、球鞋商品展示等多个场景。不同场景下Grid的列数、间距、高度等参数各有不同,但底层的渲染机制是一致的。

Grid组件通过columnsTemplate属性定义列的数量和宽度分配。1fr表示一个弹性单位,所有列的fr值之和为总宽度,每列按比例分配。例如'1fr 1fr 1fr'表示三列等宽布局,每列占据总宽度的三分之一。

Grid的行高可以通过rowsTemplate属性显式定义,也可以由内容自动撑开。在本应用中,大多数Grid都没有设置rowsTemplate,而是通过设置Grid的总高度来间接控制行高。这种方式适用于行数固定且每行高度一致的场景。

Grid组件内部使用GridItem子组件来包裹每个网格项的内容。GridItem可以设置rowStartrowEndcolumnStartcolumnEnd等属性来实现跨行或跨列布局。在本应用中,所有GridItem都是单格布局,没有使用跨行跨列的特性。

Grid的性能优化是一个值得关注的话题。对于大量数据的网格布局,Grid组件本身提供了一定的回收复用机制,但开发者也可以通过一些最佳实践来进一步提升性能。

首先,确保ForEach的键生成函数返回唯一且稳定的键。键是框架识别列表项的重要依据,唯一稳定的键可以最大程度地减少不必要的组件重建和DOM操作。在本应用中,球鞋Grid的键包含了品类信息,这是因为品类切换时数据会完全变化,通过改变键可以强制框架重建组件,避免状态残留。

其次,避免在Grid的构建函数中执行复杂的计算。每次状态变化触发重新渲染时,构建函数都会被重新执行,如果其中包含耗时的计算,会影响渲染性能。对于复杂的计算结果,可以考虑使用计算属性或将计算逻辑移到状态变更的地方。

第三,合理设置Grid的高度。如果Grid的高度是通过内容自动计算的,框架需要先测量所有子组件的高度才能确定Grid的高度,这会增加布局计算的开销。如果能提前知道Grid的高度(如本应用中的动态高度计算),显式设置高度可以减少一次布局计算。

11.2 Scroll组件与滚动交互机制

Scroll组件是ArkUI中实现可滚动内容的基础组件。本应用的每个Tab页都使用了Scroll组件包裹内容,确保在内容超出屏幕高度时可以滚动查看。

Scroll组件的基本使用非常简单:将需要滚动的内容作为Scroll的子组件,设置滚动方向和滚动条显示模式即可。在本应用中,所有的Scroll都设置为垂直滚动方向(默认值),并且隐藏了滚动条(scrollBar(BarState.Off))。

隐藏滚动条是移动端应用的常见设计。移动端的滚动通常通过手指滑动来操作,滚动条的存在更多是视觉上的干扰。隐藏滚动条可以让界面更加简洁,但也需要确保用户能够感知到内容是可滚动的。

Scroll组件提供了丰富的滚动控制能力,包括滚动到指定位置、滚动到边缘、监听滚动事件等。在本应用中,没有使用这些高级功能,Scroll仅仅作为内容的滚动容器使用。

Scroll组件的边缘效果是另一个值得关注的特性。在HarmonyOS中,Scroll默认提供了弹性滚动效果,当滚动到边缘时继续拉动会有弹性回弹的效果。这种效果提升了滚动的手感和趣味性,是HarmonyOS UI的标志性特性之一。

在性能方面,Scroll组件内置了内容回收机制。对于长列表,Scroll会回收不可见区域的组件,只保留可见区域的渲染,从而大大减少内存占用和渲染开销。不过,这种回收机制通常需要配合List组件或特定的使用方式才能生效。

在本应用中,由于每个Tab的内容量不算特别大,Scroll的性能压力相对较小。但如果内容量大幅增加,就需要考虑使用更高效的列表组件来优化滚动性能。

11.3 Stack布局与层级管理原理

Stack布局是ArkUI中实现堆叠效果的核心布局组件。在本应用中,Stack被广泛用于各种需要元素叠加的场景,如进度条、标签叠加、弹框遮罩、图标徽章等。

Stack布局的核心特点是子组件按照添加顺序从上到下堆叠,后添加的子组件会覆盖在先添加的子组件之上。每个子组件可以通过alignContent属性控制其在Stack中的对齐方式,如顶部对齐、底部对齐、居中等。

Stack的一个重要属性是zIndex,它可以显式控制组件的堆叠顺序。默认情况下,zIndex为0,组件按添加顺序堆叠。通过设置更高的zIndex,可以让组件显示在更上层。在本应用中,弹框遮罩层设置了zIndex(999),确保弹框始终显示在所有内容的最上方。

Stack布局的另一个重要特性是position属性。通过position属性可以精确控制子组件在Stack中的位置,支持x和y坐标偏移。这种绝对定位的方式在实现一些特殊的视觉效果时非常有用,如通知红点、粒子动画等。

在本应用中,进度条是Stack最典型的应用场景。进度条的底层是灰色的背景轨道,上层是彩色的进度条。两层都使用Column组件,通过Stack堆叠在一起。上层进度条的宽度根据进度值动态计算,从而实现进度条的视觉效果。

弹框遮罩是Stack的另一个典型应用。弹框使用半透明的黑色背景铺满整个屏幕,在屏幕中央显示弹框内容。遮罩层既起到了视觉强调的作用,也可以拦截用户对下方内容的操作。

Stack布局虽然功能强大,但也需要谨慎使用。过多的Stack嵌套会增加布局计算的复杂度,影响渲染性能。在可以使用其他布局实现的情况下,优先使用更简单的布局方式。

11.4 Slider组件与手势交互深度解析

Slider组件是ArkUI中用于范围选择的交互组件。在球鞋Tab的编辑参数弹框中,使用了两个Slider组件分别用于调节目标价格和热度阈值。

Slider组件的基本配置包括当前值、最小值、最大值、步长和样式。步长(step)决定了滑块的调节精度,设置合适的步长可以让用户更精确地选择数值。在本应用中,价格滑块的步长为50,热度滑块的步长为5,分别对应不同的精度需求。

Slider的样式有两种可选:OutSet和InSet。OutSet样式的滑块按钮在轨道外侧,InSet样式的滑块按钮在轨道内侧。本应用使用了OutSet样式,这种样式的滑块更大、更容易操作,适合移动端的触控交互。

Slider的视觉配置包括选中轨道颜色、未选中轨道颜色和滑块按钮颜色。在本应用中,两个Slider的颜色配置正好相反:价格滑块的选中轨道是主色调,滑块按钮是强调色;热度滑块的选中轨道是强调色,滑块按钮是主色调。这种颜色互换的设计既区分了两个滑块的功能,又保持了整体的配色协调。

Slider的交互通过onChange事件回调来处理。当用户拖动滑块时,onChange会被频繁触发,每次触发时传入当前的滑块值。在本应用中,每次值变化都会立即更新到对应的@State状态变量中,从而触发UI的实时更新。用户可以在拖动滑块的同时看到数值的变化,获得即时的视觉反馈。

Slider组件的手势交互设计非常精细。它支持拖拽操作,有最小滑块区域的触控检测,有滑块回弹的动画效果。这些细节虽然用户不一定能明确说出,但会在潜意识中影响用户对应用"好用"与否的判断。

在性能方面,Slider的频繁值变化可能会导致大量的重新渲染。如果Slider控制的UI变化很复杂,可能会影响拖动的流畅度。在这种情况下,可以考虑使用节流(throttle)或防抖(debounce)技术来减少渲染次数。但在本应用中,由于只是更新一个Text组件的数值,性能开销很小,不需要额外优化。

11.5 TextInput组件与表单状态管理

TextInput组件是ArkUI中用于文本输入的基础组件。在客制Tab的新增定制弹框中,使用了TextInput组件让用户输入鞋款名称。

TextInput组件的基本配置包括占位符(placeholder)、初始值(text)、输入类型等。占位符在用户输入前显示提示文字,引导用户输入正确的内容。占位符的颜色和字体可以单独配置,确保与实际输入的文字有所区分。

在本应用中,TextInput的样式经过了精心设计:高度为40像素,深卡片色背景,10像素圆角,左右各12像素的内边距。这些样式参数使得输入框看起来舒适且易于操作,符合移动端的设计规范。

TextInput的输入状态通过onChange事件回调来监听。每次用户输入时,onChange都会被触发,传入当前输入框中的完整文本。在本应用中,输入的文本被实时更新到newName状态变量中,实现了双向数据绑定的效果。

表单状态管理是一个重要的话题。在包含多个输入项的表单中,如何有效地管理表单数据、验证用户输入、处理表单提交,是开发者需要面对的问题。

在本应用中,新增定制弹框是一个简单的表单,包含一个文本输入、一组尺码选择按钮和一组配色选择按钮。表单的状态分散在三个状态变量中:newName存储文本输入值,selSize存储选中的尺码,selColorIdx存储选中的配色索引。

这种分散的状态管理方式对于简单表单来说是可行的,但对于复杂表单可能会变得难以维护。在更复杂的场景下,可以考虑将表单数据组织成一个对象,或者使用专门的表单状态管理方案。

表单验证是另一个重要的方面。在本应用中,没有实现输入验证的逻辑,用户可以输入任意内容甚至空内容后直接提交。在实际应用中,需要对用户输入进行验证,如检查必填项是否为空、检查输入格式是否正确、检查数值是否在合理范围内等。

11.6 Canvas 2D绘制API深度解析

Canvas是ArkUI提供的强大自定义绘制能力,通过Canvas 2D API,开发者可以在画布上绘制任意形状的图形、文字和图片。在配色Tab中,Canvas被用于绘制色系分布饼图。

Canvas的使用需要两个关键步骤:创建渲染上下文和执行绘制命令。渲染上下文(CanvasRenderingContext2D)是所有绘制操作的入口,它提供了丰富的绘制方法,如画线、画弧、填充、描边等。

在本应用中,Canvas渲染上下文在组件的成员变量中创建,传入一个RenderingContextSettings对象来配置渲染设置。RenderingContextSettings的构造参数是一个布尔值,表示是否开启抗锯齿。开启抗锯齿可以让图形的边缘更加平滑,提升视觉质量。

绘制操作在Canvas的onReady回调中执行。onReady回调在Canvas组件准备就绪、可以开始绘制时触发。这是开始绘制的最佳时机,因为此时Canvas的尺寸和上下文都已经准备好。

饼图的绘制涉及多个Canvas API的使用:

beginPath()方法用于开始一条新的路径。每次绘制新的图形之前,都应该调用beginPath()来清除之前的路径,否则新的绘制会和之前的路径叠加在一起。

moveTo()方法用于移动绘制起点到指定坐标。在绘制扇形时,需要先移动到圆心位置,然后再画圆弧。

arc()方法用于绘制圆弧。它接收六个参数:圆心x坐标、圆心y坐标、半径、起始弧度、结束弧度、是否逆时针。在饼图绘制中,通过计算每个扇形的起始角度和结束角度,绘制出对应的圆弧。

closePath()方法用于闭合当前路径。它会从当前点画一条直线回到路径的起点,形成一个闭合的形状。在扇形绘制中,closePath()会将圆弧的终点与圆心连接起来,形成一个完整的扇形。

fillStyle属性用于设置填充颜色。设置后,后续的fill()操作会使用这个颜色填充路径。在饼图中,每个扇形使用不同的颜色填充,因此需要在每次填充前设置对应的fillStyle

fill()方法用于填充当前路径所围成的区域。它会使用当前的fillStyle颜色填充路径内部。

strokeStyle属性用于设置描边颜色。设置后,后续的stroke()操作会使用这个颜色描边。

lineWidth属性用于设置描边线宽。线宽越大,描边越粗。

stroke()方法用于描边当前路径。它会使用当前的strokeStyle颜色和lineWidth线宽沿着路径绘制线条。

通过这些基础API的组合使用,可以绘制出非常复杂的图形。Canvas的强大之处在于它的灵活性,开发者可以精确控制每一个像素的绘制。但相应地,Canvas也需要开发者手动处理交互、动画等逻辑,相比组件化开发更加底层。

11.7 @Builder装饰器与UI复用模式

@Builder装饰器是ArkTS中一种特殊的函数装饰器,它允许将UI构建逻辑封装为一个可复用的函数。@Builder装饰的函数可以像组件一样在build()方法中调用,返回构建好的UI元素。

在本应用中,所有的弹框组件都使用@Builder装饰器定义。每个弹框被封装为一个构建方法(modalOverlay),在需要显示弹框的地方调用这个方法即可。

使用@Builder的好处在于:

第一,代码组织更加清晰。将复杂的UI片段抽取为独立的构建函数,可以让主build()方法更加简洁,可读性更强。

第二,支持条件渲染。通过if语句结合@Builder函数,可以实现UI的条件显示,如弹框的显示和隐藏。

第三,可以接收参数。@Builder函数可以接收参数,根据不同的参数构建不同的UI。虽然本应用中的弹框构建函数没有显式参数(通过闭包访问组件状态),但@Builder本身是支持参数传递的。

第四,可以在多个地方复用。同一个@Builder函数可以在多个位置被调用,实现UI的复用。

当然,@Builder也有其局限性。首先,@Builder函数是与组件绑定的,不能在不同组件之间复用。如果需要跨组件复用,应该定义为自定义组件。

其次,@Builder函数内部不能定义状态变量,它只能使用所在组件的状态。这意味着@Builder函数本质上是组件的一部分,而不是独立的实体。

第三,@Builder函数的性能与内联的UI构建代码相同,没有额外的开销。因为在编译阶段,@Builder函数的内容会被内联到调用位置。

在本应用中,弹框使用@Builder而不是独立组件的方式,主要是因为弹框与父组件共享了大量的状态。如果将弹框抽取为独立组件,需要通过props传递大量数据,通过事件回调通知父组件,反而增加了复杂度。

但从可复用性和可维护性的角度来看,对于通用的弹框组件(如确认对话框、加载提示等),还是应该抽取为独立的自定义组件,以便在整个应用中复用。

11.8 ForEach组件与列表渲染优化策略

ForEach组件是ArkTS中实现列表渲染的核心组件。它可以根据数据数组动态生成一组UI元素,是声明式UI中数据驱动视图的典型代表。

在本应用中,ForEach被广泛用于各种列表场景,包括球鞋列表、配色列表、护理产品列表、定制方案列表、订单列表、收藏列表等。几乎所有的列表展示都使用了ForEach组件。

ForEach的基本用法接收三个参数:第一个是数据数组,第二个是子组件构建函数,第三个是键生成函数。

数据数组是列表数据的来源。ForEach会遍历数组中的每个元素,为每个元素调用构建函数生成对应的子组件。

子组件构建函数接收两个参数:当前遍历的数组元素和当前索引。构建函数返回一个组件,作为该数组元素对应的列表项。

键生成函数接收当前数组元素,返回一个字符串类型的键。键是ForEach性能优化的关键,框架通过键来识别列表项的身份,从而决定是更新、移动还是重建组件。

键的设计是列表性能优化的核心。一个好的键应该满足以下条件:

第一,唯一性。每个列表项的键必须是唯一的,不能有重复。如果有重复的键,框架可能无法正确识别列表项,导致渲染错误。

第二,稳定性。同一个列表项的键应该保持稳定,不随渲染次数而变化。如果键不稳定,框架会认为列表项发生了变化,导致不必要的重建。

第三,相关性。键应该与数据的身份相关,而不是与数据的位置相关。例如,使用数据的id作为键比使用索引作为键更好,因为当列表顺序发生变化时,id仍然能正确标识数据的身份。

在本应用中,所有的ForEach都提供了键生成函数,并且键都是基于数据的id生成的。这是一种很好的实践,确保了列表的渲染性能和正确性。

有些ForEach的键中还包含了额外的信息,如球鞋Grid的键包含了品类信息。这是因为品类切换时,列表中的数据会完全变化,通过在键中加入品类信息,可以强制框架重建所有列表项,避免旧数据的状态残留。

ForEach的性能优化还包括以下几个方面:

首先,避免在构建函数中创建新的函数或对象。每次渲染都创建新的闭包会增加垃圾回收的压力。可以将事件处理函数定义为组件的方法,而不是在构建函数中内联定义。

其次,合理使用组件复用。ForEach会尽可能复用组件实例,但如果组件的结构变化太大,复用的效果会打折扣。保持列表项组件的结构稳定,可以提升ForEach的复用效率。

第三,对于超长列表,考虑使用List组件代替ForEach+Scroll的组合。List组件内置了更高效的回收复用机制,可以在数据量很大时保持良好的滚动性能。

11.9 渲染流程图详解

ForEach列表更新

数据数组变化

生成新的键序列

对比新旧键序列

键存在且位置相同?

复用组件并更新数据

键存在但位置不同?

移动组件位置

键不存在于新序列?

销毁组件

新键不存在于旧序列?

创建新组件

完成列表更新

状态更新渲染流程

状态变量被修改

标记脏组件

执行build方法

生成新的组件树

Diff算法对比新旧组件树

组件类型相同?

更新组件属性

销毁旧组件/创建新组件

递归处理子组件

更新DOM

重新计算布局

重新绘制

更新屏幕显示

初始渲染流程

组件创建

初始化状态变量

执行build方法

构建组件树

创建DOM节点

计算布局

绘制渲染

显示到屏幕

十二、各模块特性对比总结

12.1 六Tab核心特性对比表

对比维度 首页Tab 球鞋Tab 配色Tab 护理Tab 客制Tab 我的Tab
核心功能 内容聚合与导航 商品浏览与筛选 色彩探索与选择 护理产品展示 定制服务介绍 个人中心管理
主要模块数 4个 4个 4个 2个 3个 4个
状态变量数 1个 5个 4个 2个 4个 4个
是否有动画 是(鞋带摆动) 是(喷漆粒子) 是(光泽扫过)
是否有弹框 是(编辑参数) 是(方案选择) 是(产品详情) 是(新增定制) 是(删除确认)
弹框类型 - 参数设置型 选择确认型 信息展示型 表单提交型 警告确认型
弹框主色调 - 暗夜紫 电光绿 电光绿 暗夜紫 危险红
数据可视化 热度进度条 进度条+柱状图 饼图+进度条+柱状图 进度条 进度条+柱状图 柱状图
图表类型 横向进度条 横向进度条 + 纵向柱状图 饼图 + 横向进度条 横向进度条 横向进度条 + 纵向柱状图 纵向柱状图
列表形式 横滑卡片 + 排行榜 双列Grid + 列表 三列Grid + 列表 排行榜列表 横向卡片列表 订单列表 + 收藏列表
筛选功能 是(品类筛选)
表单输入 滑块输入 色卡选择 文本输入 + 选择
删除操作 是(收藏删除)
Canvas使用 是(饼图)
定时器使用
生命周期钩子
数据操作 只读 只读+编辑参数 只读+选择配色 只读 只读+提交表单 读+删
交互复杂度 中等 较高 较高 中等 较高

12.2 三种动画效果对比表

对比维度 鞋带摆动动画 喷漆粒子动画 会员卡光泽动画
所在页面 首页Tab 配色Tab 我的Tab
动画类型 上下摆动 下落消散 水平扫过
状态变量 snLaceY snSprayY snShine
定时器间隔 50ms 40ms 40ms
每次增量 2 5 6
循环周期 40(取模) 160(取模) 420 ~ -80(条件重置)
动画元素数 9条鞋带 8个粒子 1个光条
变换属性 translateY(translate) translateY + opacity translateX(translate) + rotate
视觉效果 波浪式递进摆动 粒子下落渐隐 斜向光泽扫过
实现复杂度 中等 较高 中等
性能影响 较低 中等 较低
氛围营造 动态活力感 创作过程感 高端质感

12.3 五种弹框模式对比表

对比维度 编辑参数弹框 配色方案弹框 护理详情弹框 新增定制弹框 删除确认弹框
所在Tab 球鞋Tab 配色Tab 护理Tab 客制Tab 我的Tab
弹框类型 参数设置 选择确认 信息展示 表单提交 警告确认
触发方式 点击球鞋卡片 点击方案选择 点击产品项 点击发起定制 点击删除按钮
内容区组件 滑块x2 Grid色卡 参数表x7行 输入框+选择+色卡 警告图标+文字
操作按钮数 2个 2个 1个 1个 2个
确认按钮文字 保存参数 应用方案 加入护理清单 提交定制单 确认删除
确认按钮颜色 主色调紫 强调色绿 强调色绿 强调色绿 危险色红
取消按钮 有(重置)
关闭按钮
弹框宽度 86% 88% 86% 88% 80%
边框颜色 分割线色 分割线色 分割线色 深主色调 半透明危险色
状态变量数 3个(数据)+1个(显隐) 2个(数据)+1个(显隐) 1个(数据)+1个(显隐) 3个(数据)+1个(显隐) 1个(数据)+1个(显隐)
交互复杂度 中等 中等

十三、综合总结与技术展望

13.1 技术架构维度总结

从技术架构的角度来看,这个球鞋定制工坊应用展示了一个完整的ArkTS单文件应用的架构模式。整个应用采用了分层的架构设计:数据层位于最底层,包含枚举、接口、常量和辅助函数;组件层位于上层,包含入口组件和六个功能Tab组件。

数据层的设计体现了类型安全和数据驱动的思想。通过接口定义明确了数据的形状和类型,通过常量集中管理所有的业务数据,通过辅助函数封装了通用的数据处理逻辑。这种设计使得数据层独立于UI层,具有良好的可维护性和可测试性。

组件层的设计遵循了声明式UI的开发范式。每个组件都是一个独立的、自包含的单元,有自己的状态和UI描述。组件之间通过props和回调函数进行通信,形成了单向的数据流。这种设计使得组件的职责清晰、边界明确,便于理解和维护。

入口组件作为应用的根组件,承担了全局布局和导航的职责。它管理着Tab切换的全局状态,协调各Tab组件的显示和隐藏。底部Tab栏作为主导航入口,提供了页面间的快速切换。

弹框组件采用了@Builder的方式定义,与父组件共享状态。这种设计虽然简单直接,但也存在弹框与父组件耦合度高、难以复用的问题。在更复杂的应用中,可以考虑将弹框封装为独立的自定义组件,通过props和事件进行通信。

总体而言,应用的架构设计简洁清晰、层次分明,符合ArkTS声明式开发的最佳实践。对于中小型应用来说,这种架构是高效且合适的。

13.2 视觉设计维度总结

从视觉设计的角度来看,这个应用成功地营造了赛博朋克风格的暗色主题视觉效果。炭黑、暗夜紫、电光绿的三色搭配既具有强烈的视觉冲击力,又保持了良好的可读性和层次感。

应用的视觉设计具有以下几个显著特点:

第一,系统化的配色体系。应用定义了完整的调色板,包含背景色、卡片色、文字色、主色调、强调色、状态色等十三种颜色变量。所有的颜色使用都通过引用调色板变量来实现,确保了配色的一致性和可维护性。

第二,丰富的渐变效果。应用大量使用了线性渐变来增强视觉层次感。从卡片背景到商品色块,从进度条到柱状图,渐变效果无处不在。渐变不仅让界面更加美观,也增加了界面的深度和质感。

第三,统一的圆角设计。应用中的卡片、按钮、标签、进度条等元素都使用了圆角设计,圆角的大小根据元素的尺寸和重要性有所不同。统一的圆角设计赋予了界面柔和、现代的视觉风格。

第四,清晰的文字层级。应用中的文字分为三个层级:主文字色用于标题和重要内容,次文字色用于正文和辅助信息,辅助文字色用于次要说明和禁用状态。三级文字色体系确保了信息层级的清晰表达。

第五,动态的视觉效果。应用中的三个动画效果(鞋带摆动、喷漆粒子、光泽扫过)为静态的界面增添了动感和活力,提升了用户的视觉体验。

总体而言,应用的视觉设计在美学和功能性之间取得了良好的平衡。赛博风的主题不仅符合球鞋定制的潮流定位,也通过系统化的设计确保了界面的可用性和可读性。

13.3 交互设计维度总结

从交互设计的角度来看,这个应用遵循了移动端应用的常见交互模式,同时也有一些自己的特色。

Tab导航是应用的主导航模式,底部Tab栏提供了六个页面的快速切换。这种导航模式在移动端应用中非常普遍,用户学习成本低,操作效率高。Tab切换时页面会完整重建,虽然牺牲了一些性能,但实现简单,对于内容不太复杂的页面是可以接受的。

弹框交互是应用中重要的交互模式,五种不同类型的弹框覆盖了参数设置、选择确认、信息展示、表单提交和警告确认等多种场景。统一的弹框设计模式确保了交互体验的一致性。

列表和Grid是内容展示的主要形式。应用中使用了多种列表布局,包括横向滚动列表、纵向排行榜列表、双列商品Grid、三列色卡Grid等。不同的布局形式适用于不同的内容类型和展示需求。

筛选和排序是商品浏览的重要交互。球鞋Tab提供了品类筛选功能,通过顶部的分类标签快速切换不同品类的商品。各种排行榜则提供了按热度、功效等维度排序的内容展示。

表单输入是定制功能的核心交互。新增定制弹框提供了文本输入、尺码选择、配色选择等多种表单控件。滑块控件用于参数的精细调节,提供了直观的数值反馈。

删除操作采用了二次确认的交互模式,通过警告弹框防止用户误操作。这种设计体现了对用户数据的尊重和保护。

总体而言,应用的交互设计简洁直观、符合用户习惯,同时也有足够的丰富性来满足不同功能的需求。

13.4 性能优化维度总结

从性能优化的角度来看,这个应用虽然是一个原型级的应用,但在一些方面也体现了性能优化的意识。

首先,不可变数据的处理方式。在排序等可能修改原数组的操作中,应用先创建数组的浅拷贝,然后对拷贝进行排序,避免了修改原始数据。这种不可变数据的处理方式虽然主要是为了数据安全,但也有助于框架追踪数据变化,从而优化渲染性能。

其次,ForEach的键生成函数。应用中所有的ForEach都提供了键生成函数,为每个列表项生成唯一的键。这对于框架的列表更新优化非常重要。有了唯一键,框架可以准确地识别哪些元素是新增的、哪些是删除的、哪些是移动的,从而最小化DOM操作,提升列表更新的性能。

第三,条件渲染的使用。Tab切换使用条件渲染而非显示/隐藏切换,虽然每次切换都会重建组件,但也意味着不显示的Tab不会占用内存和渲染资源。对于内容较重且切换不频繁的Tab来说,条件渲染可能比保持所有Tab实例更节省资源。

第四,Scroll组件的使用。所有的长列表都使用了Scroll组件包裹,确保内容可以滚动查看。Scroll组件内置了滚动优化和回收机制,可以在内容较长时保持良好的滚动性能。

当然,作为一个原型应用,还有很多性能优化的空间。例如:

  1. 定时器动画可以替换为属性动画或显式动画,获得更好的性能和更丰富的动画效果。

  2. 辅助函数中的重复计算(如每次渲染都重新计算最大值)可以通过计算属性或缓存来优化。

  3. 弹框组件可以使用懒加载或预创建的方式优化弹框弹出的速度。

  4. 对于大量数据的列表,可以使用List组件代替ForEach+Scroll的组合,获得更好的滚动性能和内存使用效率。

总体而言,应用在基本的性能方面做得不错,但在更高级的性能优化方面还有提升空间。对于原型级的应用来说,当前的性能表现是足够的。

13.5 代码质量维度总结

从代码质量的角度来看,这个应用的代码具有以下几个优点:

第一,命名规范统一。变量、函数、组件的命名都遵循了统一的命名规范。组件使用大驼峰命名法,变量和函数使用小驼峰命名法,常量使用全大写下划线分隔法。清晰的命名使得代码具有很好的可读性。

第二,代码结构清晰。文件中的内容按照枚举、接口、主题配置、数据常量、辅助函数、入口组件、各Tab组件的顺序组织,层次分明。每个部分之间都有注释分隔符,便于快速定位。

第三,注释适度。代码中有适量的注释,包括模块说明、代码段说明等。注释既不会多到干扰代码阅读,也不会少到让人无法理解代码的意图。

第四,代码复用。应用中通过辅助函数封装了常用的数据处理逻辑,通过统一的设计模式确保了交互方式的一致性。虽然没有抽取公共组件,但相似的设计模式本身也是一种复用。

第五,类型安全。应用充分利用了TypeScript/ArkTS的类型系统,为所有的数据结构定义了接口,为函数参数和返回值定义了类型。这大大减少了类型相关的bug,提升了代码的健壮性。

当然,代码也有一些可以改进的地方:

  1. 部分代码存在重复。例如底部Tab栏的六个Tab项、尺码选择的七个按钮等,都是类似的代码重复。可以考虑使用ForEach循环来减少重复代码。

  2. 弹框代码与父组件耦合。每个弹框都定义在父组件内部,作为一个@Builder方法,导致弹框无法在其他组件中复用。可以考虑抽取为独立的自定义组件。

  3. 魔法数字较多。代码中有一些硬编码的数值,如动画参数、尺寸数值等。可以考虑抽取为常量,提高代码的可维护性。

  4. 缺少错误处理。目前代码中几乎没有错误处理逻辑,如数据为空、边界情况等都没有特殊处理。在生产环境中,需要增加完善的错误处理和边界情况处理。

总体而言,应用的代码质量较高,结构清晰、命名规范、类型安全,具有很好的可读性和可维护性。


安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

// ============================================================
// SNEAK LAB 球鞋定制工坊
// 主题:炭黑 #16161A / 暗夜紫 #7C4DFF / 电光绿 #B6FF00
// 暗色潮流赛博风 · 单文件 ArkTS 应用
// ============================================================

// ============ 枚举 ============
enum SNTab {
  Home,
  Shoes,
  Color,
  Care,
  Custom,
  Mine
}

// ============ 类型定义 ============
interface SNShoe {
  id: number
  name: string
  cat: string
  price: number
  oldPrice: number
  heat: number
  colorHex: string
  limited: boolean
  tag: string
}

interface SNColorItem {
  id: number
  name: string
  colorHex: string
  series: string
  heat: number
}

interface SNCareProduct {
  id: number
  name: string
  effect: string
  power: number
  usage: number
  price: number
  volume: string
  material: string
}

interface SNCustomPlan {
  id: number
  name: string
  craft: string
  days: number
  price: number
  popular: number
  desc: string
}

interface SNOrderItem {
  id: number
  name: string
  status: string
  date: string
  price: number
  colorHex: string
}

interface SNFavoriteItem {
  id: number
  name: string
  price: number
  note: string
  colorHex: string
}

interface SNSpendItem {
  id: number
  month: string
  amount: number
}

interface SNFuncItem {
  id: number
  icon: string
  name: string
  tab: SNTab
}

interface SNCraftStat {
  id: number
  name: string
  count: number
}

interface SNColorPalette {
  bg: string
  card: string
  cardDeep: string
  line: string
  primary: string
  primaryDeep: string
  accent: string
  textMain: string
  textSub: string
  textDim: string
  danger: string
  warn: string
}

// ============ 主题配置 ============
const SN_COLORS: SNColorPalette = {
  bg: '#16161A',
  card: '#1F1F26',
  cardDeep: '#232331',
  line: '#2E2E3A',
  primary: '#7C4DFF',
  primaryDeep: '#4A2B99',
  accent: '#B6FF00',
  textMain: '#F2F2F7',
  textSub: '#A8A8B3',
  textDim: '#6E6E7A',
  danger: '#FF4D5E',
  warn: '#FFB300'
}

// ============ 数据 ============
const SN_SHOES: SNShoe[] = [
  { id: 1, name: '幻影紫芒 Runner X1', cat: '跑步', price: 1299, oldPrice: 1599, heat: 96, colorHex: '#7C4DFF', limited: true, tag: '新品' },
  { id: 2, name: '电光绿锋 Bolt EZ', cat: '跑步', price: 1099, oldPrice: 1299, heat: 88, colorHex: '#B6FF00', limited: false, tag: '热卖' },
  { id: 3, name: '炭黑武士 Court Dark', cat: '篮球', price: 1399, oldPrice: 1699, heat: 92, colorHex: '#3A3A46', limited: false, tag: '经典' },
  { id: 4, name: '荧光夜巡 Night Patrol', cat: '滑板', price: 899, oldPrice: 999, heat: 76, colorHex: '#FFE14D', limited: false, tag: '潮玩' },
  { id: 5, name: '白紫交响 Symphony Low', cat: '休闲', price: 999, oldPrice: 1199, heat: 84, colorHex: '#EDE7FF', limited: false, tag: '百搭' },
  { id: 6, name: '熔岩橙暴 Blaze High', cat: '篮球', price: 1599, oldPrice: 1899, heat: 90, colorHex: '#FF6D3B', limited: true, tag: '限量' },
  { id: 7, name: '冰蓝极光 Aurora Mid', cat: '休闲', price: 1199, oldPrice: 1399, heat: 71, colorHex: '#4FC3F7', limited: false, tag: '清爽' },
  { id: 8, name: '暗夜潜行者 Shadow Pro', cat: '跑步', price: 1899, oldPrice: 2199, heat: 95, colorHex: '#26262E', limited: true, tag: '顶级' },
  { id: 9, name: '复古灰调 Retro Grey', cat: '滑板', price: 799, oldPrice: 899, heat: 65, colorHex: '#9E9EA6', limited: false, tag: '复古' },
  { id: 10, name: '全息幻彩 Holo Dream', cat: '休闲', price: 1699, oldPrice: 1999, heat: 89, colorHex: '#D9E4FF', limited: true, tag: '联名' }
]

const SN_COLORS_LIST: SNColorItem[] = [
  { id: 1, name: '暗夜紫', colorHex: '#7C4DFF', series: '紫系', heat: 95 },
  { id: 2, name: '电光绿', colorHex: '#B6FF00', series: '荧光', heat: 92 },
  { id: 3, name: '炭晶黑', colorHex: '#26262E', series: '暗色', heat: 88 },
  { id: 4, name: '樱花粉', colorHex: '#FF6EC7', series: '粉系', heat: 81 },
  { id: 5, name: '冰川蓝', colorHex: '#4FC3F7', series: '蓝系', heat: 85 },
  { id: 6, name: '熔岩橙', colorHex: '#FF6D3B', series: '暖系', heat: 78 },
  { id: 7, name: '幻彩银', colorHex: '#D9E4FF', series: '中性', heat: 73 },
  { id: 8, name: '荧光黄', colorHex: '#FFE14D', series: '荧光', heat: 69 }
]

const SN_CARES: SNCareProduct[] = [
  { id: 1, name: '中底去黄剂', effect: '还原中底白度', power: 95, usage: 34, price: 89, volume: '120ml', material: '橡胶中底' },
  { id: 2, name: '深层清洁泡沫', effect: '瓦解顽固污渍', power: 92, usage: 46, price: 69, volume: '200ml', material: '网面/织物' },
  { id: 3, name: '防水防污喷雾', effect: '形成纳米防护膜', power: 88, usage: 28, price: 79, volume: '150ml', material: '全鞋面' },
  { id: 4, name: '除臭杀菌球', effect: '抑菌除味 30 天', power: 81, usage: 12, price: 39, volume: '一对装', material: '鞋内腔' },
  { id: 5, name: '皮质滋养油', effect: '防裂增亮护理', power: 76, usage: 18, price: 59, volume: '80ml', material: '头层皮革' },
  { id: 6, name: '鞋带增白液', effect: '快速焕白鞋带', power: 70, usage: 8, price: 29, volume: '100ml', material: '棉质鞋带' },
  { id: 7, name: '麂皮橡皮擦', effect: '干式去污不留痕', power: 68, usage: 10, price: 25, volume: '两块装', material: '麂皮/翻毛' },
  { id: 8, name: '网面除尘刷', effect: '日常快速除尘', power: 62, usage: 22, price: 19, volume: '软毛刷', material: '网面鞋身' }
]

const SN_CUSTOMS: SNCustomPlan[] = [
  { id: 1, name: '全鞋手绘喷漆', craft: '喷绘', days: 15, price: 899, popular: 94, desc: '整鞋渐变喷绘 + 保护清漆' },
  { id: 2, name: '后跟刺绣定制', craft: '刺绣', days: 7, price: 399, popular: 88, desc: '姓名/图案激光定位刺绣' },
  { id: 3, name: '皮革激光雕花', craft: '激光', days: 10, price: 659, popular: 82, desc: '激光切割镂空个性花纹' },
  { id: 4, name: '鞋带手工编织', craft: '编织', days: 5, price: 199, popular: 76, desc: '双色编织替换鞋带系统' },
  { id: 5, name: '中底个性彩绘', craft: '中底', days: 9, price: 529, popular: 71, desc: '中底文字涂鸦 + 防水封层' },
  { id: 6, name: '全片热转印', craft: '热转印', days: 12, price: 459, popular: 66, desc: '高清图案全片热转印' }
]

const SN_ORDERS: SNOrderItem[] = [
  { id: 1, name: '幻影紫芒 Runner X1', status: '已发货', date: '08-24', price: 1299, colorHex: '#7C4DFF' },
  { id: 2, name: '暗夜潜行者 Shadow Pro', status: '定制中', date: '08-22', price: 1899, colorHex: '#26262E' },
  { id: 3, name: '熔岩橙暴 Blaze High', status: '已完成', date: '08-15', price: 1599, colorHex: '#FF6D3B' },
  { id: 4, name: '白紫交响 Symphony Low', status: '待付款', date: '08-11', price: 999, colorHex: '#EDE7FF' },
  { id: 5, name: '全息幻彩 Holo Dream', status: '已发货', date: '08-06', price: 1699, colorHex: '#D9E4FF' },
  { id: 6, name: '复古灰调 Retro Grey', status: '已取消', date: '07-28', price: 799, colorHex: '#9E9EA6' }
]

const SN_FAVORITES: SNFavoriteItem[] = [
  { id: 1, name: '电光绿锋 Bolt EZ', price: 1099, note: '等大促降价', colorHex: '#B6FF00' },
  { id: 2, name: '冰蓝极光 Aurora Mid', price: 1199, note: '夏季清爽款', colorHex: '#4FC3F7' },
  { id: 3, name: '炭黑武士 Court Dark', price: 1399, note: '实战篮球首选', colorHex: '#3A3A46' },
  { id: 4, name: '荧光夜巡 Night Patrol', price: 899, note: '夜跑安全色', colorHex: '#FFE14D' },
  { id: 5, name: '全息幻彩 Holo Dream', price: 1699, note: '联名收藏款', colorHex: '#D9E4FF' },
  { id: 6, name: '樱花粉定制版', price: 1499, note: '情侣配色款', colorHex: '#FF6EC7' }
]

const SN_SPENDS: SNSpendItem[] = [
  { id: 1, month: '3月', amount: 1299 },
  { id: 2, month: '4月', amount: 2498 },
  { id: 3, month: '5月', amount: 899 },
  { id: 4, month: '6月', amount: 3298 },
  { id: 5, month: '7月', amount: 1899 },
  { id: 6, month: '8月', amount: 2898 }
]

const SN_SIZES: number[] = [38, 39, 40, 41, 42, 43, 44]

const SN_SIZE_STOCK: number[] = [12, 18, 26, 34, 30, 22, 14]

const SN_HOME_FUNCS: SNFuncItem[] = [
  { id: 1, icon: '▲', name: '球鞋库', tab: SNTab.Shoes },
  { id: 2, icon: '◐', name: '配色', tab: SNTab.Color },
  { id: 3, icon: '✚', name: '护理', tab: SNTab.Care },
  { id: 4, icon: '⚒', name: '定制', tab: SNTab.Custom },
  { id: 5, icon: '☰', name: '订单', tab: SNTab.Mine },
  { id: 6, icon: '★', name: '会员', tab: SNTab.Mine },
  { id: 7, icon: '⚡', name: '闪购', tab: SNTab.Shoes },
  { id: 8, icon: '◉', name: '工坊', tab: SNTab.Custom }
]

const SN_CRAFT_STATS: SNCraftStat[] = [
  { id: 1, name: '喷绘', count: 156 },
  { id: 2, name: '刺绣', count: 98 },
  { id: 3, name: '激光', count: 74 },
  { id: 4, name: '编织', count: 62 },
  { id: 5, name: '中底', count: 85 },
  { id: 6, name: '热转印', count: 48 }
]

// ============ 辅助函数 ============
function snFilterShoes(cat: string): SNShoe[] {
  let r: SNShoe[] = []
  for (let i = 0; i < SN_SHOES.length; i++) {
    if (cat === '全部' || SN_SHOES[i].cat === cat) {
      r.push(SN_SHOES[i])
    }
  }
  return r
}

function snLimitedShoes(): SNShoe[] {
  let r: SNShoe[] = []
  for (let i = 0; i < SN_SHOES.length; i++) {
    if (SN_SHOES[i].limited) {
      r.push(SN_SHOES[i])
    }
  }
  return r
}

function snHotShoes(): SNShoe[] {
  let src: SNShoe[] = []
  for (let i = 0; i < SN_SHOES.length; i++) {
    src.push(SN_SHOES[i])
  }
  src.sort((a: SNShoe, b: SNShoe) => b.heat - a.heat)
  return src
}

function snTopShoes(n: number): SNShoe[] {
  let r: SNShoe[] = snHotShoes()
  if (r.length > n) {
    r.splice(n)
  }
  return r
}

function snTopColors(): SNColorItem[] {
  let src: SNColorItem[] = []
  for (let i = 0; i < SN_COLORS_LIST.length; i++) {
    src.push(SN_COLORS_LIST[i])
  }
  src.sort((a: SNColorItem, b: SNColorItem) => b.heat - a.heat)
  return src
}

function snTopCares(): SNCareProduct[] {
  let src: SNCareProduct[] = []
  for (let i = 0; i < SN_CARES.length; i++) {
    src.push(SN_CARES[i])
  }
  src.sort((a: SNCareProduct, b: SNCareProduct) => b.power - a.power)
  return src
}

function snTotalHeat(): number {
  let t = 0
  for (let i = 0; i < SN_COLORS_LIST.length; i++) {
    t += SN_COLORS_LIST[i].heat
  }
  return t
}

function snHeatBefore(idx: number): number {
  let t = 0
  for (let i = 0; i < idx; i++) {
    t += SN_COLORS_LIST[i].heat
  }
  return t
}

function snMaxSpend(): number {
  let m = 0
  for (let i = 0; i < SN_SPENDS.length; i++) {
    if (SN_SPENDS[i].amount > m) {
      m = SN_SPENDS[i].amount
    }
  }
  return m
}

function snMaxCraft(): number {
  let m = 0
  for (let i = 0; i < SN_CRAFT_STATS.length; i++) {
    if (SN_CRAFT_STATS[i].count > m) {
      m = SN_CRAFT_STATS[i].count
    }
  }
  return m
}

function snMaxSizeStock(): number {
  let m = 0
  for (let i = 0; i < SN_SIZE_STOCK.length; i++) {
    if (SN_SIZE_STOCK[i] > m) {
      m = SN_SIZE_STOCK[i]
    }
  }
  return m
}

function snStatusColor(st: string): string {
  if (st === '已发货' || st === '已完成') {
    return SN_COLORS.accent
  }
  if (st === '定制中') {
    return SN_COLORS.primary
  }
  if (st === '待付款') {
    return SN_COLORS.warn
  }
  return SN_COLORS.textDim
}

// ============ 入口 ============
@Entry
@Component
struct SneakerLabApp {
  @State activeTab: SNTab = SNTab.Home

  build() {
    Column() {
      // 顶部头部(电商风格,无动画)
      Row({ space: 14 }) {
        Column() {
          Text('SNEAK LAB')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(SN_COLORS.textMain)
            .letterSpacing(1.5)
          Text('球鞋定制工坊 · CUSTOM YOUR SOLE')
            .fontSize(9)
            .fontColor(SN_COLORS.accent)
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Stack() {
          Text('◎')
            .fontSize(19)
            .fontColor(SN_COLORS.textMain)
          Circle()
            .width(7)
            .height(7)
            .fill(SN_COLORS.danger)
            .position({ x: 13, y: -2 })
        }
        .width(24)
        .height(24)
        .alignContent(Alignment.Center)

        Text('✉')
          .fontSize(19)
          .fontColor(SN_COLORS.textMain)
      }
      .width('100%')
      .padding({ left: 18, right: 18, top: 14, bottom: 12 })
      .backgroundColor(SN_COLORS.bg)
      .border({ width: { bottom: 1 }, color: SN_COLORS.line })

      // 内容区
      Stack() {
        if (this.activeTab === SNTab.Home) {
          SNHomeTab({ onGoTab: (t: SNTab) => {
            this.activeTab = t
          } })
        } else if (this.activeTab === SNTab.Shoes) {
          SNShoeTab()
        } else if (this.activeTab === SNTab.Color) {
          SNColorTab()
        } else if (this.activeTab === SNTab.Care) {
          SNCareTab()
        } else if (this.activeTab === SNTab.Custom) {
          SNCustomTab()
        } else {
          SNMineTab()
        }
      }
      .layoutWeight(1)

      // 底部 Tab 栏
      Row() {
        Column({ space: 3 }) {
          Text('⌂')
            .fontSize(19)
            .fontColor(this.activeTab === SNTab.Home ? SN_COLORS.accent : SN_COLORS.textDim)
          Text('首页')
            .fontSize(10)
            .fontColor(this.activeTab === SNTab.Home ? SN_COLORS.accent : SN_COLORS.textDim)
        }
        .justifyContent(FlexAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = SNTab.Home
        })

        Column({ space: 3 }) {
          Text('▲')
            .fontSize(19)
            .fontColor(this.activeTab === SNTab.Shoes ? SN_COLORS.accent : SN_COLORS.textDim)
          Text('球鞋')
            .fontSize(10)
            .fontColor(this.activeTab === SNTab.Shoes ? SN_COLORS.accent : SN_COLORS.textDim)
        }
        .justifyContent(FlexAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = SNTab.Shoes
        })

        Column({ space: 3 }) {
          Text('◐')
            .fontSize(19)
            .fontColor(this.activeTab === SNTab.Color ? SN_COLORS.accent : SN_COLORS.textDim)
          Text('配色')
            .fontSize(10)
            .fontColor(this.activeTab === SNTab.Color ? SN_COLORS.accent : SN_COLORS.textDim)
        }
        .justifyContent(FlexAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = SNTab.Color
        })

        Column({ space: 3 }) {
          Text('✚')
            .fontSize(19)
            .fontColor(this.activeTab === SNTab.Care ? SN_COLORS.accent : SN_COLORS.textDim)
          Text('护理')
            .fontSize(10)
            .fontColor(this.activeTab === SNTab.Care ? SN_COLORS.accent : SN_COLORS.textDim)
        }
        .justifyContent(FlexAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = SNTab.Care
        })

        Column({ space: 3 }) {
          Text('⚒')
            .fontSize(19)
            .fontColor(this.activeTab === SNTab.Custom ? SN_COLORS.accent : SN_COLORS.textDim)
          Text('客制')
            .fontSize(10)
            .fontColor(this.activeTab === SNTab.Custom ? SN_COLORS.accent : SN_COLORS.textDim)
        }
        .justifyContent(FlexAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = SNTab.Custom
        })

        Column({ space: 3 }) {
          Text('☺')
            .fontSize(19)
            .fontColor(this.activeTab === SNTab.Mine ? SN_COLORS.accent : SN_COLORS.textDim)
          Text('我的')
            .fontSize(10)
            .fontColor(this.activeTab === SNTab.Mine ? SN_COLORS.accent : SN_COLORS.textDim)
        }
        .justifyContent(FlexAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = SNTab.Mine
        })
      }
      .width('100%')
      .height(58)
      .padding({ bottom: 4 })
      .backgroundColor(SN_COLORS.card)
      .border({ width: { top: 1 }, color: SN_COLORS.line })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(SN_COLORS.bg)
  }
}

// ============ 首页 Tab ============
@Component
struct SNHomeTab {
  @State snLaceY: number = 0
  onGoTab: (t: SNTab) => void = (t: SNTab) => {
  }
  private laceTimer: number = -1

  aboutToAppear(): void {
    this.laceTimer = setInterval(() => {
      this.snLaceY = (this.snLaceY + 2) % 40
    }, 50)
  }

  aboutToDisappear(): void {
    if (this.laceTimer >= 0) {
      clearInterval(this.laceTimer)
    }
  }

  build() {
    Stack() {
      Scroll() {
        Column({ space: 14 }) {
          // 1. 鞋型仪表盘(鞋带摆动动画)
          Column({ space: 12 }) {
            Row() {
              Text('鞋型仪表盘')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(SN_COLORS.textMain)
              Column().layoutWeight(1)
              Text('● 实时监测')
                .fontSize(10)
                .fontColor(SN_COLORS.accent)
            }
            .width('100%')

            Stack() {
              Column()
                .width('100%')
                .height(112)
                .linearGradient({
                  angle: 135,
                  colors: [['#2A1E4A', 0], ['#16161A', 1]]
                })

              Column({ space: 9 }) {
                Row({ space: 10 }) {
                  Column()
                    .width(52)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor('#5E3AC8')
                    .translate({ y: ((((this.snLaceY + 32) % 40) - 20) * 0.4) })
                  Column()
                    .width(74)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor(SN_COLORS.primary)
                    .translate({ y: ((((this.snLaceY + 24) % 40) - 20) * 0.4) })
                  Column()
                    .width(96)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor(SN_COLORS.accent)
                    .translate({ y: ((((this.snLaceY + 16) % 40) - 20) * 0.4) })
                  Column()
                    .width(74)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor('#9E7BFF')
                    .translate({ y: ((((this.snLaceY + 8) % 40) - 20) * 0.4) })
                  Column()
                    .width(52)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor('#5E3AC8')
                    .translate({ y: ((((this.snLaceY) % 40) - 20) * 0.4) })
                }
                .alignItems(VerticalAlign.Center)

                Row({ space: 10 }) {
                  Column()
                    .width(64)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor('#4A2B99')
                    .translate({ y: ((((this.snLaceY + 28) % 40) - 20) * 0.4) })
                  Column()
                    .width(88)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor('#CE9BFF')
                    .translate({ y: ((((this.snLaceY + 20) % 40) - 20) * 0.4) })
                  Column()
                    .width(108)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor(SN_COLORS.accent)
                    .translate({ y: ((((this.snLaceY + 12) % 40) - 20) * 0.4) })
                  Column()
                    .width(88)
                    .height(5)
                    .borderRadius(3)
                    .backgroundColor('#4A2B99')
                    .translate({ y: ((((this.snLaceY + 4) % 40) - 20) * 0.4) })
                }
                .alignItems(VerticalAlign.Center)
              }
              .justifyContent(FlexAlign.Center)

              Text('LACE SYSTEM')
                .fontSize(8)
                .letterSpacing(2)
                .fontColor('#4A4A5A')
                .position({ x: 12, y: 8 })
            }
            .width('100%')
            .height(112)
            .borderRadius(14)
            .clip(true)

            Row() {
              Column({ space: 3 }) {
                Text('96.2')
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(SN_COLORS.accent)
                Text('今日热度指数')
                  .fontSize(9)
                  .fontColor(SN_COLORS.textSub)
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)

              Column()
                .width(1)
                .height(30)
                .backgroundColor(SN_COLORS.line)

              Column({ space: 3 }) {
                Text('128')
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(SN_COLORS.primary)
                Text('定制单进行中')
                  .fontSize(9)
                  .fontColor(SN_COLORS.textSub)
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)

              Column()
                .width(1)
                .height(30)
                .backgroundColor(SN_COLORS.line)

              Column({ space: 3 }) {
                Text('6')
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(SN_COLORS.textMain)
                Text('本周新品')
                  .fontSize(9)
                  .fontColor(SN_COLORS.textSub)
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }
            .width('100%')
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(SN_COLORS.cardDeep)
            .borderRadius(12)
          }
          .width('100%')
          .padding(14)
          .backgroundColor(SN_COLORS.card)
          .borderRadius(16)

          // 2. 限量发售横滑
          Column({ space: 10 }) {
            Row() {
              Text('限量发售')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(SN_COLORS.textMain)
              Column().layoutWeight(1)
              Text('查看全部 >')
                .fontSize(10)
                .fontColor(SN_COLORS.textSub)
            }
            .width('100%')

            Scroll() {
              Row({ space: 12 }) {
                ForEach(snLimitedShoes(), (s: SNShoe) => {
                  Column({ space: 7 }) {
                    Stack({ alignContent: Alignment.TopStart }) {
                      Column()
                        .width('100%')
                        .height(82)
                        .borderRadius(10)
                        .linearGradient({
                          angle: 120,
                          colors: [[s.colorHex, 0], ['#232331', 1]]
                        })
                      Text('LIMITED')
                        .fontSize(7)
                        .fontColor(SN_COLORS.bg)
                        .backgroundColor(SN_COLORS.accent)
                        .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                        .borderRadius(4)
                        .margin(6)
                    }
                    .width('100%')

                    Text(s.name)
                      .fontSize(11)
                      .fontColor(SN_COLORS.textMain)
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })

                    Text('¥' + s.price.toString())
                      .fontSize(12)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(SN_COLORS.accent)
                  }
                  .width(132)
                  .padding(9)
                  .backgroundColor(SN_COLORS.card)
                  .borderRadius(14)
                  .alignItems(HorizontalAlign.Start)
                }, (s: SNShoe) => s.id.toString() + '-lim')
              }
              .padding({ left: 2, right: 2 })
            }
            .scrollable(ScrollDirection.Horizontal)
            .scrollBar(BarState.Off)
            .width('100%')
          }
          .width('100%')

          // 3. 本周热销榜
          Column({ space: 10 }) {
            Row() {
              Text('本周热销榜')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(SN_COLORS.textMain)
              Column().layoutWeight(1)
              Text('TOP 5')
                .fontSize(10)
                .fontColor(SN_COLORS.accent)
            }
            .width('100%')

            Column({ space: 10 }) {
              ForEach(snTopShoes(5), (s: SNShoe, idx: number) => {
                Row({ space: 10 }) {
                  Text((idx + 1).toString())
                    .fontSize(17)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(idx === 0 ? SN_COLORS.warn : (idx === 1 ? SN_COLORS.textMain : SN_COLORS.textDim))
                    .width(26)
                    .textAlign(TextAlign.Center)

                  Column()
                    .width(44)
                    .height(44)
                    .borderRadius(10)
                    .linearGradient({
                      angle: 120,
                      colors: [[s.colorHex, 0], ['#232331', 1]]
                    })

                  Column({ space: 5 }) {
                    Text(s.name)
                      .fontSize(12)
                      .fontWeight(FontWeight.Medium)
                      .fontColor(SN_COLORS.textMain)
                      .maxLines(1)
                    Row({ space: 6 }) {
                      Text(s.cat)
                        .fontSize(9)
                        .fontColor(SN_COLORS.primary)
                        .backgroundColor('#33264D')
                        .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                        .borderRadius(4)
                      Text('热度 ' + s.heat.toString())
                        .fontSize(9)
                        .fontColor(SN_COLORS.textSub)
                    }

                    Stack() {
                      Column()
                        .width('100%')
                        .height(4)
                        .borderRadius(2)
                        .backgroundColor(SN_COLORS.line)
                      Column()
                        .width(s.heat.toString() + '%')
                        .height(4)
                        .borderRadius(2)
                        .linearGradient({
                          angle: 0,
                          colors: [[SN_COLORS.primary, 0], [SN_COLORS.accent, 1]]
                        })
                    }
                    .width('100%')
                  }
                  .layoutWeight(1)
                  .alignItems(HorizontalAlign.Start)

                  Text('¥' + s.price.toString())
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(SN_COLORS.accent)
                }
                .width('100%')
                .padding(10)
                .backgroundColor(SN_COLORS.card)
                .borderRadius(12)
                .alignItems(VerticalAlign.Center)
              }, (s: SNShoe) => s.id.toString() + '-hot')
            }
            .width('100%')
          }
          .width('100%')

          // 4. 功能宫格
          Column({ space: 10 }) {
            Text('功能导航')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(SN_COLORS.textMain)
              .width('100%')

            Grid() {
              ForEach(SN_HOME_FUNCS, (f: SNFuncItem) => {
                GridItem() {
                  Column({ space: 6 }) {
                    Text(f.icon)
                      .fontSize(21)
                      .fontColor(SN_COLORS.accent)
                    Text(f.name)
                      .fontSize(10)
                      .fontColor(SN_COLORS.textSub)
                  }
                  .width('100%')
                  .padding({ top: 13, bottom: 13 })
                  .justifyContent(FlexAlign.Center)
                }
                .backgroundColor(SN_COLORS.card)
                .borderRadius(12)
                .onClick(() => {
                  this.onGoTab(f.tab)
                })
              }, (f: SNFuncItem) => f.id.toString())
            }
            .columnsTemplate('1fr 1fr 1fr 1fr')
            .columnsGap(10)
            .rowsGap(10)
            .width('100%')
            .height(178)
          }
          .width('100%')
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 14, bottom: 26 })
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(SN_COLORS.bg)
  }
}

// ============ 球鞋 Tab ============
@Component
struct SNShoeTab {
  @State shoeCat: string = '全部'
  @State showEdit: boolean = false
  @State editShoe: SNShoe | null = null
  @State editTarget: number = 1500
  @State editHeat: number = 70

  build() {
    Stack() {
      Scroll() {
        Column({ space: 14 }) {
          // 分类筛选
          Row({ space: 8 }) {
            Text('全部')
              .fontSize(11)
              .fontColor(this.shoeCat === '全部' ? SN_COLORS.bg : SN_COLORS.textSub)
              .backgroundColor(this.shoeCat === '全部' ? SN_COLORS.accent : SN_COLORS.card)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .borderRadius(14)
              .onClick(() => {
                this.shoeCat = '全部'
              })
            Text('跑步')
              .fontSize(11)
              .fontColor(this.shoeCat === '跑步' ? SN_COLORS.bg : SN_COLORS.textSub)
              .backgroundColor(this.shoeCat === '跑步' ? SN_COLORS.accent : SN_COLORS.card)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .borderRadius(14)
              .onClick(() => {
                this.shoeCat = '跑步'
              })
            Text('篮球')
              .fontSize(11)
              .fontColor(this.shoeCat === '篮球' ? SN_COLORS.bg : SN_COLORS.textSub)
              .backgroundColor(this.shoeCat === '篮球' ? SN_COLORS.accent : SN_COLORS.card)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .borderRadius(14)
              .onClick(() => {
                this.shoeCat = '篮球'
              })
            Text('休闲')
              .fontSize(11)
              .fontColor(this.shoeCat === '休闲' ? SN_COLORS.bg : SN_COLORS.textSub)
              .backgroundColor(this.shoeCat === '休闲' ? SN_COLORS.accent : SN_COLORS.card)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .borderRadius(14)
              .onClick(() => {
                this.shoeCat = '休闲'
              })
            Text('滑板')
              .fontSize(11)
              .fontColor(this.shoeCat === '滑板' ? SN_COLORS.bg : SN_COLORS.textSub)
              .backgroundColor(this.shoeCat === '滑板' ? SN_COLORS.accent : SN_COLORS.card)
              .padding({ left: 13, right: 13, top: 6, bottom: 6 })
              .borderRadius(14)
              .onClick(() => {
                this.shoeCat = '滑板'
              })
          }
          .width('100%')

          // 双列球鞋 Grid
          Column({ space: 10 }) {
            Row() {
              Text('全部球鞋')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(SN_COLORS.textMain)
              Column().layoutWeight(1)
              Text(snFilterShoes(this.shoeCat).length.toString() + ' 款')
                .fontSize(10)
                .fontColor(SN_COLORS.textSub)
            }
            .width('100%')

            Grid() {
              ForEach(snFilterShoes(this.shoeCat), (s: SNShoe) => {
                GridItem() {
                  Column({ space: 6 }) {
                    Stack({ alignContent: Alignment.TopEnd }) {
                      Column()
                        .width('100%')
                        .height(108)
                        .borderRadius(10)
                        .linearGradient({
                          angle: 125,
                          colors: [[s.colorHex, 0], ['#1B1B22', 1]]
                        })
                      Text(s.tag)
                        .fontSize(8)
                        .fontColor(SN_COLORS.bg)
                        .backgroundColor(SN_COLORS.warn)
                        .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                        .borderRadius(4)
                        .margin(6)
                    }
                    .width('100%')

                    Text(s.name)
                      .fontSize(12)
                      .fontWeight(FontWeight.Medium)
                      .fontColor(SN_COLORS.textMain)
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })

                    Row({ space: 5 }) {
                      Text(s.cat)
                        .fontSize(9)
                        .fontColor(SN_COLORS.primary)
                      Text('·')
                        .fontSize(9)
                        .fontColor(SN_COLORS.textDim)
                      Text('热度 ' + s.heat.toString())
                        .fontSize(9)
                        .fontColor(SN_COLORS.textSub)
                    }
                    .width('100%')

                    Row({ space: 6 }) {
                      Text('¥' + s.price.toString())
                        .fontSize(14)
                        .fontWeight(FontWeight.Bold)
                        .fontColor(SN_COLORS.accent)
                      Text('¥' + s.oldPrice.toString())
                        .fontSize(10)
                        .fontColor(SN_COLORS.textDim)
                        .decoration({ type: TextDecorationType.LineThrough })
                    }
                    .width('100%')
                    .alignItems(VerticalAlign.Bottom)

         
  }
}


13.6 总结:

在这里插入图片描述

基于HarmonyOS ArkTS API 24的开发框架为开发者提供了强大的UI构建能力。从这个应用中我们可以看到,使用ArkTS声明式UI范式,开发者可以快速构建出功能丰富、界面美观的原生应用。

展望未来,HarmonyOS的开发技术还将继续演进。我们可以期待以下几个方向的发展:

第一,更强大的状态管理。随着应用规模的扩大,组件内部状态管理将难以满足需求。更加强大和灵活的全局状态管理方案将成为开发者的迫切需求。

第二,更丰富的动画能力。ArkTS的动画系统已经相当强大,但在复杂动画、物理动画、共享元素动画等方面还有提升空间。更丰富的动画能力将使得应用的交互体验更加流畅和自然。

第三,更完善的组件库。虽然ArkUI已经提供了丰富的内置组件,但在一些特定场景(如图表、富文本、复杂列表等)仍然需要开发者自己实现。更完善的组件生态将大大提升开发效率。

第四,更好的性能优化工具。性能优化是移动端开发永恒的主题。更智能的框架优化、更强大的性能分析工具,将帮助开发者构建更加流畅的应用。

第五,跨设备能力的深化。HarmonyOS的分布式能力是其核心竞争力之一。未来,跨设备的状态同步、UI自适应、任务流转等能力将进一步增强,为用户带来更加无缝的跨设备体验。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐