一、技术前言

在鸿蒙生态蓬勃发展的今天,ArkUI 作为 HarmonyOS 官方推荐的声明式 UI 开发框架,正以其简洁高效的开发体验赢得越来越多开发者的青睐。ArkUI 采用声明式编程范式,开发者只需描述 UI 的"是什么",而非"怎么做",框架会自动处理状态变化与视图刷新之间的映射关系。这种开发模式极大地提升了跨端应用的开发效率,使得一套代码可以同时运行在手机、平板、穿戴设备等多种形态的鸿蒙设备上。

ArkUI 的核心设计理念围绕"组件化"与"响应式"两大支柱展开。组件化体现在每一个 UI 元素都是一个可组合、可复用的组件,从最基础的 Text、Column、Row 到复杂的自定义组件,都遵循统一的声明式语法。响应式则通过 @State@Observed@Link 等装饰器实现数据驱动视图,当状态数据发生变化时,框架会自动定位受影响的组件并进行高效的局部刷新,而非全量重绘。这种机制在保证流畅交互体验的同时,也显著降低了开发者的心智负担。

对于跑腿代办这类即时服务类应用而言,ArkUI 的优势尤为明显。一方面,声明式布局能够快速构建复杂的订单卡片、时间轴、人物卡等多样化 UI 结构;另一方面,响应式状态管理可以轻松处理订单状态切换、数据增删改查等动态交互场景。本文将深入剖析一款名为"跑腿侠"的跑腿代办平台应用的源码,从色彩体系设计、数据模型构建,到六大功能模块的差异化布局实现,全方位展现 ArkUI 框架在行业应用开发中的强大能力与实践技巧。

