HarmonyOS 6.1 实战:MERCHANT_LIST.slice()——先对原数组进行浅拷贝,再对拷贝进行排序
在万物互联时代,鸿蒙 HarmonyOS 以其分布式架构和一次开发多端部署的核心理念,正在重塑移动应用开发的范式。ArkTS 作为鸿蒙生态的首选开发语言,在 TypeScript 的基础上进行了深度扩展,引入了声明式 UI 编程模型、状态驱动渲染机制以及丰富的装饰器系统,使得开发者能够以极简的代码构建出复杂而高性能的跨设备应用界面。本文将以一个完整的二手数码竞价回收平台为蓝本,逐行逐段地剖析其架构设计、数据建模、组件拆分、状态管理、动画系统以及布局策略,全方位展示 ArkTS 在电商级应用场景下的工程实践。
一、鸿蒙开发背景与 ArkTS 语言特性
鸿蒙操作系统(HarmonyOS)是华为推出的面向全场景的分布式操作系统,其应用开发框架经历了从 Java 风格的 Ability 模型到 Stage 模型的演进,UI 声明范式也逐步收敛到 ArkUI 体系。ArkUI 是一套以 TypeScript 为语言基础的声明式 UI 框架,核心思想是"状态驱动视图"——开发者只需要声明界面的结构和初始状态,当状态数据发生变化时,框架会自动触发 UI 的局部重绘,无需手动调用 DOM 操作或刷新命令。这种模式极大降低了状态与视图同步的心智负担,是现代前端范式的典型代表。
ArkTS 在 TypeScript 之上做了若干约束与增强。一方面,它保留了 TS 的静态类型系统,要求所有变量、参数、返回值都有明确的类型标注,编译期即可捕获大量类型错误;另一方面,它引入了一批装饰器来标注组件、状态、构建函数、入口等关键语义,例如 @Component 标记一个可复用的自定义组件,@Entry 标记应用入口组件,@State 声明组件内部可观察的状态变量,@Builder 定义可复用的 UI 构建片段。这些装饰器不是简单的注解,它们会在编译期被转换为框架可识别的渲染指令,驱动声明式 UI 树的构建与差量更新。
组件化开发思想在本平台中得到了充分体现。整个应用被拆分为一个 @Entry 主入口组件和六个 @Component 子组件,分别对应底部导航的六个标签页。主入口组件负责整体布局骨架——顶部内容区和底部 TabBar,通过一个 @State activeTab 状态变量控制当前激活的标签页,利用 if/else 条件分支动态渲染对应的子组件。这种"主组件 + 子组件"的拆分模式,既保证了代码的模块化,又让每个标签页拥有独立的状态空间和构建逻辑,互不干扰。
声明式 UI 的另一个显著特征是链式属性调用。在 ArkTS 中,每个组件(如 Text、Column、Row)后面都可以紧跟一串以点号连接的属性方法,如 .fontSize()、.fontColor()、.padding()、.borderRadius() 等。这些方法返回组件自身,因此可以无限链式调用。这种写法虽然与 Flutter 的 Widget 嵌套风格不同,但本质上也是在描述一个组件树的节点属性,框架会在内部将其转化为渲染指令。
核心概念:状态驱动渲染。ArkTS 的 UI 不是命令式地"画"出来的,而是声明式地"描述"出来的。当
@State变量改变时,框架自动比对新旧 UI 描述的差异,只更新变化的部分。这种细粒度的差量更新是 ArkUI 高性能的基础。
二、整体架构与组件关系总览
在深入代码细节之前,先从宏观层面理解整个应用的架构。本平台采用单文件多组件的组织方式,包含一个入口组件和六个标签页组件,外加若干全局数据模型和纯函数。这种结构清晰地体现了"数据—逻辑—视图"的分层思路。

上图展示了主入口组件如何通过 activeTab 状态变量驱动内容区的条件渲染,以及底部 TabBar 如何通过点击事件反向修改该状态,形成完整的用户交互闭环。这种"状态变量 + 条件渲染 + 事件回调修改状态"的模式,是 ArkUI 最核心的编程模型。
从数据流的角度来看,整个应用的数据是单向流动的:全局常量数据(如品牌列表、机型列表、回收商列表等)作为不可变的"数据源",通过纯函数进行筛选、排序、计算后,传递给组件的 build() 方法进行渲染。组件内部的 @State 变量仅管理局部的 UI 交互状态(如当前选中的标签、弹框是否显示、步骤进度等),不持有业务数据。这种设计使得数据层与视图层职责分明,便于维护和测试。

三、配色系统与全局常量定义
3.1 配色接口与常量对象
interface RbColor {
bg: string; card: string; primary: string; accent: string; orange: string; ink: string; sub: string; hint: string; mint: string; line: string; danger: string;
white: string;
}
const CO: RbColor = {
'bg': '#F4F8F6', 'card': '#FFFFFF', 'primary': '#1E6B52', 'accent': '#3BB08F', 'orange': '#E89B3C', 'ink': '#163026', 'sub': '#6B7F77', 'hint': '#A3B5AD', 'mint': '#E3F2ED',
'line': '#DCE9E3', 'danger': '#D9534F', 'white': '#FFFFFF'
}

这段代码定义了整个应用的视觉基调——配色系统。首先通过 interface RbColor 声明了一个接口类型,它列举了应用所需的全部颜色语义槽位:背景色 bg、卡片色 card、主色 primary、辅助色 accent、强调色 orange、墨色文字 ink、次要文字 sub、提示文字 hint、薄荷底色 mint、分割线 line、危险色 danger 以及纯白 white。
接口(interface)在 ArkTS/TypeScript 中是一种纯类型声明,它不会在运行时产生任何对象,仅用于编译期的类型检查。通过先定义接口再实现常量对象的方式,编译器能够确保 CO 常量完整地提供了所有声明的颜色字段,任何遗漏或拼写错误都会在编译期被捕获。这是一种防御性编程的实践。
const CO: RbColor 则是一个全局常量对象,使用 const 声明意味着它的引用不可被重新赋值(虽然对象内部的属性在 TypeScript 中技术上可变,但在实际使用中它被当作不可变常量对待)。整个应用中所有组件都通过 CO.primary、CO.card 等方式引用颜色值,而不是在各处硬编码十六进制色值。这种集中式配色管理带来了三大好处:第一,视觉一致性有保障,所有卡片背景都用同一个 CO.card,不会出现色差;第二,主题切换变得可行,只需替换 CO 对象即可全局换肤;第三,代码可读性强,CO.danger 比 '#D9534F' 更具语义表达力。
从配色方案本身来看,这是一套典型的"青绿科技浅色风":以墨绿 #1E6B52 为主色调,传达环保、回收的品牌调性;回收青 #3BB08F 作为辅助色用于积极状态和高亮;强调橙 #E89B3C 用于价格、促销等需要吸睛的元素;云白 #F4F8F6 作为页面底色营造清爽感。这套配色的色相分布合理,主色与辅助色同属绿系形成层次,橙色作为对比色打破单调,整体既专业又活泼。
3.2 Tab 定义与导航数据
interface TabDef {
icon: string; label: string;
}
const REBIRD_TABS: TabDef[] = [
{ icon: '🏠', label: '首页' },
{ icon: '🧮', label: '估价' },
{ icon: '⚡', label: '竞价' },
{ icon: '📦', label: '订单' },
{ icon: '🔄', label: '换新' },
{ icon: '👤', label: '我的' }
]