跑腿侠应用以"跑腿橙"(#FF7A00)与"活力红"(#FF3B30)为主色调,搭配浅色背景,营造出热情、高效、值得信赖的品牌形象。应用包含首页、订单、跑腿师、评价、钱包、我的六大功能模块,每个模块都采用了截然不同的布局风格——宫格入口、状态时间轴、人物大卡、评论卡片、渐变数字卡、清单行——充分展示了 ArkUI 在多样化布局方面的灵活性与表现力。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、整体架构流程图

下面通过 Mermaid 流程图展示该跑腿侠应用的整体架构层次,从底层数据到上层视图,清晰呈现各模块之间的协作关系。

弹窗系统

视图组件层

状态管理层

数据模型层

数据层

色彩体系 COLORS

Tab元数据 TAB_LIST

辅助常量数组

工具函数 errandColor/ratingColor

ServiceItem 服务项

ErrandItem 订单项

RunnerItem 跑腿师

ReviewItem 评价项

WalletItem 钱包项

ProfileItem 个人资料

@State currentTab

@State 各列表数据

@State 弹窗控制变量

@State breath 呼吸动画

headerHome 头部区域

tabHome 首页宫格

tabOrder 订单时间轴

tabRunner 跑腿师大卡

tabReview 评价卡片

tabWallet 钱包渐变卡

tabMine 我的清单

chartCard 图表卡片

tabBar 底部导航

panelAdd 发布订单

panelEdit 编辑订单

panelDel 删除确认

架构解读

从架构图中可以清晰地看到,该源码采用了典型的分层设计思想,自上而下依次为数据层、数据模型层、状态管理层、视图组件层与弹窗系统。各层职责明确,数据单向流动,确保了代码的可维护性与可扩展性。

数据层位于架构最底层,提供全局共享的基础配置,包括色彩体系、Tab 元数据、辅助常量数组以及工具函数。这些数据贯穿整个应用生命周期,为所有视图组件提供统一的视觉规范与计算逻辑。

数据模型层是业务数据的载体,通过六个 @Observed 装饰的类分别封装服务、订单、跑腿师、评价、钱包、个人资料六大业务实体。每个模型类都包含构造函数,便于快速实例化并填充初始数据。

状态管理层是连接数据与视图的桥梁,通过 @State 装饰器将各类数据接入响应式系统。当状态数据发生变化时,框架自动通知相关视图进行刷新。此外,弹窗控制变量与呼吸动画状态也由该层统一管理。

视图组件层是用户直接感知的界面层,由头部区域、六个 Tab 页面、图表卡片与底部导航栏组成。每个 Tab 页面都拥有独特的布局结构,通过 @Builder 装饰器封装为独立的构建方法,实现了代码的模块化组织。

弹窗系统提供了发布订单、编辑订单、删除确认三种模态弹窗,均采用半透明遮罩加居中卡片的经典弹窗模式。弹窗与订单页面通过状态变量进行联动,实现了完整的增删改查交互闭环。

三、色彩体系设计

色彩是一款应用视觉风格的灵魂。跑腿侠应用在色彩体系的构建上可谓匠心独运,通过精心调配的暖色调色板,传递出跑腿服务行业热情、活力、专业的品牌气质。

3.1 色彩接口定义

源码中首先定义了 ColorPalette 接口,作为色彩体系的类型契约。该接口规范了所有可用颜色字段的名称与类型,为后续的色彩常量定义提供了类型安全保障。

interface ColorPalette {
  bg: string; card: string; chip: string;
  title: string; sub: string; text3: string;
  orange: string; orangeD: string;
  red: string; redD: string;
  green: string; blue: string;
  line: string; tabOn: string; mask: string;
}

这段接口定义将色彩划分为几个清晰的组别:背景类(bg、card、chip)、文字类(title、sub、text3)、主题色类(orange、orangeD、red、redD)、功能色类(green、blue)以及辅助类(line、tabOn、mask)。这种分组方式使得色彩的用途一目了然,开发者在使用时能够快速定位到所需的颜色变量。

3.2 色彩常量配置

在接口定义之后,源码通过 COLORS 常量赋予了每个字段具体的颜色值,形成了完整的色彩方案。

const COLORS: ColorPalette = {
  bg: '#FFF6F0', card: '#FFFFFF', chip: '#FFE0CC',
  title: '#2D1810', sub: '#8C6B5A', text3: '#C4A88F',
  orange: '#FF7A00', orangeD: '#E66E00',
  red: '#FF3B30', redD: '#E03228',
  green: '#34C759', blue: '#007AFF',
  line: '#F0DCC8', tabOn: '#FF7A00',
  mask: 'rgba(0,0,0,0.5)',
};

让我们逐一解析这些色彩的设计考量:

背景色系bg 采用暖米色 #FFF6F0,相较于纯白更具温度感,为整个应用奠定温暖舒适的基调;card 使用纯白 #FFFFFF,在米色背景上形成清晰的卡片层次;chip 选用浅橙色 #FFE0CC,作为胶囊标签、图标容器等辅助元素的底色,与主题橙形成呼应关系。

文字色系:三级文字色阶由深至浅依次为 title(深棕 #2D1810)、sub(中棕 #8C6B5A)、text3(浅棕 #C4A88F)。相较于传统的灰阶文字,棕色调文字与暖色系背景更加协调统一,避免了冷灰色在暖色环境中的突兀感。

主题色系:跑腿橙 #FF7A00 是整个应用的主色调,饱和度适中、明度较高,既醒目又不刺眼;其深色版本 orangeD#E66E00)用于按钮按下状态或需要加重的场景。活力红 #FF3B30 作为辅助主题色,用于强调重要信息与危险操作;其深色版本 redD#E03228)同样提供状态变化的视觉反馈。橙红双色的搭配,既区分了功能层级,又保持了视觉上的热情与活力。

功能色系:绿色 #34C759 用于"已完成"状态、好评率等积极正面的语义;蓝色 #007AFF 用于"进行中"状态与保存操作,传递冷静可靠的感觉。这两种功能色与 iOS 系统色接近,用户认知成本低。

辅助色系:分隔线 line 采用浅棕色调 #F0DCC8,与背景色接近但仍可分辨;底部 Tab 选中色 tabOn 与主题橙一致,保持品牌识别;遮罩层 mask 使用半透明黑色 rgba(0,0,0,0.5),为弹窗提供合适的背景暗化效果。

3.3 色彩体系的应用价值

这套精心设计的色彩体系具有多重价值。首先,它实现了视觉风格的高度统一,无论用户切换到哪个 Tab 页面,都能感受到一致的品牌调性。其次,色彩与语义的对应关系清晰明确——橙色代表主要操作与强调,红色代表危险与警示,绿色代表成功与完成,蓝色代表进行与中性——用户可以通过颜色快速理解界面元素的含义。最后,集中式的色彩管理使得后续的主题定制与风格调整变得极为便捷,只需修改 COLORS 常量即可全局生效。

四、Tab元数据与辅助数据

除了色彩体系之外,源码顶部还定义了一系列 Tab 元数据与辅助常量数组。这些数据虽然不直接构成业务模型,却为界面的构建提供了重要的支撑。

4.1 Tab元数据

底部导航栏的六个 Tab 由 TabMeta 接口与 TAB_LIST 常量共同定义。

interface TabMeta { name: string; icon: string; }

const TAB_LIST: TabMeta[] = [
  { name: '首页', icon: '🏠' },
  { name: '订单', icon: '📋' },
  { name: '跑腿师', icon: '🏃' },
  { name: '评价', icon: '⭐' },
  { name: '钱包', icon: '💰' },
  { name: '我的', icon: '👤' },
];

TabMeta 接口定义了每个 Tab 的两个核心属性:name 为显示名称,icon 为图标字符。这里采用了 Emoji 字符作为图标,无需额外引入图标字体或图片资源,开发成本极低且跨平台兼容性好。TAB_LIST 数组按照底部导航从左到右的顺序排列了六个 Tab,与后面的 currentTab 索引一一对应。

4.2 图表辅助数据

图表卡片所需的数据由三组常量数组共同维护:

const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const ERRAND_VAL: number[] = [120, 168, 215, 186, 258, 312];

MONTH_IDX 提供了 0 到 5 的索引序列,用于 ForEach 循环遍历。MONTH_NAME 存储了六个月份的显示名称,作为柱状图的横坐标标签。ERRAND_VAL 则记录了对应月份的跑腿订单量,决定了柱状图的柱高。三组数组通过相同的索引位置建立关联,共同驱动图表的渲染。

4.3 服务宫格行索引

首页的服务宫格采用四行两列的布局,源码中为每一行都定义了独立的索引数组:

const SERVICE_ROW0: number[] = [0, 1];
const SERVICE_ROW1: number[] = [2, 3];
const SERVICE_ROW2: number[] = [4, 5];
const SERVICE_ROW3: number[] = [6, 7];

这种写法虽然在数据量较小时显得有些"笨拙",但在 ArkUI 的声明式布局中却有其合理性。由于 ArkUI 的 RowColumn 是显式的容器组件,每一行需要单独包裹在 Row 中,因此预先定义每行的索引范围可以让布局代码更加清晰。每行两个服务项的索引对一目了然,便于维护和调整。

4.4 我的菜单数据

"我的"页面的功能菜单由二维数组定义:

const MINE_MENU: string[][] = [
  ['📝', '我的订单'],
  ['📍', '收货地址'],
  ['💳', '支付方式'],
  ['🎫', '优惠券'],
  ['🔔', '消息通知'],
  ['⚙️', '设置'],
  ['❓', '帮助中心'],
  ['ℹ️', '关于我们'],
];

每个菜单项由图标 Emoji 和文字名称组成,数组的第一个元素是图标,第二个是名称。二维数组的结构简洁直观,ForEach 遍历时直接通过索引访问即可。八项菜单覆盖了个人中心的常见功能,从订单管理到系统设置,逻辑清晰、层级分明。

4.5 热门标签

头部区域的热门服务标签由 HOT_TAGS 数组维护:

const HOT_TAGS: string[] = ['同城急送', '代买代办', '上门服务', '24小时'];

这四个标签精准地概括了跑腿服务的核心卖点——同城范围内的即时配送、代办各类事务、上门服务的便利性,以及全天候的服务时段。标签以橙色胶囊形式呈现,既是服务特色的展示,也可能作为快速筛选的入口。

五、工具函数

在数据定义之后,源码提供了两个实用的工具函数,用于根据业务状态动态计算颜色值。这两个函数虽然代码量不大,却承担着重要的视觉逻辑职责。

5.1 errandColor 订单状态颜色映射

function errandColor(s: string): string {
  if (s === '待接单') return COLORS.orange;
  if (s === '进行中') return COLORS.blue;
  if (s === '已完成') return COLORS.green;
  if (s === '已取消') return COLORS.red;
  return COLORS.text3;
}

errandColor 函数接收一个订单状态字符串,返回对应的颜色值。四种核心状态分别映射为:待接单为橙色(等待处理、需要关注)、进行中为蓝色(正在运行、稳定可靠)、已完成为绿色(成功结束、积极正面)、已取消为红色(失败终止、警示注意)。如果传入的状态不在四种之内,则返回浅棕色作为默认兜底。

这个函数在订单时间轴页面和编辑弹窗中被广泛使用,为状态圆点、状态标签、状态文字等元素提供统一的着色逻辑。将颜色映射逻辑抽离为独立函数,而非在每个使用处硬编码判断,既保证了视觉一致性,也便于后续维护——如需新增状态类型,只需在函数中添加一条判断即可。

5.2 ratingColor 评分颜色分级

function ratingColor(r: number): string {
  if (r >= 4.8) return COLORS.green;
  if (r >= 4.5) return COLORS.orange;
  return COLORS.text3;
}

ratingColor 函数根据评分数值返回不同等级的颜色,实现了评分的可视化分级。评分在 4.8 分及以上时显示为绿色,代表"优秀"级别;4.5 分到 4.7 分之间显示为橙色,代表"良好"级别;4.5 分以下则显示为浅棕色,代表"一般"级别。

这种三级配色方案在跑腿师列表页面中发挥着重要作用,用户只需扫一眼评分数字的颜色,就能快速判断跑腿师的服务质量等级。颜色的语义与用户的心理预期高度吻合——绿色象征放心选择,橙色表示尚可考虑,浅棕色则暗示需要谨慎。

工具函数的设计体现了"关注点分离"的架构原则,将视觉计算逻辑从布局代码中抽离出来,使得布局代码更加专注于结构描述,而颜色计算逻辑也获得了独立复用的能力。

六、数据模型层

数据模型是整个应用的业务核心。该源码通过六个 @Observed 装饰的类,构建了完整的业务数据模型体系。@Observed 装饰器是 ArkUI 响应式系统的重要组成部分,它标记的类在实例化后,其属性变化可以被框架自动追踪,从而触发关联视图的刷新。

6.1 ServiceItem 服务项模型

@Observed export class ServiceItem {
  icon: string; name: string; desc: string; price: number;
  constructor(icon: string, name: string, desc: string, price: number) {
    this.icon = icon; this.name = name; this.desc = desc; this.price = price;
  }
}

ServiceItem 是服务分类的数据模型,包含四个属性:icon 为服务图标(Emoji 字符)、name 为服务名称、desc 为服务描述、price 为起步价格。构造函数接收四个参数并依次赋值,支持快速实例化。

源码中预置了 8 条服务数据,涵盖取快递、送文件、买药品、代排队、搬家搬货、鲜花配送、宠物寄养、上门保洁等常见跑腿场景,价格从 5 元到 35 元不等,基本覆盖了同城跑腿服务的主要品类。

6.2 ErrandItem 订单项模型

@Observed export class ErrandItem {
  title: string; runner: string; status: string;
  fee: number; time: string; from: string; to: string;
  constructor(title: string, runner: string, status: string, fee: number, time: string, from: string, to: string) {
    this.title = title; this.runner = runner; this.status = status;
    this.fee = fee; this.time = time; this.from = from; this.to = to;
  }
}

ErrandItem 是订单的数据模型,包含七个属性,信息维度丰富:title 为订单标题(含服务类型与简要说明)、runner 为接单跑腿师姓名、status 为订单状态、fee 为服务费用、time 为下单时间、from 为取件地址、to 为送达地址。

该模型是六个模型中属性最多的一个,这也符合订单作为核心业务实体的地位——订单需要承载从服务内容、执行人员、状态流转,到费用、时间、地点等全方位的信息。预置的 6 条订单数据涵盖了待接单、进行中、已完成、已取消四种状态,每种状态各有代表性的场景,为订单页面的时间轴展示提供了丰富的测试数据。

6.3 RunnerItem 跑腿师模型

@Observed export class RunnerItem {
  name: string; avatar: string; rating: number;
  orders: number; tags: string[]; fee: number; status: string;
  constructor(name: string, avatar: string, rating: number, orders: number, tags: string[], fee: number, status: string) {
    this.name = name; this.avatar = avatar; this.rating = rating;
    this.orders = orders; this.tags = tags; this.fee = fee; this.status = status;
  }
}

RunnerItem 是跑腿师的数据模型,同样包含七个属性:name 为姓名、avatar 为头像(Emoji 表示)、rating 为评分、orders 为累计接单量、tags 为服务标签数组、fee 为起步价、status 为当前接单状态。

跑腿师模型的设计非常注重"选择维度"的构建——用户选择跑腿师时,会综合考虑服务评分、接单数量、服务特色标签、价格以及当前是否在线等因素。tags 字段采用字符串数组的形式,可以灵活添加任意数量的标签,如"速度快"“金牌跑腿”"夜间服务"等,为用户提供丰富的决策信息。预置的 6 位跑腿师各具特色,有速度快的张师傅、细心的赵师傅、力气大的王师傅、金牌老司机陈师傅等,人物形象丰满。

6.4 ReviewItem 评价项模型

@Observed export class ReviewItem {
  author: string; avatar: string; target: string;
  content: string; rating: number; time: string;
  constructor(author: string, avatar: string, target: string, content: string, rating: number, time: string) {
    this.author = author; this.avatar = avatar; this.target = target;
    this.content = content; this.rating = rating; this.time = time;
  }
}

ReviewItem 是用户评价的数据模型,包含六个属性:author 为评价人姓名、avatar 为评价人头像、target 为被评价的跑腿师、content 为评价内容、rating 为星级评分、time 为评价时间。

评价模型巧妙地连接了用户与跑腿师两个实体,通过 authortarget 字段明确了评价的发出方与接收方。rating 使用数字表示星级(1 到 5 星),在界面上通过循环渲染五角星符号来展示。预置的 6 条评价数据均为 4 星或 5 星好评,内容具体详实,涉及速度、态度、细心程度等多个评价维度,具有较强的真实感与说服力。

6.5 WalletItem 钱包项模型

@Observed export class WalletItem {
  balance: number; frozen: number; total: number; withdraw: number;
  constructor(balance: number, frozen: number, total: number, withdraw: number) {
    this.balance = balance; this.frozen = frozen; this.total = total; this.withdraw = withdraw;
  }
}

WalletItem 是钱包账户的数据模型,包含四个金额属性:balance 为可用余额、frozen 为冻结金额、total 为累计收入、withdraw 为累计提现。这四个指标全面反映了钱包账户的财务状况——用户既关心当前可以使用多少钱,也关心有多少资金正在清算中,以及历史总收入和已提现金额。

钱包模型虽然只有四个数字属性,但每个数字都需要在界面上以醒目的方式呈现,尤其是余额数字需要使用大字号渐变卡片突出显示。预置的三条钱包数据余额从 800 多元到 2300 多元不等,可以展示不同余额量级下的卡片效果。

6.6 ProfileItem 个人资料模型

@Observed export class ProfileItem {
  name: string; phone: string; level: string; avatar: string;
  constructor(name: string, phone: string, level: string, avatar: string) {
    this.name = name; this.phone = phone; this.level = level; this.avatar = avatar;
  }
}

ProfileItem 是个人资料的数据模型,包含四个属性:name 为用户姓名、phone 为手机号码(部分脱敏显示)、level 为会员等级、avatar 为头像。

个人资料模型虽然属性不多,但在"我的"页面中扮演着"门面"的角色——用户头像、姓名、会员等级等信息是个人中心最醒目的元素。手机号采用中间四位打码的方式展示(如 186****5257),兼顾了显示需求与隐私保护。预置的三条资料数据分别对应钻石会员、金牌会员、银牌会员三个等级,展示了不同会员层级的标识效果。

六个数据模型各司其职,共同构成了跑腿侠应用的业务数据骨架。每个模型都遵循相同的设计模式:@Observed 装饰器 + 属性声明 + 构造函数,结构清晰、风格统一,体现了良好的编码规范。

七、组件主体结构

在完成了所有数据层的定义之后,源码进入了视图层的核心——Page809 组件的构建。这是一个使用 @Entry@Component 双重装饰的页面级组件,是整个应用的入口与主体。

7.1 组件装饰器与状态声明

@Entry
@Component
struct Page809 {
  @State currentTab: number = 0;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = -1;
  @State delIdx: number = -1;
  @State breath: boolean = false;
  timer: number = -1;
  @State serviceList: ServiceItem[] = SERVICE_LIST;
  @State errandList: ErrandItem[] = ERRAND_LIST;
  @State runnerList: RunnerItem[] = RUNNER_LIST;
  @State reviewList: ReviewItem[] = REVIEW_LIST;
  @State walletList: WalletItem[] = WALLET_LIST;
  @State profileList: ProfileItem[] = PROFILE_LIST;

@Entry 装饰器标记该组件为页面入口,应用启动时会首先渲染此组件。@Component 装饰器则将 Page809 结构体标记为 ArkUI 组件,使其具备声明式构建 UI 的能力。

组件内部的状态变量可以分为几组:

导航状态currentTab 记录当前选中的 Tab 索引,初始值为 0 即首页,是控制六个 Tab 页面切换的核心变量。

弹窗状态addModaleditModaldelModal 三个布尔变量分别控制发布订单弹窗、编辑订单弹窗、删除确认弹窗的显示与隐藏。editIdxdelIdx 则记录当前正在编辑或删除的订单索引,为弹窗提供数据上下文。

动画状态breath 是一个布尔开关,配合定时器实现呼吸动画效果。timer 存储定时器的句柄,用于在页面销毁时清理定时器资源。

列表数据:六个 @State 装饰的数组变量,分别对应六个业务模型的列表数据。它们以全局常量作为初始值,保证了页面首次加载时即可展示完整内容。将列表数据声明为 @State 的意义在于,后续的增删改操作(如新增订单、删除订单)能够实时反映到界面上。

7.2 生命周期方法

  aboutToAppear() {
    this.timer = setInterval(() => { this.breath = !this.breath; }, 1000);
  }

  aboutToDisappear() { clearInterval(this.timer); }

ArkUI 提供了丰富的生命周期钩子,aboutToAppear 在组件即将显示时调用,aboutToDisappear 在组件即将销毁时调用。这里利用这两个钩子实现了呼吸动画的启停控制。

页面出现时,通过 setInterval 设置了一个 1000 毫秒(即 1 秒)的定时器,每次触发就将 breath 布尔值取反。这个布尔值的变化会驱动所有使用了 breath 状态的界面元素产生缩放等动画效果,形成呼吸般的节律感。页面销毁时则通过 clearInterval 清理定时器,防止内存泄漏。

7.3 主构建函数

  build() {
    Column() {
      this.headerHome()
      Divider().strokeWidth(1).color(COLORS.line)
      Scroll() {
        Column() {
          if (this.currentTab === 0) {
            this.tabHome()
          } else if (this.currentTab === 1) {
            this.tabOrder()
          } else if (this.currentTab === 2) {
            this.tabRunner()
          } else if (this.currentTab === 3) {
            this.tabReview()
          } else if (this.currentTab === 4) {
            this.tabWallet()
          } else if (this.currentTab === 5) {
            this.tabMine()
          }
          this.chartCard()
        }
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)

      this.tabBar()

      if (this.addModal) { this.panelAdd(() => { this.addModal = false }) }
      if (this.editModal) { this.panelEdit(() => { this.editModal = false }) }
      if (this.delModal) { this.panelDel(() => { this.delModal = false }) }
    }
    .backgroundColor(COLORS.bg)
    .height('100%')
  }

build() 方法是组件的核心入口,返回整个页面的 UI 结构。整体采用 Column 纵向布局,自上而下分为四个区域:

  1. 头部区域:通过 this.headerHome() 调用头部构建方法,展示应用 Logo、搜索栏、热门标签与热门跑腿师头像。紧接其下的是一条 Divider 分隔线,将头部与内容区在视觉上分开。

  2. 内容滚动区Scroll 组件包裹了主要内容,设置了 scrollBar(BarState.Off) 隐藏滚动条,提供更清爽的视觉体验。layoutWeight(1) 让滚动区占据剩余所有高度。滚动区内根据 currentTab 的值通过 if-else 条件渲染对应的 Tab 页面,实现了 Tab 切换的核心逻辑。无论哪个 Tab 被选中,底部都会显示图表卡片 chartCard(),为用户提供数据洞察。

  3. 底部导航栏this.tabBar() 渲染六个 Tab 的底部导航按钮,用户点击即可切换当前 Tab。

  4. 弹窗层:三个 if 条件分别控制三个弹窗的显示。当对应的弹窗状态变量为 true 时,弹窗组件被渲染到界面上,覆盖在其他内容之上。每个弹窗接收一个关闭回调函数,弹窗内部点击取消或操作完成时调用该回调,将状态变量设为 false,弹窗随之消失。

整个页面的背景色设置为 COLORS.bg(暖米色),高度设为 100% 占满整个屏幕。这种结构清晰的主构建函数,将复杂的页面拆解为多个独立的 @Builder 方法,每个方法只负责一个功能区域的构建,代码可读性与可维护性都非常出色。

八、头部区域详解

头部区域是用户打开应用后最先看到的部分,承担着品牌展示、搜索入口、服务引导等多重功能。headerHome 方法构建了一个信息密度适中、层次分明的头部区域。

8.1 Logo 与应用标题行

  @Builder
  headerHome() {
    Column() {
      Row() {
        Stack() { Text('🏃').fontSize(28) }
          .width(50).height(50).borderRadius(25)
          .backgroundColor(COLORS.chip).alignContent(Alignment.Center)
          .scale(this.breath ? { x: 1.08, y: 1.08 } : { x: 1, y: 1 })
          .animation({ duration: 1000, curve: Curve.EaseInOut })

        Column() {
          Text('跑腿侠').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
          Text('您身边的跑腿专家').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 })
        }.margin({ left: 10 }).alignItems(HorizontalAlign.Start)

        Blank()

        Stack() { Text('🔔').fontSize(20) }
          .width(40).height(40).borderRadius(20).backgroundColor(COLORS.chip)
          .alignContent(Alignment.Center)
      }
      .width('100%').padding({ left: 16, right: 16, top: 16, bottom: 10 })

第一行是品牌与通知区域,采用横向布局。左侧是一个圆形的 Logo 图标容器,使用跑步小人 Emoji 作为应用图标,放置在 50x50 的浅橙色圆形背景上。这个 Logo 图标还绑定了呼吸动画——当 breathtrue 时放大到 1.08 倍,为 false 时恢复原尺寸,配合 1 秒时长的缓动动画曲线,产生持续的呼吸缩放效果,为静态界面注入了生命力。

Logo 右侧是应用名称与标语的纵向排列。"跑腿侠"三个大字使用 18 号加粗深棕色字体,醒目而有力;下方的"您身边的跑腿专家"使用 11 号中棕色字体,作为品牌 Slogan 传达服务定位。

中间的 Blank() 组件将左右两侧撑开,实现两端对齐的布局效果。右侧是一个铃铛图标按钮,代表消息通知入口,同样采用圆形胶囊背景,与左侧 Logo 形成视觉呼应。

8.2 搜索栏

      Row() {
        Text('🔍').fontSize(16)
        Text('搜索服务 / 跑腿师 / 订单')
          .fontSize(13).fontColor(COLORS.text3).margin({ left: 8 }).layoutWeight(1)
        Text('📷').fontSize(18)
      }
      .width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })
      .margin({ left: 16, right: 16 })
      .backgroundColor(COLORS.bg).borderRadius(22)

搜索栏是头部区域的核心功能入口。整个搜索框采用圆角矩形设计,圆角半径 22px,背景色与页面背景相同(暖米色),在白色的头部卡片上形成凹陷的视觉效果。

搜索栏内部从左到右依次排列:放大镜图标(搜索语义的视觉提示)、占位文字"搜索服务 / 跑腿师 / 订单"(浅棕色,提示用户可以搜索的内容范围)、相机图标(暗示支持拍照或图片搜索)。占位文字通过 layoutWeight(1) 占据剩余空间,实现两端对齐。

8.3 热门标签

      Row() {
        ForEach(HOT_TAGS, (tag: string) => {
          Text(tag).fontSize(11).fontColor(COLORS.orange)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
            .backgroundColor(COLORS.chip).borderRadius(12).margin({ right: 8 })
        })
      }
      .width('100%').padding({ left: 16, right: 16, top: 8, bottom: 10 })

热门标签行使用 ForEach 遍历 HOT_TAGS 数组,将四个热门服务关键词渲染为橙色胶囊标签。标签采用橙字浅橙底的配色方案,既醒目又不刺眼。胶囊式的圆角设计(borderRadius: 12)给人以柔和友好的感觉。

8.4 热门跑腿师预览

      Row() {
        Column() {
          Text('热门跑腿师').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
          Text('6位金牌骑手在线').fontSize(10).fontColor(COLORS.sub).margin({ top: 2 })
        }.alignItems(HorizontalAlign.Start)

        Blank()

        Row() {
          ForEach([0, 1, 2], (idx: number) => {
            Stack() { Text(this.runnerList[idx].avatar).fontSize(20) }
              .width(32).height(32).borderRadius(16)
              .backgroundColor(COLORS.chip).alignContent(Alignment.Center)
              .margin({ left: idx > 0 ? -8 : 0 })
              .border({ width: 2, color: COLORS.card })
          })
        }

        Text('查看全部 ›').fontSize(11).fontColor(COLORS.orange).margin({ left: 8 })
      }
      .width('100%').padding({ left: 16, right: 16, top: 4, bottom: 14 })
      .alignItems(VerticalAlign.Center)
    }
    .backgroundColor(COLORS.card)
  }