这段代码定义了底部导航栏的六个标签项数据。TabDef 接口只有两个字段:icon(Emoji 图标)和 label(文字标签)。REBIRD_TABS 是一个 TabDef[] 类型的数组常量,包含六个标签项。
这里使用 Emoji 作为图标是一种轻量化的方案——无需引入图标字体库或图片资源,直接使用 Unicode 字符即可在 Text 组件中渲染。虽然 Emoji 在不同平台上的渲染样式可能略有差异,但在鸿蒙设备上表现一致,且开发成本极低。对于原型开发或 MVP 阶段,这是一种非常实用的选择。
值得注意的是,虽然定义了 REBIRD_TABS 常量数组,但在主入口组件的 build() 方法中,底部 TabBar 的六个 tabItem 实际上是逐一硬编码调用的,而非通过 ForEach 遍历 REBIRD_TABS 生成。这说明 REBIRD_TABS 更多是作为设计文档式的数据声明存在,便于开发者快速了解应用有哪些标签页,而在渲染层面选择了更直观的显式调用方式。两种方式各有优劣:ForEach 更简洁但调试时不够直观,显式调用更冗余但每个 Tab 的参数一目了然。
设计决策:导航项的数据驱动 vs 显式声明。在实际项目中,如果 Tab 项可能动态变化(如权限控制下的 Tab 显隐),应使用
ForEach遍历数据数组;如果 Tab 结构固定不变,显式声明更易读。本平台选择了后者,体现了固定导航的场景特点。
四、数据模型层:十八个接口定义
本平台定义了十八个接口(interface),覆盖了应用所需的全部业务实体。这种"接口先行"的建模方式是 TypeScript 工程的最佳实践——在编写任何业务逻辑之前,先用接口定义清楚数据结构,能够为后续的组件编码提供类型安全保障。
4.1 品牌与品类模型
interface BrandT {
id: number; name: string; icon: string; hot: boolean; models: number;
}
interface CategoryT {
id: number; name: string; icon: string; desc: string;
}
BrandT 描述手机品牌实体,包含唯一标识 id、品牌名 name、Emoji 图标 icon、是否热门 hot(布尔值)以及该品牌下的机型数量 models。hot 字段是一个布尔标记,用于在品牌宫格中对热门品牌做视觉区分(如加红色"热"角标)。
CategoryT 描述回收品类,比品牌模型多了 desc 描述字段。品类是比品牌更高一层的分类维度——品牌是"手机"品类下的厂商,而品类覆盖手机、平板、相机、游戏机等多种设备类型。这两个接口的关系是:一个品类包含多个品牌,一个品牌包含多个机型。
4.2 Banner 与高价机型模型
interface BannerT {
id: number; title: string; sub: string; icon: string; tag: string; bg: string;
}
interface HotModelT {
id: number; name: string; icon: string; brand: string; kind: string; maxPrice: number; upRate: number; recycled: number; tag: string;
}

BannerT 是首页横幅广告卡片的数据模型,字段包括标题 title、副标题 sub、图标 icon、标签 tag(如"加价 15%")和背景色 bg。每个 Banner 自带背景色字段,使得不同 Banner 可以呈现不同的视觉风格,这是数据驱动设计的体现——视觉差异通过数据而非代码分支来实现。
HotModelT 是高价回收机型的模型,是整个应用最核心的业务实体之一。它包含了机型名、所属品牌 brand、设备类型 kind、最高回收价 maxPrice、涨价幅度 upRate(百分比)、已回收数量 recycled 以及状态标签 tag。这个模型的字段密度很高,反映了二手回收业务对价格敏感度的高要求——用户需要同时看到最高价、涨幅趋势和成交量才能做出回收决策。
4.3 价格趋势与回收商模型
interface TrendT {
id: number; month: string; avg: number; volume: number;
}
interface MerchantT {
id: number; name: string; icon: string; price: number; rating: number; orders: number; speed: string; pay: string; certified: boolean; delta: number;
}

TrendT 是价格趋势数据模型,按月记录某机型的平均回收价 avg 和成交量 volume,用于在首页绘制柱状趋势图。MerchantT 是回收商模型,字段最为丰富:出价 price、评分 rating(1-5 星)、成交量 orders、上门速度 speed、打款速度 pay、是否认证 certified 以及加价幅度 delta。这些字段共同构成了用户选择回收商的决策矩阵。
4.4 订单、记录与档位模型
interface OrderT {
id: number; no: string; device: string; icon: string; price: number; status: string; step: number; date: string; merchant: string;
}
interface RecordT {
id: number; device: string; icon: string; price: number; grade: string; date: string; valid: boolean;
}
interface GradeT {
id: number; name: string; desc: string; ratio: number; icon: string;
}
interface CapT {
name: string; ratio: number;
}

OrderT 是回收订单模型,其中 step 字段(1-5)表示订单在五步流程中的当前节点,status 是文字状态描述(如"验机中"“已完成”)。RecordT 是估价记录模型,valid 布尔值标识报价是否仍在有效期内。GradeT 是成色档位模型,ratio 是价格系数(如 99 新为 0.92,即原价的 92%),这个系数直接参与估价计算。CapT 是容量档位模型,同样有 ratio 系数,大容量机型回收价更高。
4.5 换新、任务、分布与验机模型
interface TradeinT {
id: number; name: string; icon: string; price: number; old: number; subsidy: number; stock: boolean; tag: string;
}
interface TaskT {
id: number; name: string; icon: string; points: number; desc: string; done: boolean;
}
interface DistT {
id: number; name: string; ratio: number; color: string; count: number;
}
interface InspectT {
id: number; item: string; result: string; ok: boolean; note: string;
}