头部最下方是热门跑腿师预览区域。左侧是标题与副标题,告知用户当前有 6 位金牌骑手在线接单。中间是三位跑腿师的头像,采用了层叠排列的设计——后一个头像向左偏移 8px(通过负 margin 实现),并且每个头像都带有 2px 的白色边框,确保层叠时边缘清晰可辨。这种层叠头像的设计既节省了横向空间,又营造出"人气旺盛"的视觉效果。

右侧是"查看全部 ›"的橙色链接文字,引导用户点击进入跑腿师列表页面。整个头部区域使用白色卡片背景,与米色页面背景形成对比,视觉层次分明。

九、首页——宫格入口布局

首页是用户进入应用后的默认页面,采用宫格入口的布局形式,将各类跑腿服务以直观的图标+文字+价格的方式呈现,方便用户快速找到所需服务。

9.1 服务标题栏

  @Builder
  tabHome() {
    Column() {
      Row() {
        Text('🛵 全部服务').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('更多 ›').fontSize(13).fontColor(COLORS.orange)
      }.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })

标题栏采用经典的左右布局,左侧是带电动车图标的"全部服务"标题,右侧是橙色的"更多 ›"链接。这种标题栏设计在六个 Tab 页面中保持了一致的模式,形成了统一的视觉语言。

9.2 四行两列服务宫格

宫格区域是首页的核心内容,包含 8 个服务入口,排列为 4 行 2 列的网格。源码中通过四个独立的 Row 组件构建每一行,每行内部通过 ForEach 遍历对应的行索引数组。

      Column() {
        Row() {
          ForEach(SERVICE_ROW0, (idx: number) => {
            Column() {
              Stack() { Text(this.serviceList[idx].icon).fontSize(36) }
                .width(56).height(56).borderRadius(28).backgroundColor(COLORS.chip)
                .alignContent(Alignment.Center)
              Text(this.serviceList[idx].name).fontSize(14).fontWeight(FontWeight.Medium)
                .fontColor(COLORS.title).margin({ top: 8 })
              Text(this.serviceList[idx].desc).fontSize(10).fontColor(COLORS.sub)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 2 })
              Row() {
                Text('¥' + this.serviceList[idx].price.toString()).fontSize(12)
                  .fontWeight(FontWeight.Bold).fontColor(COLORS.orange)
                Text(' 起').fontSize(10).fontColor(COLORS.text3)
              }.margin({ top: 4 })
            }
            .layoutWeight(1).padding({ top: 16, bottom: 16 })
            .alignItems(HorizontalAlign.Center)
          })
        }.width('100%')

        Divider().strokeWidth(1).color(COLORS.line)
        // ... SERVICE_ROW1, SERVICE_ROW2, SERVICE_ROW3 结构相同
      }
      .width('100%').margin({ left: 16, right: 16 })
      .backgroundColor(COLORS.card).borderRadius(16)

每个服务格都采用纵向布局,从上到下依次为:图标、服务名称、服务描述、价格。图标使用 56x56 的圆形浅橙色背景,Emoji 图标大小为 36px,视觉重心突出。服务名称使用 14 号中等粗细的深棕色字体,是格子中最主要的文字信息。服务描述使用 10 号浅棕色小字,设置了单行省略(maxLines(1) + textOverflow),防止描述过长时破坏布局。价格行采用"¥价格 + 起"的组合,价格数字用橙色加粗强调,"起"字用浅棕色弱化处理,清晰传达起步价的含义。

每行之间通过 Divider 分隔线隔开,增强了行与行之间的视觉区分。整个宫格区域使用白色卡片背景,圆角 16px,左右各 16px 的外边距,与头部区域的卡片风格保持一致。

layoutWeight(1) 是实现两列等分的关键——每个服务格设置相同的权重,它们会平均分配所在 Row 的可用宽度,确保两列宽度完全相等。

9.3 限时优惠横幅

      Row() {
        Text('🔥 限时优惠').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('全部优惠 ›').fontSize(13).fontColor(COLORS.red)
      }.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })

      Row() {
        Column() {
          Text('新用户首单').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.card)
          Text('立减10元').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.card)
            .margin({ top: 4 })
          Text('限时领取').fontSize(10).fontColor(COLORS.chip).margin({ top: 4 })
        }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding(14)

        Stack() { Text('🎁').fontSize(40) }
          .width(60).height(60).alignContent(Alignment.Center)
      }
      .width('100%').margin({ left: 16, right: 16, bottom: 16 })
      .backgroundColor(COLORS.orange).borderRadius(16)
    }.width('100%')
  }

宫格下方是限时优惠区域,标题栏的"全部优惠"使用红色而非橙色,暗示优惠活动与紧迫感的关联。优惠横幅是一个醒目的橙色卡片,左侧是"新用户首单立减10元"的营销文案,右侧是一个礼物图标。白色文字在橙色背景上具有极高的对比度,信息传达非常直接。这个横幅既是新用户转化的重要入口,也丰富了首页的视觉层次。

十、订单——状态时间轴布局

订单页面采用时间轴的布局形式,将每条订单以时间线节点的方式纵向排列,左侧的状态圆点与连接线构成了完整的时间轴视觉效果。

10.1 标题与筛选栏

  @Builder
  tabOrder() {
    Column() {
      Row() {
        Text('📋 我的订单').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('+ 发布订单').fontSize(13).fontColor(COLORS.orange)
          .onClick(() => { this.addModal = true; })
      }.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })

订单页面的标题栏右侧是"+ 发布订单"按钮,点击后将 addModal 设为 true,弹出发布订单的弹窗。这是订单页面新增数据的入口。

      Row() {
        ForEach(['全部', '待接单', '进行中', '已完成'], (s: string, idx: number) => {
          Text(s).fontSize(12).fontColor(idx === 0 ? COLORS.card : COLORS.sub)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(idx === 0 ? COLORS.orange : COLORS.chip)
            .borderRadius(14).margin({ right: 8 })
        })
      }.width('100%').padding({ left: 16, right: 16, bottom: 12 })

筛选栏提供了"全部、待接单、进行中、已完成"四个状态筛选标签。第一个标签(全部)默认为选中状态,使用橙色填充+白色文字的高亮样式;其余标签为浅橙底+棕色文字的普通样式。这种胶囊式筛选器在移动端应用中非常常见,操作便捷、状态清晰。

10.2 时间轴订单列表

订单列表是订单页面的核心,通过 ForEach 遍历 errandList 数组渲染每条订单。每条订单采用左侧时间轴 + 右侧内容卡片的布局结构。

      ForEach(this.errandList, (item: ErrandItem, idx: number) => {
        Row() {
          Column() {
            Column().width(2).height(idx === 0 ? 0 : 20).backgroundColor(COLORS.line)
            Stack() { Text('●').fontSize(14).fontColor(errandColor(item.status)) }
              .width(24).height(24).borderRadius(12).backgroundColor(COLORS.card)
              .alignContent(Alignment.Center)
              .border({ width: 2, color: errandColor(item.status) })
            Column().width(2).height(idx === this.errandList.length - 1 ? 0 : 20)
              .backgroundColor(COLORS.line)
          }.alignItems(HorizontalAlign.Center)

时间轴部分由三段组成:上方连接线、中间状态圆点、下方连接线。上方连接线的高度在第一条记录时为 0(起始位置不需要上线),其余为 20px;下方连接线的高度在最后一条记录时为 0(结束位置不需要下线),其余为 20px。这种处理确保了时间轴在首尾处的自然收束。

状态圆点是时间轴的视觉焦点:一个 24x24 的白色圆形,内部有一个根据订单状态着色的实心圆点(●),圆形边框同样使用对应状态的颜色,宽度为 2px。颜色通过 errandColor 函数动态计算——待接单为橙色、进行中为蓝色、已完成为绿色、已取消为红色。这种颜色编码让用户一眼就能识别订单状态。

10.3 订单内容卡片

时间轴右侧是订单的详细内容,信息层次丰富,从上到下依次为标题行、跑腿师与时间行、地址与金额行、操作按钮行。

          Column() {
            Row() {
              Text(item.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
              Text(item.status).fontSize(10).fontColor(COLORS.card)
                .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
                .backgroundColor(errandColor(item.status))
            }.width('100%')

标题行左侧是订单标题,单行省略;右侧是状态标签,彩色背景+白色文字,与时间轴圆点颜色呼应,形成双重状态标识。

            Row() {
              Text('🏃 ' + item.runner).fontSize(12).fontColor(COLORS.sub)
              Text(' · ').fontSize(12).fontColor(COLORS.text3)
              Text(item.time).fontSize(12).fontColor(COLORS.sub)
            }.margin({ top: 6 })

第二行显示接单跑腿师姓名和下单时间,中间用点号分隔。使用次级文字颜色,作为辅助信息呈现。

            Row() {
              Column() {
                Row() {
                  Stack() { Text('📍').fontSize(12) }
                    .width(20).height(20).alignContent(Alignment.Center)
                  Text(item.from).fontSize(11).fontColor(COLORS.sub)
                    .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
                }.width('100%')
                Row() {
                  Stack() { Text('🏁').fontSize(12) }
                    .width(20).height(20).alignContent(Alignment.Center)
                  Text(item.to).fontSize(11).fontColor(COLORS.sub)
                    .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
                }.width('100%').margin({ top: 4 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)

              Column() {
                Text('¥' + item.fee.toString()).fontSize(16).fontWeight(FontWeight.Bold)
                  .fontColor(errandColor(item.status))
                Text(item.status === '待接单' ? '立即接单' : '查看详情')
                  .fontSize(10).fontColor(COLORS.orange).margin({ top: 2 })
              }.alignItems(HorizontalAlign.End)
            }.width('100%').margin({ top: 8 })

地址与金额行是订单卡片的信息核心。左侧是取件地址(📍图标+起点)和送达地址(🏁图标+终点)的纵向排列,清晰展示跑腿路径的起止点。右侧是费用金额和操作提示,金额使用 16 号加粗字体,颜色同样与状态关联——待接单时橙色最为醒目,提示用户尽快处理;已完成时绿色则传达一种安心感。金额下方根据状态显示不同的引导文字,待接单显示"立即接单"(强调行动),其他状态显示"查看详情"(强调了解)。

            Row() {
              Text('✏️ 编辑').fontSize(11).fontColor(COLORS.sub)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .backgroundColor(COLORS.bg).borderRadius(10)
                .onClick(() => { this.editIdx = idx; this.editModal = true; })
              Text('🗑️ 删除').fontSize(11).fontColor(COLORS.red)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .backgroundColor(COLORS.bg).borderRadius(10).margin({ left: 8 })
                .onClick(() => { this.delIdx = idx; this.delModal = true; })
            }.margin({ top: 8 })
          }.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12, top: 4, bottom: 4 })
        }.width('100%').padding({ left: 16, right: 16 })
      }, (item: ErrandItem) => item.title + item.time)
    }.width('100%').padding({ bottom: 16 })
  }

最底部是编辑和删除两个操作按钮。编辑按钮为棕色文字,删除按钮为红色文字,视觉上区分了操作的危险程度。点击编辑时,将当前索引保存到 editIdx 并打开编辑弹窗;点击删除时,将索引保存到 delIdx 并打开删除确认弹窗。这种"先保存索引再打开弹窗"的模式,确保了弹窗操作时知道用户操作的是哪一条数据。

ForEach 的第二个参数 (item: ErrandItem) => item.title + item.time 是 key 生成函数,用于为每一项生成唯一标识,帮助框架高效地进行列表更新。

十一、跑腿师——人物大卡布局

跑腿师页面采用人物大卡的布局形式,每位跑腿师以一张横向卡片的形式呈现,卡片内容丰富,包含头像、姓名、状态、评分、接单量、标签、价格和下单按钮等信息。

11.1 标题栏

  @Builder
  tabRunner() {
    Column() {
      Row() {
        Text('🏃 跑腿师列表').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('智能匹配 ›').fontSize(13).fontColor(COLORS.orange)
      }.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })

标题栏右侧是"智能匹配 ›"的功能入口,暗示平台可以根据用户需求自动推荐最合适的跑腿师。