TradeinT 是以旧换新机型模型,包含新机价格 price、旧机抵扣 old、平台补贴 subsidy 和库存状态 stock,到手价通过 price - old - subsidy 计算得出。TaskT 是积分任务模型,done 标识任务是否完成。DistT 是品类分布模型,用于绘制环形图,自带 color 字段实现数据驱动的图表配色。InspectT 是验机检查项模型,ok 布尔值决定检查结果是通过还是不通过。
4.6 地址、城市与流程模型
interface SlotT {
id: number; day: string; date: string; times: string; left: number;
}
interface AddressT {
id: number; name: string; phone: string; region: string; detail: string; tag: string; def: boolean;
}
interface CityT {
id: number; name: string; zone: string; merchants: number; icon: string;
}
interface StepT {
id: number; title: string; desc: string; icon: string;
}
SlotT 是上门时间段模型,left 字段表示该时段剩余可预约数量,制造稀缺感促进转化。AddressT 是收货地址模型,tag 字段用于地址分类(公司/家/亲属),def 标识是否默认地址。CityT 是服务城市模型,zone 字段表示所属区域(华北/华东等),merchants 表示该城市的网点数量。StepT 是回收流程步骤模型,用于首页"回收六步曲"的展示。
类型系统的价值。十八个接口构成了完整的领域模型,它们不仅是编译期的类型检查工具,更是业务需求的精确文档。任何开发者阅读这些接口定义,就能快速理解应用涉及哪些业务实体、每个实体有哪些属性、属性的类型是什么。这是"类型即文档"理念的生动体现。
五、Mock 数据常量的组织与设计
5.1 品牌列表数据
const BRAND_LIST: BrandT[] = [
{ id: 1, name: 'Apple', icon: '🍎', hot: true, models: 48 },
{ id: 2, name: '华为', icon: '🛰️', hot: true, models: 52 },
{ id: 3, name: '小米', icon: '🌀', hot: true, models: 38 },
// ... 共 12 个品牌
]
BRAND_LIST 包含 12 个手机品牌的数据。每个品牌对象都严格遵循 BrandT 接口定义,包含 id、name、icon、hot、models 五个字段。数据按 models(机型数量)大致降序排列——华为 52 款最多,Apple 48 款次之,这与真实市场的品牌机型分布基本吻合。
hot 字段的分布体现了运营策略:Apple、华为、小米、荣耀被标记为热门品牌(hot: true),这些品牌在二手市场的流通量最大、用户关注度最高。在 UI 层面,热门品牌可能会获得视觉上的强调(如角标或排序优先),这个标记为后续的视觉差异化提供了数据依据。
5.2 品类与 Banner 数据
const CATEGORY_LIST: CategoryT[] = [
{ id: 1, name: '手机', icon: '📱', desc: '覆盖 300+ 型号' },
{ id: 2, name: '平板', icon: '📲', desc: 'iPad / 安卓平板' },
{ id: 3, name: '相机', icon: '📷', desc: '单反 / 微单 / 镜头' },
{ id: 4, name: '游戏机', icon: '🎮', desc: 'Switch / PS5 / XBOX' },
{ id: 5, name: '笔记本', icon: '💻', desc: '轻薄本 / 游戏本' },
{ id: 6, name: '智能手表', icon: '⌚', desc: 'AppleWatch 等' },
{ id: 7, name: '耳机', icon: '🎧', desc: '头戴 / 真无线' },
{ id: 8, name: '无人机', icon: '🚁', desc: '大疆全系' }
]
品类列表共 8 项,每项的 desc 字段提供了该品类的简短描述。这 8 个品类覆盖了数码产品回收的主要品类范围。desc 字段的文案设计注重信息量——"覆盖 300+ 型号"既是对手机品类的说明,也是一种营销话术,暗示平台的服务覆盖能力。
Banner 数据共 4 条,每条自带 bg 背景色字段,分别是墨绿、回收青、强调橙和深青蓝四种背景色。这种数据驱动的配色方案使得四张 Banner 卡片在首页以 2x2 网格排列时呈现丰富的视觉层次。
5.3 高价机型与价格趋势数据
const HOT_MODEL_LIST: HotModelT[] = [
{ id: 1, name: 'iPhone 15 Pro Max', icon: '🍏', brand: 'Apple', kind: '手机', maxPrice: 7200, upRate: 3.2, recycled: 1286, tag: '加价中' },
{ id: 2, name: 'iPhone 14 Pro', icon: '🍏', brand: 'Apple', kind: '手机', maxPrice: 5150, upRate: 2.1, recycled: 2341, tag: '热收' },
// ... 共 15 个机型
]
const TREND_LIST: TrendT[] = [
{ id: 1, month: '1月', avg: 4600, volume: 821 },
{ id: 2, month: '2月', avg: 4550, volume: 903 },
// ... 共 10 个月
]
HOT_MODEL_LIST 是 15 个高价回收机型的数据集,覆盖了手机、平板、游戏机、相机、无人机等多个品类。每个机型记录了最高回收价、涨价幅度和已回收数量。tag 字段有三种植:"加价中"表示回收商正在竞价推高价格、"热收"表示该机型回收热度高、"稳定"表示价格波动小、"高价"表示绝对价格较高。这些标签帮助用户快速识别机型的回收状态。
TREND_LIST 是 iPhone 14 Pro 近 10 个月的均价趋势数据。从 1 月的 4600 元到 10 月的 4250 元,整体呈下降趋势,跌幅约 7.6%,这符合电子产品随时间贬值的规律。同时 volume(成交量)呈上升趋势,说明随着价格下降,更多用户选择回收。这组数据将在首页以柱状图形式展示。
5.4 回收商竞价数据
const MERCHANT_LIST: MerchantT[] = [
{ id: 1, name: '回收兽严选自营', icon: '🦜', price: 4520, rating: 5, orders: 12876, speed: '2小时上门', pay: '验机后30分钟', certified: true, delta: 120 },
{ id: 2, name: '估吗优品回收', icon: '📐', price: 4480, rating: 5, orders: 9876, speed: '当天上门', pay: '验机后1小时', certified: true, delta: 80 },
// ... 共 12 家回收商
]
MERCHANT_LIST 是 12 家回收商的竞价数据,是竞价页面的核心数据源。每家回收商的 price 字段就是其对当前设备的出价,从 4520 元(最高)到 4120 元(最低),差距 400 元。delta 字段表示该回收商相比上一轮竞价的加价幅度,数值越大说明回收商回收意愿越强。
certified 字段标识回收商是否通过平台认证。12 家中有 8 家已认证,认证回收商享受平台先行赔付保障。rating 是星级评分(1-5),orders 是近 30 天成交量,这两个字段共同构成回收商的信誉指标。speed 和 pay 分别描述上门速度和打款速度,是用户选择回收商的关键服务指标。
5.5 订单与估价记录数据
const ORDER_LIST: OrderT[] = [
{ id: 1, no: 'RB2026082701', device: 'iPhone 14 Pro', icon: '🍏', price: 4480, status: '验机中', step: 3, date: '08-27', merchant: '回收兽严选自营' },
{ id: 2, no: 'RB2026082603', device: '华为 Mate 60 Pro', icon: '🛰️', price: 5600, status: '待上门', step: 2, date: '08-26', merchant: '估吗优品回收' },
// ... 共 15 条订单
]
const EST_RECORD_LIST: RecordT[] = [
{ id: 1, device: 'iPhone 15 Pro Max', icon: '🍏', price: 6480, grade: '95新', date: '08-27 09:12', valid: true },
// ... 共 8 条记录
]
ORDER_LIST 包含 15 条回收订单,每条订单的 status 字段有五种取值:待上门、验机中、待打款、已完成、已取消。step 字段(1-5)与 status 对应,表示订单在"提交订单→上门取件→验机检测→确认报价→打款到账"五步流程中的当前节点。订单号 no 采用 RB + 日期 + 序号 的格式,便于人工识别。
EST_RECORD_LIST 是 8 条估价记录,valid 字段标识报价是否在有效期内。估价报价通常有 7 天有效期,过期后需要重新估价。这个 valid 标记在 UI 上通过文字颜色区分——有效报价显示绿色"报价有效中",过期报价显示灰色"报价已过期"。
5.6 成色、容量、换新与任务数据
const GRADE_LIST: GradeT[] = [
{ id: 1, name: '99新', desc: '仅拆封 / 几乎无使用痕迹', ratio: 0.92, icon: '✨' },
{ id: 2, name: '95新', desc: '轻微使用痕 / 功能完好', ratio: 0.84, icon: '🌟' },
{ id: 3, name: '9成新', desc: '少量划痕 / 无维修', ratio: 0.72, icon: '🙂' },
{ id: 4, name: '8成新', desc: '明显使用痕迹 / 功能正常', ratio: 0.58, icon: '😯' },
{ id: 5, name: '有明显磕碰', desc: '磕碰掉漆或屏幕划伤', ratio: 0.42, icon: '⚠️' }
]
const CAP_LIST: CapT[] = [
{ name: '64GB', ratio: 0.82 },
{ name: '128GB', ratio: 0.90 },
{ name: '256GB', ratio: 1.00 },
{ name: '512GB', ratio: 1.12 },
{ name: '1TB', ratio: 1.22 }
]
GRADE_LIST 定义了 5 档成色及其价格系数。从 99 新的 0.92 到有明显磕碰的 0.42,成色每降一档价格近乎打八折。CAP_LIST 定义了 5 档容量及其价格系数,以 256GB 为基准(1.00),容量越小系数越低,容量越大系数越高。这两组系数在估价计算函数 quotePrice() 中参与乘法运算,共同决定最终报价。
成色和容量的系数设计体现了二手市场的定价逻辑:成色对价格的影响远大于容量。从 99 新到有明显磕碰,价格从 92% 降到 42%,降幅超过一半;而从 64GB 到 1TB,价格从 82% 升到 122%,增幅约 49%。这种差异化的系数设计使得估价结果更贴近真实市场行情。
const TRADEIN_LIST: TradeinT[] = [
{ id: 1, name: 'iPhone 16 Pro', icon: '🍏', price: 7999, old: 4480, subsidy: 600, stock: true, tag: '热门' },
// ... 共 12 个换新机型
]
const TASK_LIST: TaskT[] = [
{ id: 1, name: '每日签到', icon: '📅', points: 5, desc: '连续签到 7 天得 50 积分', done: false },
// ... 共 8 个任务
]
TRADEIN_LIST 是 12 个可换新机型,每个机型记录了新机价格、旧机抵扣价和平台补贴。stock 字段标识是否有现货,tag 字段有"热门"“补贴高”“新品”“预售”“轻换”"办公"等多种标签。TASK_LIST 是 8 个积分任务,done 字段标识完成状态,初始有 3 个已完成。
5.7 分布、验机、时段与地址数据
const DIST_LIST: DistT[] = [
{ id: 1, name: '手机', ratio: 42, color: '#1E6B52', count: 66 },
{ id: 2, name: '平板', ratio: 18, color: '#3BB08F', count: 28 },
{ id: 3, name: '相机', ratio: 16, color: '#E89B3C', count: 25 },
{ id: 4, name: '游戏机', ratio: 14, color: '#2A5A6B', count: 22 },
{ id: 5, name: '其他数码', ratio: 10, color: '#A3B5AD', count: 17 }
]
const INSPECT_LIST: InspectT[] = [
{ id: 1, item: '外观成色', result: '95新 · 边框轻微划痕', ok: true, note: '不影响回收价' },
{ id: 2, item: '屏幕显示', result: '显示正常无坏点', ok: true, note: '烧屏检测通过' },
// ... 共 10 项检查
]
DIST_LIST 是用户的回收品类分布数据,5 个品类的比例加起来正好 100%,对应环形图的五段弧线。每个品类自带 color 字段,与主配色方案一致,确保图表与应用整体风格协调。INSPECT_LIST 是 10 项验机检查结果,ok 全为 true 表示设备通过了所有检测。
验机项的设计非常专业,覆盖了外观、屏幕、电池、摄像头、面容/指纹、听筒扬声器、WiFi 蓝牙、按键接口、进水检测和维修记录十个维度。note 字段提供了每项检查的技术说明,如"烧屏检测通过"“试纸无变色”"螺丝原封"等,这些细节增强了验机报告的可信度。
const SLOT_LIST: SlotT[] = [
{ id: 1, day: '今天', date: '08-27', times: '09:00-11:00', left: 3 },
{ id: 2, day: '今天', date: '08-27', times: '14:00-16:00', left: 5 },
// ... 共 9 个时段
]
const ADDRESS_LIST: AddressT[] = [
{ id: 1, name: '李维', phone: '138****6672', region: '北京市朝阳区', detail: '望京SOHO T2 座 1808', tag: '公司', def: true },
// ... 共 3 条地址
]
const CITY_LIST: CityT[] = [
{ id: 1, name: '北京', zone: '华北', merchants: 12, icon: '🏛️' },
// ... 共 10 个城市
]
const FLOW_STEPS: StepT[] = [
{ id: 1, title: '在线估价', desc: '30 秒选机型看成色报价', icon: '🧮' },
{ id: 2, title: '货比三家', desc: '12 家回收商实时竞价', icon: '⚡' },
{ id: 3, title: '免费上门', desc: '顺丰包邮或师傅上门', icon: '🚚' },
{ id: 4, title: '专业验机', desc: '26 项检测全程录像', icon: '🔍' },
{ id: 5, title: '确认打款', desc: '满意再卖极速到账', icon: '💰' },
{ id: 6, title: '环保处理', desc: '数据清除绿色再生', icon: '🌱' }
]
SLOT_LIST 是 9 个上门时间段,分布在今天、明天、后天三天,每天三个时段。left 字段表示剩余名额,制造紧迫感。"今天 18:00-20:00"只剩 2 单,这种稀缺性设计能有效促进用户尽快预约。ADDRESS_LIST 是 3 条收货地址,手机号做了脱敏处理(中间四位用星号替代),这是隐私保护的体现。CITY_LIST 是 10 个服务城市,每个城市自带 Emoji 图标增加辨识度。FLOW_STEPS 是回收六步流程,每步包含标题、描述和图标,是首页流程展示区的数据源。
5.8 辅助数组常量
const TRACK_STEPS: string[] = ['提交订单', '上门取件', '验机检测', '确认报价', '打款到账']
const EST_STEPS: string[] = ['选品牌', '选机型', '选出成色', '查看报价']
const ALERT_METHODS: string[] = ['App 推送', '短信提醒', '微信服务号']
const BID_DEVICES: string[] = ['iPhone 14 Pro', '华为 Mate 60 Pro', '小米 14 Pro', 'iPad Air 5', 'Switch OLED', 'PS5 Slim']
这四个字符串数组是辅助性的 UI 数据常量。TRACK_STEPS 是订单进度的五个节点名称,EST_STEPS 是估价器的四个步骤名称,ALERT_METHODS 是加价提醒的三种通知方式,BID_DEVICES 是竞价页面的六个可切换设备。这些数组虽然简单,但将 UI 文案与组件逻辑分离,使得后续的国际化或文案调整变得容易。
数据层的整体设计哲学。本平台将所有 Mock 数据定义为全局
const常量,而非放在组件内部或通过异步请求获取。这种设计在原型开发阶段非常高效——数据立即可用、无需等待网络请求、类型完全安全。在实际生产环境中,这些常量会被替换为 API 调用的返回值,但由于接口类型已经定义好,组件代码几乎不需要修改,只需将常量引用改为异步赋值即可。这种"接口先行、数据后填"的模式极大地降低了从原型到生产的迁移成本。
六、全局纯函数层详解
本平台定义了十余个全局纯函数,它们是数据层与视图层之间的"逻辑中间件"。纯函数的特点是:给定相同的输入,总是返回相同的输出,且不产生副作用。这些函数不修改任何全局状态,只是对数据常量进行筛选、排序、计算和格式化。
6.1 格式化函数
function starText(r: number): string {
let s: string = ''
for (let i = 0; i < r; i++) {
s += '★'
}
return s
}
function money(v: number): string {
return '¥' + v
}
starText 函数将数字评分转换为星号字符串,如输入 3 返回 '★★★'。这个函数在回收商列表中使用,将 rating 字段(1-5 的数字)渲染为直观的星级文本。使用 for 循环拼接字符串是最朴素的实现方式,在 ArkTS 中,函数体内首行可以出现 let 声明(这是 ArkTS 的语法约束之一——let 只能在函数体首行或后续语句中使用,不能在 build() 方法内随意声明局部变量)。
money 函数是金额格式化器,在数字前加上人民币符号。虽然实现极其简单,但它的存在意义重大——全应用所有价格显示都通过这个函数格式化,如果未来需要改为千分位分隔或保留小数,只需修改这一个函数即可。
6.2 排序与筛选函数
function merchantsSorted(): MerchantT[] {
let r: MerchantT[] = MERCHANT_LIST.slice()
r.sort((a: MerchantT, b: MerchantT) => b.price - a.price)
return r
}
function hotSorted(): HotModelT[] {
let r: HotModelT[] = HOT_MODEL_LIST.slice()
r.sort((a: HotModelT, b: HotModelT) => b.maxPrice - a.maxPrice)
return r
}
merchantsSorted 函数返回按出价降序排列的回收商列表。关键在于第一步 MERCHANT_LIST.slice()——先对原数组进行浅拷贝,再对拷贝进行排序。如果直接对 MERCHANT_LIST 调用 sort(),会修改全局常量的原始顺序,导致后续依赖原始顺序的逻辑出错。slice() 不带参数时返回数组的完整浅拷贝,是函数式编程中保护原数据的常用技巧。
排序的比较函数 (a, b) => b.price - a.price 是降序排列的标准写法。当 b.price > a.price 时返回正数,sort 会将 b 排在 a 前面,从而实现从高到低排列。hotSorted 函数的逻辑完全相同,只是作用于 HOT_MODEL_LIST 并按 maxPrice 排序。
function filterModels(brand: string): HotModelT[] {
let r: HotModelT[] = []
for (let i = 0; i < HOT_MODEL_LIST.length; i++) {
if (HOT_MODEL_LIST[i].brand === brand) {
r.push(HOT_MODEL_LIST[i])
}
}
return r
}
function filterOrders(st: string): OrderT[] {
let r: OrderT[] = []
for (let i = 0; i < ORDER_LIST.length; i++) {
if (ORDER_LIST[i].status === st) {
r.push(ORDER_LIST[i])
}
}
return r
}
function orderCount(st: string): number {
let n: number = 0
for (let i = 0; i < ORDER_LIST.length; i++) {
if (ORDER_LIST[i].status === st) {
n++
}
}
return n
}
这三个函数实现了按条件筛选和计数的功能。filterModels 按品牌名筛选机型,filterOrders 按状态筛选订单,orderCount 统计某状态的订单数量。它们都使用 for 循环遍历数组,通过条件判断构建结果集。虽然 ArkTS 支持 filter() 等数组高阶方法,但这里选择了显式循环写法,更符合 ArkTS 的语法偏好和编译优化要求。
filterModels 在估价器中被调用——当用户选择了某个品牌后,该函数返回该品牌下的所有机型供用户选择。filterOrders 和 orderCount 在订单页面被调用——用户点击状态筛选胶囊时,filterOrders 返回对应状态的订单列表,orderCount 则用于在筛选胶囊上显示各状态的订单数量。
6.3 计算函数
function trendMax(): number {
let m: number = 0
for (let i = 0; i < TREND_LIST.length; i++) {
if (TREND_LIST[i].avg > m) {
m = TREND_LIST[i].avg
}
}
return m
}
function merchantMax(): number {
let m: number = 0
for (let i = 0; i < MERCHANT_LIST.length; i++) {
if (MERCHANT_LIST[i].price > m) {
m = MERCHANT_LIST[i].price
}
}
return m
}
function trendBarH(v: number): number {
return Math.round(v / trendMax() * 92)
}
function barPercent(v: number, max: number): number {
return Math.round(v / max * 100)
}
trendMax 和 merchantMax 分别返回趋势数据和回收商数据中的最大值。这两个最大值用于归一化计算——将绝对数值转换为相对比例,以便在 UI 上绘制柱状图和对比条。
trendBarH 函数接收一个均价数值,返回其在柱状图中的像素高度(最大 92px)。计算方式是 v / trendMax() * 92,即当前值占最大值的比例乘以最大高度。Math.round 确保返回整数像素值,避免亚像素渲染导致的模糊。
barPercent 函数更通用,接收一个值和一个最大值,返回百分比整数。这个函数在竞价页面用于计算横向对比条的宽度百分比——每个回收商的出价占最高出价的百分比,决定对比条的视觉长度。
function quotePrice(mIdx: number, gIdx: number, cIdx: number): number {
let p: number = HOT_MODEL_LIST[mIdx].maxPrice * GRADE_LIST[gIdx].ratio * CAP_LIST[cIdx].ratio
return Math.round(p)
}
function tradeinFinal(t: TradeinT): number {
return t.price - t.old - t.subsidy
}
quotePrice 是估价器的核心计算函数。它接收三个索引参数(机型索引、成色索引、容量索引),从对应列表中取出数据,将最高回收价乘以成色系数和容量系数,得到最终估价。这个三重乘法的设计体现了二手定价的基本公式:基础价 × 成色折扣 × 容量调整。Math.round 对结果取整,避免出现小数金额。
tradeinFinal 计算以旧换新的到手价:新机价格减去旧机抵扣再减去平台补贴。这个计算非常直观——用户用旧机抵扣一部分钱,平台再补贴一部分,剩下的就是需要支付的差价。
6.4 颜色映射函数
function rankColor(i: number): string {
if (i === 0) {
return '#E89B3C'
}
if (i === 1) {
return '#3BB08F'
}
if (i === 2) {
return '#1E6B52'
}
return '#A3B5AD'
}
function statusColor(s: string): string {
if (s === '已完成') {
return CO.accent
}
if (s === '已取消') {
return CO.hint
}
return CO.orange
}
function barColor(i: number): string {
if (i === TREND_LIST.length - 1) {
return CO.orange
}
if (i % 3 === 2) {
return CO.accent
}
return CO.primary
}
function tradeTagColor(t: string): string {
if (t === '热门') {
return CO.orange
}
if (t === '补贴高') {
return CO.danger
}
return CO.accent
}
这四个函数都是颜色映射器,根据不同的条件返回对应的颜色值。rankColor 为榜单前三名分别返回金、绿、墨绿三色,第四名及以后返回灰色。这种"奖牌色"设计在排行榜 UI 中非常常见,能够直观传达排名信息。
statusColor 将订单状态文字映射为颜色——已完成用绿色(积极)、已取消用灰色(消极)、其他进行中状态用橙色(进行中/警示)。这种语义化的颜色映射让用户一眼就能识别订单状态。
barColor 为趋势柱状图的柱子着色——最后一根(最新月)用橙色突出,每第三根用回收青做节奏点缀,其余用主色。这种着色策略既突出最新数据,又通过节奏感避免了纯色柱状图的单调。
tradeTagColor 将换新机型标签映射为颜色——"热门"用橙色(醒目)、"补贴高"用红色(强调优惠力度)、其他用回收青。这种映射使得不同标签在视觉上有明确的区分度。
纯函数的工程价值。将业务逻辑抽离为全局纯函数,而非散落在各组件内部,带来了显著的工程收益:可测试性(纯函数易于单元测试)、可复用性(多个组件可共享同一函数)、可维护性(逻辑集中在一处便于修改)、可推理性(无副作用使得函数行为可预测)。这是函数式编程思想在 UI 工程中的务实应用。
七、主入口组件 RebirdApp 详解
7.1 组件声明与状态定义
@Entry
@Component
struct RebirdApp {
@State activeTab: number = 0
@Entry 装饰器标记 RebirdApp 为应用的入口组件——每个 ArkTS 页面有且只有一个 @Entry 组件,它是整个组件树的根节点。@Component 装饰器声明这是一个自定义组件,可以被其他组件引用(虽然入口组件通常不被其他组件引用,但它仍然需要此装饰器来获得组件的能力)。
struct 是 ArkTS 中定义组件的关键字,与 TypeScript 的 class 不同,struct 是值类型(虽然在实际运行中 ArkTS 对组件的处理更接近引用语义)。组件的成员包括状态变量、构建器方法和 build() 方法。
@State activeTab: number = 0 声明了一个状态变量。@State 装饰器是 ArkUI 状态管理体系中最基础的一环——被它装饰的变量会被框架劫持,当其值发生变化时,框架自动触发组件的重新渲染。activeTab 初始值为 0,表示默认显示第一个标签页(首页)。
@State 的响应式机制基于观察者模式。框架在首次渲染时会收集 build() 方法中对 activeTab 的所有引用点,当 activeTab 被重新赋值时,框架通知这些引用点进行局部更新。这种细粒度的依赖追踪使得只有真正依赖该状态的 UI 片段才会重绘,而非整个组件树。
7.2 TabItem 构建器
@Builder tabItem(idx: number, icon: string, label: string) {
if (this.activeTab === idx) {
Column() {
Text(icon).fontSize(20)
Text(label).fontSize(9).fontColor(CO.primary).fontWeight(FontWeight.Bold).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 7, bottom: 5 }).onClick(() => { this.activeTab = idx })
} else {
Column() {
Text(icon).fontSize(20).opacity(0.55)
Text(label).fontSize(9).fontColor(CO.hint).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 7, bottom: 5 }).onClick(() => { this.activeTab = idx })
}
}
@Builder 装饰器定义了一个 UI 构建片段,类似于其他框架中的"渲染函数"或"局部组件"。与 @Component 不同,@Builder 方法不能拥有自己的状态,它直接访问宿主组件的状态和上下文。@Builder 的主要价值在于代码复用——将重复的 UI 结构抽取为构建器方法,避免代码冗余。
tabItem 构建器接收三个参数:索引 idx、图标 icon 和标签 label。它使用 if/else 条件分支区分选中态和未选中态。当 this.activeTab === idx 时(当前 Tab 被选中),图标和标签使用主色高亮显示,字重加粗;否则图标半透明(opacity(0.55)),标签使用提示色。
选中态与未选中态的差异体现在三个维度:颜色(主色 vs 提示色)、透明度(1.0 vs 0.55)和字重(Bold vs 默认)。这三个维度的差异叠加在一起,使得选中项在视觉上明显"跳出",用户能够一眼识别当前所处的标签页。
每个 tabItem 都绑定了 onClick 事件,回调函数将 this.activeTab 设置为当前索引。这就是状态驱动渲染的核心闭环:用户点击 → 修改 @State 变量 → 框架检测到变化 → 触发重渲染 → UI 更新。整个流程是自动的,开发者只需编写"点击后修改状态"这一行代码。
Column 组件是 ArkUI 的线性布局容器之一,它将子组件在垂直方向(从上到下)排列。Column 内部放了两个 Text 组件——图标在上、标签在下,形成垂直排列的 Tab 项。.layoutWeight(1) 让每个 Tab 项在水平方向等分父容器(Row)的剩余空间,.alignItems(HorizontalAlign.Center) 使子组件水平居中对齐。
7.3 主构建方法
build() {
Column() {
Column() {
if (this.activeTab === 0) {
RebirdHomeContent()
} else if (this.activeTab === 1) {
RebirdEstimateContent()
} else if (this.activeTab === 2) {
RebirdBidContent()
} else if (this.activeTab === 3) {
RebirdOrderContent()
} else if (this.activeTab === 4) {
RebirdTradeInContent()
} else {
RebirdMineContent()
}
}
.layoutWeight(1).width('100%')
Divider().color(CO.line).strokeWidth(0.5)
Row() {
this.tabItem(0, '🏠', '首页')
this.tabItem(1, '🧮', '估价')
this.tabItem(2, '⚡', '竞价')
this.tabItem(3, '📦', '订单')
this.tabItem(4, '🔄', '换新')
this.tabItem(5, '👤', '我的')
}
.width('100%').height(58).backgroundColor(CO.card).padding({ left: 4, right: 4 })
}
.width('100%').height('100%').backgroundColor(CO.bg)
}
build() 是每个组件必须实现的方法,它返回组件的 UI 结构描述。注意 build() 方法内部不能使用 let 声明局部变量(这是 ArkTS 的语法约束),所有数据必须通过状态变量、参数传入或调用外部函数获取。
最外层的 Column 将整个页面分为上下两部分:内容区(占满剩余空间)和底部 TabBar(固定高度 58px)。.width('100%').height('100%') 使根 Column 撑满整个屏幕,.backgroundColor(CO.bg) 设置页面背景色。
内容区的 Column 使用 .layoutWeight(1) 占据除了 TabBar 之外的所有垂直空间。内部通过 if/else if/else 链根据 activeTab 的值条件渲染对应的子组件。当 activeTab 从 0 变为 1 时,框架会卸载 RebirdHomeContent 组件并挂载 RebirdEstimateContent 组件——这种完整的组件卸载/挂载意味着切换标签页时,前一个标签页的状态会被销毁(除非使用 @StorageLink 等跨组件状态管理方案)。
Divider 组件渲染一条分割线,位于内容区和 TabBar 之间。.strokeWidth(0.5) 设置线宽为 0.5 像素(实际上设备会取整为 1px),这条线虽然细微,但在视觉上明确区分了内容区和导航区。
底部 Row 容器水平排列六个 tabItem。Row 是与 Column 对应的线性布局容器,将子组件在水平方向(从左到右)排列。每个 tabItem 通过 this.tabItem(...) 语法调用,这是 @Builder 方法在组件内部的标准调用方式。
组件生命周期与状态销毁。由于 Tab 切换使用的是
if/else条件渲染而非Visibility显隐控制,切换 Tab 时前一个组件会被完全销毁、其@State变量重置。这意味着如果用户在估价页面填写了一半表单后切换到其他 Tab 再切回来,之前填写的数据会丢失。在生产应用中,若需保持状态,应使用@StorageLink或AppStorage进行跨组件状态持久化,或改用Visibility.Hidden来隐藏而非销毁组件。
八、首页组件 RebirdHomeContent 深度解析
8.1 状态变量与生命周期
@Component
struct RebirdHomeContent {
@State showVisit: boolean = false
@State selCat: number = 0
@State showAllHot: boolean = false
@State slotIdx: number = 1
@State coinScale: number = 1
@State coinOp: number = 0.5
@State pulseScale: number = 1
@State floatY: number = 0
aboutToAppear() {
this.getUIContext().animateTo({
duration: 1600,
iterations: -1,
playMode: PlayMode.Alternate,
curve: Curve.EaseInOut
}, () => {
this.coinScale = 1.18
this.coinOp = 1
})
// ... 更多动画
}
首页组件声明了八个 @State 变量,可分为两类:交互状态和动画状态。交互状态包括 showVisit(上门时间弹框是否显示)、selCat(当前选中的品类索引)、showAllHot(是否展开全部高价机型)、slotIdx(当前选中的时段索引)。动画状态包括 coinScale(金币缩放比例)、coinOp(金币透明度)、pulseScale(脉冲缩放比例)、floatY(悬浮偏移量)。
aboutToAppear() 是组件的生命周期回调,在组件创建后、build() 执行前调用。它通常用于初始化状态、发起网络请求或启动动画。这里利用它启动了三个无限循环动画。
this.getUIContext().animateTo() 是 ArkUI 的显式动画 API。它接收两个参数:第一个是动画选项对象,包含 duration(持续时间毫秒)、iterations(迭代次数,-1 表示无限循环)、playMode(播放模式,Alternate 表示交替往返)、curve(缓动曲线,EaseInOut 表示先慢后快再慢);第二个是回调函数,在回调中修改状态变量的目标值。
第一个动画实现"金币浮动呼吸"效果:coinScale 从 1 变到 1.18(放大 18%),coinOp 从 0.5 变到 1(从半透明变到不透明)。由于 playMode 设为 Alternate,动画会在 1→1.18 和 1.18→1 之间交替往复,形成"呼吸"效果。这个动画作用于标题栏旁边的金币 Emoji,吸引用户关注高价回收榜。
8.2 弹框遮罩与上门时间选择器
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor('rgba(22,48,38,0.6)').onClick(onClose)
}
modalOverlay 是一个通用的弹框遮罩构建器。它渲染一个全屏半透明遮罩层,背景色使用 rgba(22,48,38,0.6)——墨绿色调的 60% 透明度。遮罩层的 onClick 绑定了传入的 onClose 回调函数,用户点击遮罩区域时关闭弹框。
这种"参数化构建器"的设计模式非常巧妙——通过接收一个闭包函数作为参数,使得同一个遮罩构建器可以被多个弹框复用,每个弹框只需传入自己的关闭逻辑。这是高阶函数在 UI 构建中的典型应用。
@Builder visitTimeOverlay() {
Column() {
this.modalOverlay(() => { this.showVisit = false })
Column() {
Row() {
Text('🚚 选择上门时间').fontSize(16).fontWeight(FontWeight.Bold).fontColor(CO.ink)
Column().layoutWeight(1)
Text('✕').fontSize(17).fontColor(CO.hint).onClick(() => { this.showVisit = false })
}
.width('100%').padding({ left: 18, right: 18, top: 16, bottom: 10 })
Divider().color(CO.line)
Text('回收师傅将按所选时段免费上门 · 全程录像').fontSize(9).fontColor(CO.sub).width('100%').margin({ top: 12 }).padding({ left: 18, right: 18 })
visitTimeOverlay 是上门时间选择弹框。它的结构是:最外层 Column 包含遮罩层和内容面板两部分。遮罩层在上方(先渲染),内容面板在下方(后渲染),两者在垂直方向叠放。由于 Column 是垂直排列而非层叠,这里的遮罩层实际上占据顶部空间,内容面板通过 position 定位到屏幕 34% 位置,形成底部抽屉效果。
内容面板的头部是一个 Row,包含标题"选择上门时间"、一个占位的 Column().layoutWeight(1) 和一个关闭按钮"✕"。Column().layoutWeight(1) 是 ArkUI 中常见的"弹性占位"技巧——一个空的、权重为 1 的容器会占据所有剩余空间,将后续组件推到右端,实现左右分列布局。
Divider 组件渲染一条分割线,将弹框头部与内容区分隔开。Text 组件显示提示文案"回收师傅将按所选时段免费上门 · 全程录像",使用 9 号字体的次要色,作为辅助说明。
8.3 ForEach 与时段宫格
Scroll() {
Column() {
Row() {
ForEach(SLOT_LIST, (s: SlotT, idx: number) => {
if (this.slotIdx === idx) {
Column() {
Text(s.day).fontSize(11).fontColor(CO.white).fontWeight(FontWeight.Bold)
Text(s.date).fontSize(9).fontColor('#DFF3EC').margin({ top: 3 })
Text(s.times).fontSize(10).fontColor(CO.white).margin({ top: 3 })
Text('剩 ' + s.left + ' 单').fontSize(8).fontColor('#DFF3EC').margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 }).backgroundColor(CO.primary).borderRadius(12)
.margin({ left: 3, right: 3, top: 6 }).onClick(() => { this.slotIdx = idx })
} else {
Column() {
Text(s.day).fontSize(11).fontColor(CO.ink)
Text(s.date).fontSize(9).fontColor(CO.hint).margin({ top: 3 })
Text(s.times).fontSize(10).fontColor(CO.sub).margin({ top: 3 })
Text('剩 ' + s.left + ' 单').fontSize(8).fontColor(CO.hint).margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 10, bottom: 10 }).backgroundColor(CO.mint).borderRadius(12).margin({ left: 3, right: 3, top: 6 })
.onClick(() => { this.slotIdx = idx })
}
})
}
.width('100%')
ForEach 是 ArkUI 的列表渲染指令,类似于 Vue 的 v-for 或 React 的 map。它接收三个参数:数据源数组、子项渲染函数和(可选的)键值生成器。这里遍历 SLOT_LIST(9 个时段),为每个时段渲染一个选择卡片。
ForEach 的渲染函数接收两个参数:数据项 s 和索引 idx。函数内部使用 if/else 区分选中态和未选中态——当 this.slotIdx === idx 时,卡片使用主色背景和白色文字(高亮选中);否则使用薄荷色背景和墨色文字(默认态)。
每个时段卡片包含四行信息:日期标签(今天/明天/后天)、日期数字(08-27)、时间段(09:00-11:00)和剩余单数。这四行信息垂直排列在 Column 中,通过不同的字体大小和颜色形成信息层级——日期标签最大最醒目,剩余单数最小最次要。
Scroll 组件包裹了整个内容区,当内容超出弹框最大高度时可以滚动。.constraintSize({ maxHeight: '60%' }) 限制弹框内容区最大高度为屏幕的 60%,.scrollBar(BarState.Off) 隐藏滚动条以保持视觉整洁。
8.4 高价榜单行构建器
@Builder hotRowBuilder(m: HotModelT, idx: number) {
Row() {
Text((idx + 1).toString()).fontSize(11).fontWeight(FontWeight.Bold).fontColor(CO.white).width(20).height(20).textAlign(TextAlign.Center)
.backgroundColor(rankColor(idx)).borderRadius(10)
Text(m.icon).fontSize(20).margin({ left: 8 })
Column() {
Text(m.name).fontSize(11).fontColor(CO.ink).fontWeight(FontWeight.Medium)
Text(m.kind + ' · ' + m.brand + ' · 已回收 ' + m.recycled + ' 台').fontSize(8).fontColor(CO.hint).margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 })
Column() {
Text(money(m.max
Text('确认删除这条估价记录?').fontSize(15).fontWeight(FontWeight.Bold).fontColor(CO.ink).margin({ top: 14 })
Text(EST_RECORD_LIST[this.delIdx].icon + ' ' + EST_RECORD_LIST[this.delIdx].device + ' · ' + EST_RECORD_LIST[this.delIdx].grade)
.fontSize(11).fontColor(CO.sub).margin({ top: 8 })
Text('估价 ' + money(EST_RECORD_LIST[this.delIdx].price) + ' · ' + EST_RECORD_LIST[this.delIdx].date).fontSize(10).fontColor(CO.hint).margin({ top: 4 })
Column() {
Text(icon).fontSize(20)
Text(name).fontSize(9).fontColor(CO.sub).margin({ top: 5 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 }).onClick(() => { this.showAddr = true })
}
}

在交互层面,平台实现了八个弹框,覆盖时间选择、设备添加、记录删除、竞价对比、加价提醒、验机报告、城市选择和地址编辑等场景。弹框统一采用"遮罩层 + 内容面板"架构,通过 @State 布尔变量控制显隐,通过条件渲染实现弹框的创建和销毁。三种定位策略(底部抽屉、居中卡片、全屏弹层)适应了不同内容量的弹框需求。
在状态管理层面,@State 装饰器是整个响应式体系的核心。所有交互状态(Tab 切换、步骤推进、筛选选择、弹框显隐)和动画状态(缩放比例、透明度、位移偏移)都通过 @State 管理。状态变量的修改自动触发引用该变量的 UI 组件重绘,ArkUI 的差量更新机制确保只有真正受影响的组件才会重绘,而非整个组件树。这种细粒度的响应式更新是 ArkUI 高性能的基础保障。
在技术深度上,平台展现了若干值得注意的工程实践:使用 slice() 保护原数组不被排序修改、使用参数化构建器实现遮罩层复用、使用 SVG 路径命令手工绘制环形图、使用 Shape + Path 组合实现数据可视化、使用 linearGradient 实现渐变背景、使用 DecorationType.LineThrough 实现删除线价格对比、使用斑马纹背景增强长列表可读性、使用 maxLines(1) 防止长文本撑乱布局。这些细节处理体现了对 ArkUI 能力边界的深入理解和对用户体验的细致打磨。
更多推荐




所有评论(0)