11.2 跑腿师卡片

      ForEach(this.runnerList, (item: RunnerItem, idx: number) => {
        Row() {
          Stack() {
            Text(item.avatar).fontSize(36)
              .scale(this.breath && idx === 0 ? { x: 1.1, y: 1.1 } : { x: 1, y: 1 })
              .animation({ duration: 1000, curve: Curve.EaseInOut })
          }
          .width(72).height(72).borderRadius(36)
          .backgroundColor(COLORS.chip).alignContent(Alignment.Center)

每张卡片最左侧是跑腿师头像,使用 72x72 的大尺寸圆形头像,视觉上非常突出。值得注意的是,第一位跑腿师的头像添加了呼吸动画效果——当 breathtrue 且是列表第一项时,头像放大到 1.1 倍。这种设计将用户的注意力引导到第一位推荐的跑腿师身上,暗示其为优选推荐。

          Column() {
            Row() {
              Text(item.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
              Text(item.status).fontSize(10).fontColor(COLORS.card)
                .padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8)
                .backgroundColor(item.status === '接单中' ? COLORS.green :
                  (item.status === '忙碌中' ? COLORS.orange : COLORS.text3))
                .margin({ left: 8 })
            }.width('100%')

姓名右侧是接单状态标签,根据状态不同显示不同颜色:接单中为绿色(可接单)、忙碌中为橙色(繁忙)、休息中为浅棕色(不可用)。这种三色状态标识直观地传达了跑腿师当前的可服务状态。

            Row() {
              Text('⭐').fontSize(12)
              Text(item.rating.toFixed(1)).fontSize(13).fontWeight(FontWeight.Bold)
                .fontColor(ratingColor(item.rating))
              Text(' · ').fontSize(12).fontColor(COLORS.text3)
              Text(item.orders.toString() + '单').fontSize(12).fontColor(COLORS.sub)
            }.margin({ top: 4 })

评分行展示了跑腿师的服务质量与经验数据。评分数字保留一位小数,颜色通过 ratingColor 函数分级显示——4.8 分以上绿色、4.5 分以上橙色、其余浅棕色。接单量以"X单"的形式展示,提供了跑腿师从业经验的量化指标。

            Row() {
              ForEach(item.tags, (tag: string) => {
                Text(tag).fontSize(10).fontColor(COLORS.orange)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .backgroundColor(COLORS.chip).borderRadius(6).margin({ right: 4 })
              })
            }.margin({ top: 6 })
          }.layoutWeight(1).margin({ left: 12 }).alignItems(HorizontalAlign.Start)

标签行展示跑腿师的服务特色标签,如"速度快"“细心负责”"金牌跑腿"等。每个标签都是一个小的橙色胶囊,为用户提供了跑腿师的人格化特征,增加了选择的维度。

          Column() {
            Text('¥' + item.fee.toString()).fontSize(18).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.orange)
            Text('起步价').fontSize(10).fontColor(COLORS.text3).margin({ top: 2 })
            Text('立即下单').fontSize(11).fontColor(COLORS.card).textAlign(TextAlign.Center)
              .width(70).padding({ top: 5, bottom: 5 })
              .backgroundColor(COLORS.orange).borderRadius(14).margin({ top: 8 })
          }.alignItems(HorizontalAlign.End)
        }
        .width('100%').padding(14).margin({ left: 16, right: 16, bottom: 10 })
        .backgroundColor(COLORS.card).borderRadius(16)
      }, (item: RunnerItem) => item.name)
    }.width('100%').padding({ bottom: 16 })
  }

卡片最右侧是价格与下单区域。起步价以 18 号加粗橙色大字显示,非常醒目。下方是"立即下单"的橙色圆角按钮,白色文字,是转化的核心入口。整个卡片使用白色背景、16px 圆角、14px 内边距,视觉上干净利落。

人物大卡布局的优势在于信息密度适中、视觉层次清晰——用户可以在一张卡片上快速获取跑腿师的核心信息(头像、姓名、状态、评分、标签、价格),并直接进行下单操作,转化路径极短。

十二、评价——评论卡片布局

评价页面采用评论卡片的布局形式,每条用户评价以独立卡片的形式纵向堆叠展示,卡片内容包括评价人信息、星级评分、评价内容和互动按钮。

12.1 标题栏

  @Builder
  tabReview() {
    Column() {
      Row() {
        Text('⭐ 用户评价').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('好评率 98.6%').fontSize(13).fontColor(COLORS.green)
      }.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })

标题栏右侧展示"好评率 98.6%",使用绿色文字,直接向用户传递平台服务质量优秀的信号,建立信任感。

12.2 评价卡片结构

      ForEach(this.reviewList, (item: ReviewItem, idx: number) => {
        Column() {
          Row() {
            Stack() { Text(item.avatar).fontSize(22) }
              .width(40).height(40).borderRadius(20).backgroundColor(COLORS.chip)
              .alignContent(Alignment.Center)

            Column() {
              Text(item.author).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
              Row() {
                Text('评价 ' + item.target).fontSize(11).fontColor(COLORS.sub)
                Text(' · ').fontSize(11).fontColor(COLORS.text3)
                Text(item.time).fontSize(11).fontColor(COLORS.text3)
              }.margin({ top: 2 })
            }.margin({ left: 10 }).alignItems(HorizontalAlign.Start).layoutWeight(1)

每张评价卡片的头部是评价人信息行。左侧是 40x40 的圆形头像,中间是评价人姓名与评价信息的纵向排列。信息行包含"评价 X师傅"(评价对象)和评价时间,使用次级文字颜色,作为辅助信息。

            Row() {
              ForEach([0, 1, 2, 3, 4], (i: number) => {
                Text(i < item.rating ? '★' : '☆').fontSize(12)
                  .fontColor(i < item.rating ? COLORS.orange : COLORS.text3)
              })
            }
          }.width('100%')

卡片右上角是星级评分,通过 ForEach 循环渲染 5 个星星符号。评分对应的星星用实心五角星(★)+ 橙色显示,未评分的用空心五角星(☆)+ 浅棕色显示。五星评分是电商与服务类应用的标配,用户对此有高度的认知习惯。

12.3 评价内容与互动区

          Text(item.content).fontSize(13).fontColor(COLORS.sub)
            .lineHeight(20).margin({ top: 10 })

评价内容是卡片的主体,使用 13 号棕色字体,行高设为 20px,保证多行文字的阅读舒适度。评价内容直接引用用户原始评价,真实感强。

          Row() {
            Text('👍 有用').fontSize(11).fontColor(COLORS.sub)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .backgroundColor(COLORS.bg).borderRadius(10)
            Text('💬 回复').fontSize(11).fontColor(COLORS.sub)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .backgroundColor(COLORS.bg).borderRadius(10).margin({ left: 8 })
            Blank()
            Text('查看详情 ›').fontSize(11).fontColor(COLORS.orange)
          }.width('100%').margin({ top: 10 })
        }
        .width('100%').padding(14).margin({ left: 16, right: 16, bottom: 10 })
        .backgroundColor(COLORS.card).borderRadius(16)
      }, (item: ReviewItem) => item.author + item.time)
    }.width('100%').padding({ bottom: 16 })
  }

卡片底部是互动操作区。左侧有"👍 有用"和"💬 回复"两个浅底色按钮,用户可以对评价进行点赞或回复互动。右侧是"查看详情 ›"的橙色链接,引导用户查看完整评价。

评论卡片布局的特点是简洁真实,以用户生成内容(UGC)为核心,通过真实的评价内容和星级评分建立平台的口碑可信度。每张卡片之间留有适当间距,阅读体验舒适。

十三、钱包——渐变大数字卡布局

钱包页面采用渐变数字卡的布局形式,以醒目的渐变色卡片展示账户余额,视觉冲击力强,金额信息一目了然。

13.1 标题栏

  @Builder
  tabWallet() {
    Column() {
      Row() {
        Text('💰 我的钱包').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('账单明细 ›').fontSize(13).fontColor(COLORS.orange)
      }.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })

标题栏右侧是"账单明细 ›"入口,用户可点击查看完整的收支流水。

13.2 渐变余额卡片

      ForEach(this.walletList, (item: WalletItem, idx: number) => {
        Stack() {
          Column() {
            Text('账户余额(元)').fontSize(12).fontColor(COLORS.chip)
            Text(item.balance.toFixed(2)).fontSize(48).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.card).margin({ top: 6 })
              .scale(this.breath && idx === 0 ? { x: 1.02, y: 1.02 } : { x: 1, y: 1 })
              .animation({ duration: 1000, curve: Curve.EaseInOut })

每张钱包卡片的核心是余额数字,使用高达 48px 的字号和加粗字重,在渐变背景上极为醒目。数字保留两位小数,精确到分。第一张卡片的余额数字还添加了微妙的呼吸缩放动画(1.02 倍),虽然幅度不大,但足以让用户的注意力被吸引到最重要的金额信息上。

            Row() {
              Column() {
                Text('冻结金额').fontSize(11).fontColor(COLORS.chip)
                Text('¥' + item.frozen.toFixed(2)).fontSize(14).fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.card).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)

              Column() {
                Text('累计收入').fontSize(11).fontColor(COLORS.chip)
                Text('¥' + item.total.toFixed(2)).fontSize(14).fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.card).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)

              Column() {
                Text('累计提现').fontSize(11).fontColor(COLORS.chip)
                Text('¥' + item.withdraw.toFixed(2)).fontSize(14).fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.card).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            }.width('100%').margin({ top: 16 })

余额下方是三个数据指标的横向等分排列:冻结金额、累计收入、累计提现。每个指标都包含标签和金额,标签使用浅橙色的弱化文字,金额使用白色加粗字体。三列数据通过 layoutWeight(1) 实现等分布局,信息结构均衡。

            Row() {
              Text('提现').fontSize(14).fontColor(COLORS.orange).textAlign(TextAlign.Center)
                .layoutWeight(1).padding({ top: 10, bottom: 10 })
                .backgroundColor(COLORS.card).borderRadius(20).margin({ right: 8 })
              Text('充值').fontSize(14).fontColor(COLORS.card).textAlign(TextAlign.Center)
                .layoutWeight(1).padding({ top: 10, bottom: 10 })
                .backgroundColor(COLORS.orangeD).borderRadius(20).margin({ left: 8 })
            }.width('100%').margin({ top: 18 })
          }
          .width('100%').alignItems(HorizontalAlign.Center)
        }
        .width('100%').padding(22).margin({ left: 16, right: 16, bottom: 12 }).borderRadius(18)
        .linearGradient({
          angle: 135,
          colors: idx % 2 === 0
            ? [[COLORS.orange, 0.0], [COLORS.red, 1.0]]
            : [[COLORS.orangeD, 0.0], [COLORS.redD, 1.0]]
        })
      }, (item: WalletItem) => item.balance.toString())

卡片底部是"提现"和"充值"两个操作按钮。提现按钮为白底橙字的次要按钮样式,充值按钮为深橙底白字的主要按钮样式。这里将充值作为强调操作,与一般金融应用中"充值"是高频正向操作的认知相符。

卡片最引人注目的特点是渐变背景。通过 linearGradient 方法设置了 135 度角的线性渐变,从左上角的跑腿橙渐变到右下角的活力红,色彩过渡自然流畅,视觉效果极具冲击力。偶数索引的卡片使用深色版本的渐变(橙深→红深),与奇数卡片形成明暗交替,增加了列表的节奏感。

13.3 最近记录标题

      Row() {
        Text('📜 最近记录').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('全部 ›').fontSize(12).fontColor(COLORS.orange)
      }.width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
    }.width('100%').padding({ bottom: 16 })
  }

渐变卡片下方是"最近记录"的标题栏,暗示下方将展示交易记录列表。虽然当前源码中记录列表的详细内容未展开,但标题已经为后续扩展预留了位置。

渐变大数字卡布局的设计理念是"重点信息重点展示"——钱包页面最核心的信息就是账户余额,因此用最大的字号、最醒目的渐变背景来突出它。这种设计在金融类页面中非常有效,用户能在瞬间获取最关键的财务信息。

十四、我的——清单行布局

"我的"页面采用清单行的布局形式,以用户信息卡片为头部,下方是功能菜单的列表式排列,结构清晰、操作便捷。

14.1 用户信息卡片

  @Builder
  tabMine() {
    Column() {
      Row() {
        Stack() {
          Text(this.profileList[0].avatar).fontSize(40)
            .scale(this.breath ? { x: 1.05, y: 1.05 } : { x: 1, y: 1 })
            .animation({ duration: 1000, curve: Curve.EaseInOut })
        }
        .width(72).height(72).borderRadius(36).backgroundColor(COLORS.chip)
        .alignContent(Alignment.Center)

        Column() {
          Text(this.profileList[0].name).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
          Row() {
            Text(this.profileList[0].level).fontSize(11).fontColor(COLORS.card)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
              .backgroundColor(COLORS.orange)
            Text(' · ').fontSize(12).fontColor(COLORS.text3)
            Text(this.profileList[0].phone).fontSize(12).fontColor(COLORS.sub)
          }.margin({ top: 6 })
        }.margin({ left: 14 }).alignItems(HorizontalAlign.Start).layoutWeight(1)

        Text('›').fontSize(20).fontColor(COLORS.text3)
      }
      .width('100%').padding(16).margin({ left: 16, right: 16, top: 16 })
      .backgroundColor(COLORS.card).borderRadius(16)

用户信息卡片是个人中心的"门面"。左侧是 72x72 的大尺寸圆形头像,带有呼吸缩放动画(1.05 倍),增加了页面的灵动感。中间是用户姓名和会员信息的纵向排列:姓名使用 18 号加粗深棕色字体,是卡片中最醒目的文字;下方一行包含会员等级标签(橙色背景白色文字,如"钻石会员")和脱敏后的手机号,中间用点号分隔。右侧是一个箭头符号,暗示点击可进入个人资料详情页。

14.2 数据统计栏

      Row() {
        Column() { Text('28').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.title); Text('进行中').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 }) }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column() { Text('156').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.title); Text('已完成').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 }) }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column() { Text('1286').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.orange); Text('积分').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 }) }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column() { Text('¥88').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.red); Text('优惠券').fontSize(11).fontColor(COLORS.sub).margin({ top: 2 }) }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
      }
      .width('100%').padding({ top: 14, bottom: 14 }).margin({ left: 16, right: 16, top: 12 })
      .backgroundColor(COLORS.card).borderRadius(16)

数据统计栏以四列等分的形式展示了四个关键数据:进行中订单数(28)、已完成订单数(156)、积分数(1286)、优惠券金额(¥88)。数字使用 20 号加粗字体,标签使用 11 号次级文字。值得注意的是,积分数字使用橙色、优惠券金额使用红色——这两项是具有"价值"属性的数据,用主题色加以强调,而订单数量则使用常规的深棕色。这种差异化的着色策略,让用户能够快速区分不同类型的数据。

14.3 功能菜单列表

      Column() {
        ForEach(MINE_MENU, (menu: string[], idx: number) => {
          Row() {
            Stack() { Text(menu[0]).fontSize(20) }
              .width(36).height(36).borderRadius(18).backgroundColor(COLORS.chip)
              .alignContent(Alignment.Center)
            Text(menu[1]).fontSize(14).fontColor(COLORS.title).margin({ left: 12 }).layoutWeight(1)
            Text('›').fontSize(18).fontColor(COLORS.text3)
          }
          .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 12 })
        })
      }
      .width('100%').margin({ left: 16, right: 16, top: 12 })
      .backgroundColor(COLORS.card).borderRadius(16)

功能菜单是个人中心的主体部分,包含 8 个菜单项。每个菜单项采用经典的"图标+文字+箭头"的横向布局:左侧是 36x36 的圆形图标容器,中间是菜单名称,右侧是箭头符号。layoutWeight(1) 让文字区域占据剩余空间,箭头自然靠右对齐。

菜单项之间没有显式的分隔线,而是通过统一的内边距(上下各 12px)形成自然的间距。这种设计让列表看起来更加简洁清爽。八个菜单项覆盖了用户在个人中心可能需要的所有功能入口,从上到下按使用频率排序——订单、地址、支付方式等高频功能排在前面,设置、帮助、关于等低频功能排在后面。

14.4 退出登录

      Text('退出登录').fontSize(14).fontColor(COLORS.red).textAlign(TextAlign.Center)
        .width('100%').padding({ top: 14, bottom: 14 })
        .margin({ left: 16, right: 16, top: 12, bottom: 16 })
        .backgroundColor(COLORS.card).borderRadius(16)
    }.width('100%')
  }

菜单列表的最下方是"退出登录"按钮,使用红色文字居中显示。红色暗示了这是一个需要谨慎操作的危险动作,与删除按钮的颜色语义保持一致。按钮同样采用白色卡片背景和圆角设计,与整体风格统一。

清单行布局的特点是结构清晰、操作高效——所有功能入口一目了然,用户可以快速定位到所需的操作。这也是移动端个人中心页面最经典、最常用的布局模式之一。

十五、图表卡片

图表卡片是六个 Tab 页面共通的底部组件,无论用户浏览哪个页面,都能看到近六个月的跑腿订单量趋势数据。这个设计体现了"数据驱动决策"的产品理念,让用户随时了解业务动态。

15.1 图表标题栏

  @Builder
  chartCard() {
    Column() {
      Row() {
        Text('📊 近6月跑腿订单量')
          .fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
        Blank()
        Text('单位: 单')
          .fontSize(11).fontColor(COLORS.text3)
      }.width('100%')

图表标题栏左侧是带柱状图标的标题文字,右侧是数据单位说明。标题使用 15 号加粗深棕色字体,单位说明使用 11 号浅棕色小字,主次分明。

15.2 柱状图实现

      Row() {
        ForEach(MONTH_IDX, (idx: number) => {
          Column() {
            Text(ERRAND_VAL[idx].toString())
              .fontSize(10).fontColor(COLORS.sub).margin({ bottom: 4 })
            Column()
              .width(24)
              .height(this.breath ? ERRAND_VAL[idx] * 0.7 : ERRAND_VAL[idx] * 0.65)
              .linearGradient({
                angle: 90,
                colors: [[COLORS.orange, 0.0], [COLORS.red, 1.0]]
              })
              .borderRadius(12)
              .animation({ duration: 1000, curve: Curve.EaseInOut })
            Text(MONTH_NAME[idx])
              .fontSize(10).fontColor(COLORS.text3).margin({ top: 4 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%').padding({ top: 16, bottom: 16 })
      .alignItems(VerticalAlign.Bottom)
    }
    .width('100%').padding(16)
    .margin({ left: 16, right: 16, bottom: 16 })
    .backgroundColor(COLORS.card).borderRadius(16)
  }

柱状图的实现非常巧妙,完全使用 ArkUI 的基础组件构建,无需引入第三方图表库。每个柱子由三部分组成:顶部的数值标签、中间的柱体、底部的月份标签。

柱体使用一个空的 Column 组件,通过设置宽度(24px)和高度来模拟柱状图的柱形。柱高由订单量数值乘以一个比例系数决定——当 breathtrue 时乘以 0.7,为 false 时乘以 0.65。这个微小的高度变化配合 1 秒的缓动动画,形成了柱子上下"呼吸"的动态效果,为静态图表增添了活力。

柱体的填充使用 90 度角的线性渐变,从左侧的跑腿橙渐变到右侧的活力红,与应用整体的橙红主题色保持一致。圆角(borderRadius: 12)的设置让柱体顶部呈现圆弧状,视觉上更加柔和精致。

数值标签显示在柱子顶部,使用 10 号棕色字体,让用户可以精确读取每个月的订单量。月份标签显示在柱子底部,使用 10 号浅棕色字体,作为横坐标标识。

所有柱子通过 layoutWeight(1) 在横向等分排列,alignItems(VerticalAlign.Bottom) 确保柱子从底部向上生长,符合柱状图的常规视觉认知。

整个图表卡片使用白色背景、16px 圆角、16px 内边距,与其他卡片组件的风格完全一致。图表组件的设计展示了 ArkUI 的灵活性——即使是复杂的数据可视化需求,也可以通过基础组件的巧妙组合来实现,而且性能优异、包体轻量。

十六、底部Tab栏

底部 Tab 栏是整个应用的导航枢纽,六个 Tab 按钮横向排列,用户点击即可在不同功能模块之间自由切换。

  @Builder
  tabBar() {
    Row() {
      ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
        Column() {
          Text(tab.icon).fontSize(22)
            .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
          Text(tab.name).fontSize(10)
            .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
            .margin({ top: 2 })
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          .onClick(() => { this.currentTab = idx; })
      })
    }.width('100%').height(58).backgroundColor(COLORS.card)
      .padding({ top: 6, bottom: 6 })
      .border({ width: { top: 1 }, color: COLORS.line })
  }

Tab 栏的结构简洁明了:一个横向的 Row 容器,通过 ForEach 遍历 TAB_LIST 数组渲染六个 Tab 按钮。每个 Tab 按钮是一个纵向排列的 Column,包含图标和文字两部分。

选中态的处理通过三元表达式实现:当 currentTab === idx 时,图标和文字都使用主题橙色(COLORS.tabOn),否则使用浅棕色(COLORS.text3)。这种颜色切换方式简单直接,视觉反馈清晰。

每个 Tab 按钮通过 layoutWeight(1) 实现等宽分布,确保六个按钮在底部导航栏中均匀占据空间。点击事件 onClickcurrentTab 设为对应的索引值,触发页面内容区的条件渲染切换。

Tab 栏的整体样式:高度 58px,白色背景,顶部有 1px 的分隔线,上下各 6px 的内边距。这个高度是移动端底部导航的经典尺寸,既保证了点击区域的舒适度,又不会过多占用屏幕空间。

六个 Tab 的图标都使用 Emoji 字符实现——🏠首页、📋订单、🏃跑腿师、⭐评价、💰钱包、👤我的。这种方案的优势是开发成本极低,无需引入额外的图标资源,跨平台兼容性好,而且 Emoji 图标的色彩丰富,视觉效果生动。

十七、弹窗系统

弹窗系统是应用交互的重要组成部分,该源码实现了三种模态弹窗:发布订单弹窗、编辑订单弹窗和删除确认弹窗。三个弹窗共享相同的遮罩层结构,但内容和功能各不相同。

17.1 遮罩层组件

  @Builder
  modalOverlay(onClose: () => void) {
    Column().width('100%').height('100%').backgroundColor(COLORS.mask)
      .onClick(() => { onClose(); })
  }

三个弹窗都使用了同一个遮罩层构建方法 modalOverlay。遮罩层是一个占满全屏的半透明黑色层(rgba(0,0,0,0.5)),点击遮罩层会触发传入的 onClose 回调函数,关闭弹窗。这种设计遵循了移动端弹窗的通用交互规范——点击空白区域关闭弹窗,为用户提供了便捷的退出方式。

将遮罩层抽离为独立的 @Builder 方法,避免了在每个弹窗中重复编写遮罩层代码,体现了组件复用的设计思想。

17.2 发布订单弹窗

  @Builder
  panelAdd(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column() {
        Text('📝 发布跑腿订单')
          .fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)

        Text('服务类型')
          .fontSize(13).fontColor(COLORS.sub).margin({ top: 16 })
        Row() {
          ForEach(['取快递', '送文件', '买药品', '代排队'], (s: string) => {
            Text(s).fontSize(11).fontColor(COLORS.orange)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(COLORS.chip).borderRadius(12).margin({ right: 6 })
          })
        }.margin({ top: 4 })

发布订单弹窗使用 Stack 布局,底层是半透明遮罩,上层是居中的白色卡片。卡片标题为"📝 发布跑腿订单",使用 18 号加粗字体。

表单内容从上到下依次为:服务类型(四个橙色胶囊标签供选择)、取件地址、送达地址、备注说明。地址和备注字段使用浅米色背景的文本框样式,带有占位提示文字。虽然当前是静态展示,但结构完整,为后续接入真实输入组件预留了位置。

        Row() {
          Text('取消').fontSize(14).fontColor(COLORS.sub)
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .backgroundColor(COLORS.bg).borderRadius(20)
            .onClick(() => { onClose(); })
          Text('发布订单').fontSize(14).fontColor(COLORS.card)
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .backgroundColor(COLORS.orange).borderRadius(20).margin({ left: 14 })
            .onClick(() => {
              this.errandList.unshift(
                new ErrandItem('取快递 · 新订单', '待分配', '待接单', 10, '09-01 15:00', '起点地址', '终点地址')
              );
              onClose();
            })
        }.margin({ top: 22 })
      }
      .width('82%').backgroundColor(COLORS.card).borderRadius(20).padding(20)
    }
    .width('100%').height('100%').alignContent(Alignment.Center)
  }

弹窗底部是"取消"和"发布订单"两个操作按钮。取消按钮为浅底棕字的次要按钮,点击后直接关闭弹窗。发布订单按钮为橙色主按钮,点击后执行新增逻辑——使用 unshift 方法将一条新的订单数据插入到 errandList 数组的最前面,然后关闭弹窗。由于 errandList@State 装饰,数据变化会自动触发订单列表的刷新,新订单会立即出现在列表顶部。

卡片宽度设为屏幕的 82%,圆角 20px,内边距 20px,整体视觉圆润舒适。

17.3 编辑订单弹窗

  @Builder
  panelEdit(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column() {
        Text('✏️ 编辑订单')
          .fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title)

        Text('订单标题')
          .fontSize(13).fontColor(COLORS.sub).margin({ top: 16 })
        Text(this.editIdx >= 0 ? this.errandList[this.editIdx].title : '')
          .fontSize(14).fontColor(COLORS.title).padding(12)
          .backgroundColor(COLORS.bg).borderRadius(8).width('100%').margin({ top: 4 })
        // ... 跑腿师、取件地址、送达地址字段

编辑订单弹窗的结构与发布订单弹窗类似,但内容是根据 editIdx 索引从 errandList 中读取的现有订单数据。弹窗展示了订单标题、跑腿师、取件地址、送达地址等字段,用户可以查看当前订单的详细信息。

        Text('订单状态')
          .fontSize(13).fontColor(COLORS.sub).margin({ top: 12 })
        Row() {
          ForEach(['待接单', '进行中', '已完成', '已取消'], (s: string) => {
            Text(s).fontSize(11).fontColor(errandColor(s))
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .border({ width: 1, color: errandColor(s) })
              .borderRadius(12).margin({ right: 6 })
          })
        }.margin({ top: 4 })

编辑弹窗中还展示了订单状态的四种选项,每个状态标签使用描边样式(边框+文字同色),颜色通过 errandColor 函数动态计算。这种描边样式与填充样式形成对比,暗示了"可选"而非"已选"的交互状态。

        Row() {
          Text('取消').fontSize(14).fontColor(COLORS.sub)
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .backgroundColor(COLORS.bg).borderRadius(20)
            .onClick(() => { onClose(); })
          Text('保存').fontSize(14).fontColor(COLORS.card)
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .backgroundColor(COLORS.blue).borderRadius(20).margin({ left: 14 })
            .onClick(() => { onClose(); })
        }.margin({ top: 22 })
      }
      .width('82%').backgroundColor(COLORS.card).borderRadius(20).padding(20)
    }
    .width('100%').height('100%').alignContent(Alignment.Center)
  }

底部操作按钮中,保存按钮使用了蓝色而非橙色。蓝色传达"中性操作"的语义,与发布的橙色(正向操作)和删除的红色(危险操作)形成区分,体现了色彩语义的一致性设计。当前源码中保存按钮仅关闭弹窗(未实现实际的保存逻辑),但结构已经搭建完整,后续只需在 onClick 中添加数据更新代码即可。

17.4 删除确认弹窗

  @Builder
  panelDel(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)
      Column() {
        Text('🗑️ 确认删除')
          .fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ top: 6 })

        Text('确定要删除这个订单吗?')
          .fontSize(14).fontColor(COLORS.sub).margin({ top: 12 })

        Text(this.delIdx >= 0 ? this.errandList[this.delIdx].title : '')
          .fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.red)
          .margin({ top: 4 })

        Text('删除后无法恢复,请谨慎操作')
          .fontSize(11).fontColor(COLORS.text3).margin({ top: 8 })

删除确认弹窗与前两个弹窗的风格有所不同——它更简洁、更聚焦于"确认"这个动作。弹窗顶部是垃圾桶图标和"确认删除"标题,中间是确认提示文字和待删除的订单标题(红色加粗显示,强化危险感知),底部是风险提醒文字。

这种"标题+说明+目标+警告"的四层信息结构,是删除确认弹窗的经典设计模式,层层递进地引导用户意识到操作的严重性,有效降低误删率。

        Row() {
          Text('取消').fontSize(14).fontColor(COLORS.sub)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .backgroundColor(COLORS.bg).borderRadius(20)
            .onClick(() => { onClose(); })
          Text('删除').fontSize(14).fontColor(COLORS.card)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .backgroundColor(COLORS.red).borderRadius(20).margin({ left: 16 })
            .onClick(() => {
              if (this.delIdx >= 0) { this.errandList.splice(this.delIdx, 1); }
              onClose();
            })
        }.margin({ top: 24 })
      }
      .width('72%').backgroundColor(COLORS.card).borderRadius(20).padding(24)
    }
    .width('100%').height('100%').alignContent(Alignment.Center)
  }

底部的删除按钮使用鲜艳的红色背景,强烈警示这是一个破坏性操作。点击删除按钮后,通过 splice 方法从 errandList 数组中移除对应索引的元素,然后关闭弹窗。响应式系统会自动检测到数组变化,订单列表实时更新,被删除的订单从界面上消失。

删除弹窗的卡片宽度为 72%,比发布/编辑弹窗(82%)更窄,这种尺寸上的缩小强化了"警告对话框"的视觉特征——信息越少、越聚焦,弹窗尺寸越小。

三个弹窗共同构成了完整的订单增删改查交互闭环,从发布新订单、编辑已有订单,到删除确认,每个环节都有对应的弹窗承载。弹窗与列表通过 @State 响应式数据联动,实现了数据变化即视图变化的流畅体验。

十八、功能模块对比表

为了更直观地展示六个 Tab 页面在布局风格、信息结构、交互方式等方面的差异,下面通过对比表的形式进行系统梳理。

对比维度 首页(宫格入口) 订单(状态时间轴) 跑腿师(人物大卡) 评价(评论卡片) 钱包(渐变数字卡) 我的(清单行)
布局形式 四行两列宫格网格 左侧时间轴+右侧内容 横向人物大卡片 纵向评论卡片堆叠 大尺寸渐变卡片 列表式菜单行
核心元素 图标+名称+描述+价格 状态圆点+起止地址+费用 头像+姓名+评分+标签+价格 头像+星级+评价内容 余额大数字+三指标 图标+文字+箭头
数据模型 ServiceItem ErrandItem RunnerItem ReviewItem WalletItem ProfileItem
数据条数 8条 6条 6条 6条 3条 1条(主账号)
卡片背景 白色+圆角16px 无独立卡片(嵌入时间轴) 白色+圆角16px 白色+圆角16px 橙红渐变+圆角18px 白色+圆角16px
标题栏右侧 更多 › + 发布订单 智能匹配 › 好评率 98.6% 账单明细 › 无(用户卡右侧箭头)
状态颜色 不涉及 橙/蓝/绿/红 四态 绿/橙/棕 三态(接单状态) 橙星评分 不涉及 橙/红 强调数据
呼吸动画 第一位头像缩放 第一张余额数字缩放 头像缩放
操作按钮 更多入口+优惠横幅 编辑+删除+发布 立即下单 有用+回复+详情 提现+充值 各菜单行点击
信息密度 高(8个入口一屏) 中高(多维度订单信息) 中(人物信息丰富) 中低(以评价文字为主) 低(突出核心数字) 高(8个功能入口)
视觉冲击力 中等(规整有序) 中等(时间轴有特色) 中高(人物头像醒目) 低(简洁阅读型) 最高(渐变+大数字) 低(经典列表型)
适用场景 服务导航、快速选择 订单跟踪、状态管理 跑腿师选择、下单转化 口碑展示、信任建立 财务概览、充值提现 个人设置、账户管理

从对比表中可以看出,六个模块在设计策略上各有侧重:

  1. 首页和"我的"页面信息密度最高,承担导航入口的功能,追求的是"一目了然、快速找到"。
  2. 钱包页面视觉冲击力最强,通过渐变和大数字突出核心财务信息,追求的是"重点信息重点展示"。
  3. 订单和跑腿师页面信息维度最丰富,需要在有限空间内展示多维度的业务数据,追求的是"信息完整、层次清晰"。
  4. 评价页面最为简洁,以用户生成内容为核心,追求的是"阅读舒适、真实可信"。

这种差异化的布局策略体现了产品设计的一个核心理念:不同的业务场景需要不同的信息架构和视觉表达。没有一种"万能布局"可以适应所有场景,只有根据每个模块的业务目标和信息特征,选择最适合的布局形式,才能为用户提供最佳的使用体验。

十九、总结与展望

通过对跑腿侠应用源码的深度剖析,我们可以清晰地看到 ArkUI 声明式开发框架在构建复杂行业应用时的强大能力与独特优势。本文从色彩体系、数据模型、组件结构,到六大功能模块的差异化布局实现,进行了全方位的代码解读与设计分析。下面对本次源码分析进行总结,并展望未来的优化方向。

19.1 核心亮点总结

1. 完善的色彩体系设计

该源码构建了一套层次分明、语义清晰的色彩系统,从背景色、文字色、主题色到功能色,每个颜色都有明确的用途和含义。跑腿橙与活力红的双色主题搭配,既符合跑腿服务行业热情活力的品牌调性,又通过明暗深浅的变化提供了足够的视觉层次。集中式的色彩管理方式也为后续的主题定制和风格迭代奠定了良好基础。

2. 规范的数据模型设计

六个 @Observed 装饰的业务模型类,结构清晰、职责明确,每个模型对应一个业务实体。统一的构造函数模式保证了实例化的便捷性,@Observed 装饰器则为响应式数据驱动提供了底层支持。模型与视图的分离设计,使得业务逻辑与界面渲染各司其职,代码可维护性高。

3. 差异化的布局策略

六个 Tab 页面采用了六种截然不同的布局方式——宫格、时间轴、人物大卡、评论卡、渐变数字卡、清单行——每种布局都与所在模块的业务特征高度匹配。这种差异化设计不仅丰富了应用的视觉体验,更重要的是让每个模块的信息传达效率达到最优。这充分展示了 ArkUI 在多样化布局方面的灵活性和表现力。

4. 巧妙的呼吸动画设计

通过一个全局的 breath 布尔状态配合 1 秒定时器,实现了多处界面元素的呼吸缩放动画效果。头部 Logo、跑腿师第一位头像、钱包余额数字、个人头像、图表柱子——这些关键视觉元素都因为呼吸动画而"活"了起来。呼吸动画的幅度控制得恰到好处,既增添了界面的灵动感,又不会过于炫目造成干扰。这种"一个状态驱动多处动画"的模式,实现成本极低而视觉收益极高,堪称性价比最优的动效方案。

5. 完整的交互闭环

弹窗系统提供了发布、编辑、删除三种模态弹窗,与订单列表通过 @State 响应式数据联动,实现了完整的增删改查交互闭环。遮罩层的复用、颜色语义的一致性、删除确认的警告设计,都体现了良好的交互设计素养。用户操作后的反馈即时可见,数据变化实时反映到界面,体验流畅自然。

6. 高度的组件化与模块化

整个页面被拆解为多个 @Builder 装饰的构建方法,每个方法负责一个功能区域。头部、六个 Tab、图表、底部导航、三个弹窗——各自独立封装、职责单一。这种模块化的组织方式,使得代码结构清晰、可读性强、维护成本低。开发者在修改某个功能区域时,只需关注对应的构建方法,不会影响其他部分。

19.2 技术价值与借鉴意义

这款跑腿侠应用的源码虽然只是一个前端界面原型,但其中蕴含的设计思想和技术实践,对于使用 ArkUI 开发行业应用的开发者具有很高的参考价值。

首先,它展示了如何从零构建一套完整的移动端应用界面——从色彩定义、数据建模,到组件拆分、布局实现,再到动效设计和交互逻辑,形成了一套完整的开发范式。开发者可以将这套范式迁移到其他行业应用的开发中,如外卖、电商、家政、物流等。

其次,它证明了声明式 UI 框架在复杂界面开发中的效率优势。传统的命令式开发中,开发者需要手动管理视图的创建、更新和销毁,代码繁琐且容易出错。而在 ArkUI 的声明式范式下,开发者只需描述"状态 X 对应界面 Y",框架自动处理中间的所有渲染细节。代码量显著减少,逻辑更加直观,开发效率大幅提升。

再次,它提供了多样化布局的实现参考。很多开发者在接触新框架时,往往只会用一两种布局模式反复套用,导致界面单调乏味。而该源码展示了六种完全不同的布局方式,每种都有其独特的结构和适用场景。这为开发者拓宽了思路,启发他们根据业务场景选择最合适的布局形式。

19.3 未来优化方向

尽管当前源码已经相当完善,但从生产级应用的标准来看,仍有不少可以优化和扩展的空间:

1. 组件粒度的进一步细化

当前所有的构建方法都集中在一个 Page809 组件中,对于约 1000 行的代码量来说尚可维护。但如果功能继续扩展,建议将独立的功能模块(如订单卡片、跑腿师卡片、评价卡片等)抽离为独立的自定义组件,通过 @Component 装饰器封装,使用 @Link@Prop 进行数据传递。这样可以进一步降低单文件的复杂度,提升组件的复用性。

2. 状态管理的架构升级

当前使用 @State 进行组件内状态管理,对于单页面应用来说已经足够。但如果应用扩展为多页面架构,建议引入全局状态管理方案,如 ArkUI 官方推荐的 AppStorage/LocalStorage,或者第三方的状态管理库。将用户信息、订单数据等全局状态提升到应用级管理,可以实现跨页面的数据共享与同步。

3. 真实数据对接

当前所有数据都是硬编码的常量数组,用于界面展示和交互演示。在实际项目中,需要将数据层替换为真实的网络请求,接入后端 API。可以封装专门的 Service 层负责数据请求,使用 Promise 或 async/await 处理异步逻辑,同时增加加载状态、错误处理、空状态等边界情况的 UI 展示。

4. 表单功能的完善

发布订单和编辑订单弹窗中的表单字段目前只是静态展示,尚未接入真实的输入组件。后续可以使用 ArkUI 的 TextInputTextAreaPicker 等输入类组件,构建完整的表单交互。同时需要增加表单验证逻辑,对必填项、格式正确性进行校验,提升用户输入的准确性。

5. 列表性能优化

当前列表数据量较小(最多 8 条),使用 ForEach 直接渲染没有性能问题。但如果数据量增长到几十上百条,建议引入 List + LazyForEach 的懒加载方案,只渲染可视区域内的列表项,大幅提升长列表的滑动流畅度和内存使用效率。

6. 多设备适配

ArkUI 的一大优势是跨设备适配。当前界面是针对手机竖屏设计的,可以进一步适配平板、折叠屏等更大屏幕的设备,利用更大的显示空间优化布局结构,比如将单列宫格扩展为多列、将列表页改造为左右分栏等。

7. 无障碍支持

可以为重要的界面元素添加无障碍属性(如 accessibilityLabel),让屏幕阅读器能够正确朗读界面内容,提升应用的可访问性。这既是技术人文关怀的体现,也是产品合规性的要求。

19.4 结语

跑腿侠应用的源码为我们展现了一幅 HarmonyOS ArkUI 开发的生动画卷。从精心调配的色彩体系,到结构清晰的数据模型;从差异化的六模块布局,到巧妙的呼吸动画设计;从完整的增删改查交互,到模块化的代码组织——每一处细节都体现了开发者对 ArkUI 框架的深入理解和对用户体验的用心打磨。

随着鸿蒙生态的持续发展,ArkUI 声明式开发框架正在被越来越多的开发者所认识和采用。相信通过对这类优秀行业应用源码的学习与借鉴,开发者们能够更快地掌握 ArkUI 的核心精髓,开发出更多高质量、体验优的鸿蒙原生应用,共同推动鸿蒙生态的繁荣发展。

在即时服务赛道上,跑腿侠这样的应用形态还有很大的想象空间。随着 LBS 技术、智能调度算法、即时通讯能力的不断融入,跑腿代办平台将不仅仅是一个下单工具,更会成为连接人与服务的智慧枢纽。而 ArkUI 这样高效、灵活、跨端的开发框架,将为这些创新应用的快速落地提供坚实的技术支撑。

附录:DevEco Studio 创建新项目与查看 SDK 版本

本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。


一、创建新项目

1.1 进入欢迎界面

启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:

  • 新建项目:从头创建新项目
  • 打开项目:打开本地已有项目
  • 克隆仓库:从 Git 等版本控制拉取代码

点击 “新建项目” 按钮,进入项目创建向导。

在这里插入图片描述

1.2 选择项目模板

在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:

类型 说明
应用(Application) 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期
元服务(Atomic Service) 开发轻量级的原子化服务,无需安装即可使用

选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

在这里插入图片描述

1.3 配置项目信息

点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:

配置项 示例值 说明
项目名称(Project name) rollboat 应用的项目名称,建议使用英文命名
包名(Bundle name) com.rollboat.myapplication 应用唯一标识,采用反向域名格式
保存路径(Save location) D:\CodeFactory\rollboat 项目本地存储路径,避免使用中文和空格
兼容 SDK(Compatible SDK) 6.1.1(24) 目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异
模块名称(Module name) entry 主模块名称,默认 entry 为应用入口模块
设备类型(Device types) ☑ Phone 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV

右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

在这里插入图片描述

1.4 完成创建

确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 Hvigor 构建初始化(Build Init

构建日志中显示 “退出代码为 0” 表示项目初始化成功。

在这里插入图片描述

1.5 项目结构概览

创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:

rollboat/
├── .hvigor/                   # Hvigor 构建工具缓存
├── .idea/                     # IDE 配置文件
├── AppScope/                  # 应用级全局配置
│   └── app.json5
├── entry/                     # 主模块(入口模块)
│   ├── src/main/ets/
│   │   ├── entryability/      # Ability 生命周期管理
│   │   │   └── EntryAbility.ets
│   │   └── pages/             # UI 页面
│   │       └── Index.ets      # 首页(默认 Hello World)
│   ├── src/main/resources/    # 资源文件
│   ├── module.json5           # 模块配置
│   └── build-profile.json5    # 构建配置
├── oh_modules/                # OHPM 依赖包
├── build-profile.json5        # 工程构建配置
├── hvigorfile.ts              # Hvigor 构建脚本
└── oh-package.json5           # 包管理配置

核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
关键语法 作用
@Entry 标记为页面入口,可用于路由跳转
@Component 声明为自定义组件
@State 状态变量,数据变更时自动触发 UI 刷新
RelativeContainer 相对布局容器,替代传统线性布局
.onClick() 点击事件,此处点击后文本变为 “Welcome”

打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

在这里插入图片描述


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:

文件 → 设置 → HarmonyOS SDK(或快捷键 Ctrl + Alt + S 搜索 “HarmonyOS SDK”)

在设置面板中,可以看到当前已安装的 SDK 版本信息:

名称 阶段 状态
HarmonyOS 6.1.1 Release ✅ 已安装

界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

在这里插入图片描述

2.2 查看 ArkUI-X SDK(跨平台扩展)

如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:

文件 → 设置 → 语言和框架 → ArkUI-X

在这里可以查看已安装和可选的 ArkUI-X SDK 版本:

版本 SDK 版本号 阶段 状态
API Version 24 6.1.1.100 Release ✅ 已安装
API Version 23 6.1.0.28 Beta1 未安装
API Version 22 6.0.2.112 Release 未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤 操作 关键点
创建项目 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 使用 Stage 模型 + ArkTS 语言
查看 SDK 设置 → HarmonyOS SDK SDK 已内置,无需手动安装
跨平台扩展 设置 → ArkUI-X 根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