一、技术前言

在移动互联网时代,网约车出行平台已经成为了城市交通基础设施中不可或缺的一环。从滴滴出行到 Uber,从高德打车到 Lyft,各大出行平台都在不断优化用户体验、提升匹配效率、增强安全保障。对于开发者而言,构建一个功能完备的网约车应用,不仅需要处理复杂的业务逻辑,还需要在 UI 层面实现多场景、多状态的流畅切换。

本文将以一款基于 HarmonyOS ArkUI 框架开发的网约车出行平台 Demo 应用——"哒哒出行"为例,深入剖析其完整的技术实现方案。这款应用涵盖了叫车、行程记录、钱包管理、优惠券、司机评价、安全中心六大核心业务模块,底部采用单排六 Tab 导航,整体设计语言以"活力橙"为主题色,风格现代、视觉统一。

ArkUI 是华为鸿蒙生态下的声明式 UI 开发框架,它吸收了现代前端框架的设计理念,同时充分利用了鸿蒙操作系统的底层能力。ArkUI 的核心理念可以概括为"声明即所得"——开发者只需要描述 UI 应该是什么样子,框架会自动处理渲染和更新的细节。这种范式与命令式 UI 编程有着本质区别,它让开发者从繁琐的 DOM 操作中解放出来,专注于业务逻辑和用户体验。
在这里插入图片描述

在 ArkUI 框架中,以下几个核心概念贯穿了全套应用开发:

装饰器系统(Decorator System):ArkUI 通过 TypeScript 装饰器为组件注入了状态管理、生命周期、数据绑定等能力。@Entry 装饰器标记页面入口组件,@Component 装饰器声明自定义组件,@State 装饰器声明组件内部状态变量,@Observed 装饰器标记可观察的数据模型类,@Builder 装饰器定义可复用的构建函数。这套装饰器体系的优雅之处在于,它将状态管理与 UI 声明进行了有机融合,开发者无需手动管理数据绑定和视图更新。

声明式布局(Declarative Layout):ArkUI 提供了丰富的布局组件,如 Column(垂直布局)、Row(水平布局)、Stack(层叠布局)、Scroll(滚动容器)、Flex(弹性布局)等。通过组合这些基础布局组件,可以构建出任意复杂的页面结构。声明式布局的核心优势在于,代码即文档,阅读代码即可直观理解页面的层级结构。

响应式状态管理(Reactive State Management):当 @State 装饰的变量发生变化时,所有依赖该变量的 UI 组件会自动重新渲染。这种细粒度的响应式更新机制,确保了界面始终与数据保持一致,而无需开发者手动调用刷新方法。

动画系统(Animation System):ArkUI 内置了强大的动画引擎,支持透明度、缩放、旋转、位移等多种动画效果。通过 animation 属性,可以非常简便地为组件添加过渡动画,设置循环次数和播放模式,实现呼吸灯、脉冲、闪烁等视觉效果。

在这款哒哒出行应用中,开发者充分利用了以上技术特性的组合,构建了一个结构清晰、交互流畅、视觉精美的多 Tab 出行平台。下面,我们将从代码的第一行开始,逐段深入分析其完整的技术实现。
在这里插入图片描述


二、整体架构流程图

在深入代码细节之前,我们先通过一张 Mermaid 流程图来建立对整个应用架构的宏观认知。这张图展示了从入口组件到各个子系统的完整调用关系和数据流向。

弹框系统层

底部导航层

内容区域_Scroll

头部区域

数据模型层

状态管理层

入口组件层

Page791
@Entry @Component

currentTab: number
当前激活 Tab 索引

breath: boolean
呼吸动画开关

addModal / editModal / delModal
弹框控制开关

editIdx / delIdx
编辑与删除索引

addrList / tripList
@State 响应式列表

ColorPalette
21 色色彩体系

CarType 车型数据模型

Addr 地址数据模型

Trip 行程数据模型

Bill 账单数据模型

Coupon 优惠券数据模型

Driver 司机数据模型

Contact 紧急联系人数据模型

tripColor 工具函数

headerGo
起终点搜索条 + 附近车辆概览 + 优惠横条

tabRide
车型选择 + 常用地址

tabTrip
行程时间轴

tabWallet
渐变大卡 + 账单明细

tabCoupon
优惠券两段式卡片

tabDriver
司机评分卡

tabSafe
安全中心 + 报警按钮

chartCard
近 6 月行程柱状图

tabBar
单排 6 Tab 导航栏

panelAdd
新增地址面板

panelEdit
编辑地址面板

panelDel
删除行程面板

modalOverlay
遮罩层容器

从上述架构流程图中可以清晰地看到,整个应用以 Page791 组件为核心枢纽,向下连接了状态管理层、数据模型层、头部区域、内容区域、底部导航层和弹框系统层六大子系统。状态管理通过 @State 装饰器实现响应式绑定,数据模型层通过 @Observed 装饰器提供类型安全的可观察数据,而工具函数 tripColor 则负责将行程状态字符串映射为对应的颜色值。

在内容区域中,currentTab 状态变量作为条件判断的依据,决定了当前哪个 Tab 的内容被渲染。这种设计避免了多个 Tab 内容同时存在于视图树中,实现了按需渲染,有效降低了内存占用和渲染开销。弹框系统通过 addModaleditModaldelModal 三个布尔状态控制不同面板的显示与隐藏,所有弹框共享同一个遮罩层容器 modalOverlay,实现了弹框管理逻辑的统一。


三、文件头部注释与设计意图

// ============================================================
// 791 哒哒出行 · 网约车出行平台(现代行业:出行)
// 头部样式:出行搜索风(起终点搜索条 + 附近车辆概览 + 优惠横条)无动画
// 布局风格:6 个 tab 每个布局完全不同
//   叫车=车型行列表+地址卡 / 行程=竖向时间轴 / 钱包=渐变大数字卡+账单行
//   券包=两段式票券卡 / 司机=头像评分卡 / 安全=中心大卡+报警按钮
// 底部 6 tab 单排,主题:活力橙(浅色)
// ============================================================

这段文件头部注释是整个文件的"设计说明书"。在团队协作或者项目迭代中,开发者阅读这段注释就能在几秒钟内理解整个文件的结构和设计意图。我们来逐行解析:

“哒哒出行 · 网约车出行平台”:给出了这个 Demo 应用的名称和所属行业领域,明确了业务边界。这里的"现代行业:出行"标注提醒开发者,这是一个模拟真实出行场景的应用,所有功能和数据模型都围绕网约车出行展开。

“头部样式:出行搜索风”:定义了头部区域的设计风格为"出行搜索风",这是网约车应用的标志性设计——用户打开应用首先看到的就是"从哪里出发"和"去哪里"的搜索条。这个头部由一个起终点搜索条、一个附近车辆概览区域和一个优惠横条组成。"无动画"的标注说明头部区域采用静态展示,保持简洁高效,让用户专注于核心的搜索操作。

“布局风格:6 个 tab 每个布局完全不同”:这是最能体现开发者用心程度的设计理念。与常规的列表复用不同,每个 Tab 都采用了最适合其业务场景的独特布局方式。具体来说:叫车 Tab 采用车型行列表加地址卡片的组合,方便用户快速选择车型和常用地址;行程 Tab 采用竖向时间轴,清晰展示行程的时间顺序和状态流转;钱包 Tab 采用渐变大数字卡片加账单明细行,突出展示账户余额和收支情况;券包 Tab 采用两段式票券卡片,左侧金额右侧详情,一目了然;司机 Tab 采用头像评分卡片,展示司机形象和评分数据;安全 Tab 采用中心大卡片加报警按钮,强调安全功能的突出地位。

“底部 6 tab 单排”:底部导航栏采用单排六 Tab 布局,与常见的 4-5 个 Tab 不同,这里将六个功能模块全部平铺在一行中,每个 Tab 占据均等的宽度,通过 Emoji 图标和文字标签的组合来传递功能信息。

“主题:活力橙(浅色)”:色彩主题为活力橙,整体采用浅色背景。橙色是一种充满活力和温暖感的颜色,非常适合出行类应用——它让人联想到朝阳、温暖、行动力,与网约车"即刻出发"的产品理念高度契合。


四、色彩体系设计

4.1 ColorPalette 接口定义

interface ColorPalette {
  bg: string;
  card: string;
  chip: string;
  title: string;
  sub: string;
  text3: string;
  orange: string;
  orangeD: string;
  orangeL: string;
  blue: string;
  blueL: string;
  green: string;
  greenD: string;
  red: string;
  redD: string;
  purple: string;
  gold: string;
  goldL: string;
  line: string;
  tabOn: string;
  mask: string;
  tagBg: string;
}

ColorPalette 接口定义了应用所需的全部颜色变量,共计 21 个字段。这个数字虽然看起来不少,但每一个都有其特定的使用场景和语义含义。采用 TypeScript 接口定义色彩体系的优势体现在以下几个方面:

类型安全保障:TypeScript 编译器会在编译阶段对所有颜色引用进行类型检查。如果开发者不慎将 COLORS.orange 写成了 COLORS.orang,IDE 会立即标红提示,编译也会失败。这种机制从根本上杜绝了因拼写错误导致的运行时颜色异常问题。

语义化命名规范:每个颜色变量都与其实际用途紧密关联,阅读代码时无需记忆色值,只需理解语义即可。例如 title 代表标题文字颜色,sub 代表副标题文字颜色,text3 代表三级辅助文字颜色,line 代表分割线颜色,mask 代表遮罩层颜色。这种命名方式让代码的可读性大幅度提升,新加入项目的开发者也能快速上手。

色彩变体设计:对于品牌色 orange 和功能色 redgreen,都设计了深浅两种变体。orange(#FF7A00)是标准品牌色,orangeD(#D95F00)是深色变体,orangeL(#FFF1E3)是极浅变体。这种变体设计让开发者在不同场景下可以使用同一色系的不同明度,既保持了视觉统一性,又增加了层次感。例如,按钮背景使用 orange 标准色,文字使用 orangeD 深色变体,背景填充使用 orangeL 浅色变体。

主题可扩展性:如果未来需要支持深色模式,只需要新增一个 COLORS_DARK 常量对象,替换所有色值,然后在运行时根据用户偏好切换引用即可。所有组件代码无需修改,因为组件只引用 COLORS 而不是硬编码色值。

4.2 COLORS 常量赋值

const COLORS: ColorPalette = {
  bg: '#F5F6FA',
  card: '#FFFFFF',
  chip: '#F0F1F6',
  title: '#1F2430',
  sub: '#6B7280',
  text3: '#9CA3AF',
  orange: '#FF7A00',
  orangeD: '#D95F00',
  orangeL: '#FFF1E3',
  blue: '#3B82F6',
  blueL: '#EAF2FF',
  green: '#22C55E',
  greenD: '#16A34A',
  red: '#EF4444',
  redD: '#B91C1C',
  purple: '#8B5CF6',
  gold: '#F59E0B',
  goldL: '#FEF6E7',
  line: '#ECEEF3',
  tabOn: '#FF7A00',
  mask: 'rgba(0,0,0,0.55)',
  tagBg: '#FFF1E3'
};

这段代码将 ColorPalette 接口实例化为具体的色值常量。整体配色方案以浅色为主题基调,我们来逐一分析每个颜色变量的设计考量:

背景色 bg: '#F5F6FA':整体页面背景采用略带灰蓝的浅色,这是一个非常经典的中性浅色背景。它既不会像纯白那样刺眼,也不会像深灰那样沉闷,能够很好地衬托出白色卡片的内容。#F5F6FA 这个色值在 iOS 和 Material Design 系统中都有类似的用法,是经过大量用户测试验证的舒适背景色。

卡片色 card: '#FFFFFF':所有内容卡片采用纯白色背景,与页面背景形成清晰的层级对比。白色卡片在浅灰背景上会有明显的"浮起"效果,帮助用户建立清晰的视觉层次感。

碎片色 chip: '#F0F1F6':用于小标签、小按钮、小图标容器的背景。这个颜色比 bg 稍深一点,比 card 稍浅一点,起到了在白色卡片内部进一步区分区域的辅助作用。

文字颜色三层体系title: '#1F2430' 是接近黑色的深灰,用于最重要的标题文字,确保阅读清晰度;sub: '#6B7280' 是中灰色,用于副标题和次要信息,形成适度的视觉降级;text3: '#9CA3AF' 是浅灰色,用于提示文字、辅助信息、时间戳等,让用户知道这些信息重要但不紧急。这种三层文字颜色体系是移动端 UI 设计中的经典模式,通过颜色深浅来传递信息层级。

品牌色 orange: '#FF7A00':活力橙是这个应用的核心品牌色。#FF7A00 是一个饱和度适中的橙色,既有足够的视觉冲击力来吸引用户注意,又不会过于刺眼。它被用于按钮、选中态、重要标签、金额数字等关键位置。

功能色设计blue: '#3B82F6' 用于链接和操作按钮,green: '#22C55E' 用于成功状态和收入金额,red: '#EF4444' 用于警告和删除操作,purple: '#8B5CF6' 作为辅助色,gold: '#F59E0B' 用于评分和特殊标记。这些颜色都遵循了用户对颜色语义的普遍认知:蓝色代表"可点击"、绿色代表"成功/正向"、红色代表"危险/警告"、金色代表"优质/高价值"。

遮罩色 mask: 'rgba(0,0,0,0.55)':半透明黑色遮罩,用于弹框出现时的背景遮罩层。0.55 的透明度是一个经过精心调试的值,太浅会导致背景内容干扰弹框阅读,太深会显得过于突兀,0.55 在这个平衡点上提供了良好的视觉体验。


在这里插入图片描述

五、Tab 导航与辅助数据

5.1 Tab 数据模型

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

const TAB_LIST: TabMeta[] = [
  { icon: '🚗', label: '叫车' },
  { icon: '🧾', label: '行程' },
  { icon: '💰', label: '钱包' },
  { icon: '🎫', label: '券包' },
  { icon: '🧑‍✈️', label: '司机' },
  { icon: '🛡️', label: '安全' }
];

TabMeta 接口定义了 Tab 条目的数据结构,包含两个字段:icon 用于展示 Tab 的图标(使用 Emoji 字符),label 用于展示 Tab 的文字标签。将 Tab 数据抽象为接口和常量数组的设计,带来了几个重要的工程优势:

数据驱动渲染:底部 Tab 栏通过 ForEach 循环遍历 TAB_LIST 生成,而不是硬编码六个单独的 Column 组件。这意味着如果要增加或减少 Tab 数量,只需要修改 TAB_LIST 数组即可,无需修改渲染逻辑。这种"数据驱动 UI"的模式是声明式框架的核心优势之一。

语义化图标:使用 Emoji 字符作为图标是一种轻量级的图标方案。与传统的图片资源或 SVG 图标相比,Emoji 无需额外的资源文件,不占用包体积,且在不同平台上都能保持一致的渲染效果。每个 Emoji 的选择都经过了精心考量:🚗 代表叫车、🧾 代表行程记录、💰 代表钱包、🎫 代表优惠券、🧑‍✈️ 代表司机、🛡️ 代表安全保护。

可扩展性:如果未来需要为 Tab 添加更多属性(比如未读消息数、红点提示、权限控制等),只需在 TabMeta 接口中添加对应字段,然后在 TAB_LIST 数组中为每个 Tab 设置相应值即可。
在这里插入图片描述

5.2 辅助数据定义

const NEAR_CARS: string[] = ['附近快车 12 辆', '优享 4 辆', '豪华 2 辆', '接驾 3 分钟'];
const ADDR_TAGS: string[] = ['家', '公司', '学校', '机场', '自定义'];

const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['2月', '3月', '4月', '5月', '6月', '7月'];
const RIDE_VAL: number[] = [46, 62, 38, 74, 55, 88];

这几组辅助数据分别服务于不同的 UI 组件:

NEAR_CARS 附近车辆数据:这个数组被用于头部区域的附近车辆概览标签。四个字符串分别展示了快车数量、优享数量、豪华车数量和预计接驾时间。这些数据在真实应用中会通过 API 实时获取,在 Demo 中则使用静态数据模拟。

ADDR_TAGS 地址标签:在新增地址弹框中,用户可以从中选择一个标签来标记地址类型。这五个标签覆盖了最常见的出行场景:家、公司、学校、机场,以及一个"自定义"兜底选项。

MONTH_IDXMONTH_NAMERIDE_VAL 图表数据:这三组数组共同构成了近六个月行程柱状图的数据源。MONTH_IDX 是索引数组,MONTH_NAME 是月份名称,RIDE_VAL 是每月行程数量。数据显示出行量在 38 到 88 单之间波动,整体呈上升趋势,符合网约车使用频率随季节变化的规律。
在这里插入图片描述

5.3 工具函数

function tripColor(s: string): string {
  if (s === '已完成') {
    return COLORS.green;
  }
  if (s === '进行中') {
    return COLORS.orange;
  }
  return COLORS.text3;
}

tripColor 是一个纯函数,根据行程状态字符串返回对应的颜色值。这个函数的设计体现了"将业务逻辑从 UI 中分离"的最佳实践:

单一职责:函数只做一件事——将状态字符串映射为颜色。它不关心调用方如何使用这个颜色,也不关心状态字符串的来源。这种纯粹的函数式设计让代码易于测试和维护。

清晰的映射规则:已完成行程使用绿色(COLORS.green),传达"成功、安心"的语义;进行中行程使用橙色(COLORS.orange),传达"活跃、进行中"的语义;其他状态(如已取消)使用三级文本色(COLORS.text3),传达"已失效、不重要"的语义。

可复用性:行程时间轴中的圆点颜色和状态标签颜色都调用这个函数,保证了颜色规则的一致性。如果未来需要修改状态颜色映射,只需修改这一个函数,所有引用处都会自动更新。


六、数据模型层设计

6.1 CarType 车型数据模型

@Observed export class CarType {
  name: string;
  desc: string;
  price: string;
  eta: string;
  icon: string;
  tag: string;

  constructor(name: string, desc: string, price: string, eta: string, icon: string, tag: string) {
    this.name = name;
    this.desc = desc;
    this.price = price;
    this.eta = eta;
    this.icon = icon;
    this.tag = tag;
  }
}

const CAR_LIST: CarType[] = [
  new CarType('特惠快车', '拼座 · 顺路同行', '¥16.8', '3 分钟接驾', '🚙', '最省'),
  new CarType('快车', '一人独享 · 舒适', '¥24.5', '2 分钟接驾', '🚕', '推荐'),
  new CarType('优享', '新车型 · 更宽敞', '¥32.0', '4 分钟接驾', '🚘', '品质'),
  new CarType('六座商务', '适合多人出行', '¥48.0', '6 分钟接驾', '🚐', '拼团'),
  new CarType('豪华车', '高端车型 · 尊享', '¥86.0', '8 分钟接驾', '🏎️', '旗舰')
];

CarType 类使用了 @Observed 装饰器,这意味着当这个类的实例属性发生变化时,依赖该实例的 UI 组件会自动更新。类中定义了六个字段:

name 车型名称:如"特惠快车"、“快车”、“优享"等,是用户选择车型的主要标识。desc 描述信息:简洁地概括车型的特点,如"拼座 · 顺路同行”、“一人独享 · 舒适”。price 预估价格:以"¥"开头的价格字符串,方便直接展示。eta 预计接驾时间:告诉用户多久能上车。icon 车型图标:使用 Emoji 字符区分车型,🚙 代表特惠快车、🚕 代表快车、🚘 代表优享。tag 标签:辅助用户快速识别车型定位,如"最省"、“推荐”、“品质”、“旗舰”。

车型数据的设计遵循了真实网约车平台的定价策略:从最低的 16.8 元(特惠快车)到最高的 86.0 元(豪华车),价格梯度清晰,覆盖了从经济型到豪华型的全谱系。每种车型的接驾时间也随之递增,特惠快车因拼座需要等待 3 分钟,快车一人独享仅需 2 分钟,豪华车因数量少需要 8 分钟,这些数据都是符合真实出行场景的合理设定。
在这里插入图片描述

6.2 Addr 地址数据模型

@Observed export class Addr {
  tag: string;
  place: string;
  detail: string;
  icon: string;

  constructor(tag: string, place: string, detail: string, icon: string) {
    this.tag = tag;
    this.place = place;
    this.detail = detail;
    this.icon = icon;
  }
}

const ADDR_LIST: Addr[] = [
  new Addr('家', '高新区天府三街 88 号', '1 栋 2 单元 602', '🏠'),
  new Addr('公司', '软件园 B 区 3 栋', '12 层 1207 室', '🏢'),
  new Addr('学校', '川大江安校区东门', '接送孩子常用点', '🏫'),
  new Addr('健身房', '燃力健身环球店', '负一层停车场入口', '🏋️'),
  new Addr('机场', '天府国际机场 T2', '出发层 6 号门', '✈️')
];

Addr 类定义了常用地址的数据结构,四个字段涵盖了地址展示所需的所有信息:tag 是地址分类标签(家、公司、学校等),place 是地址的大致位置描述,detail 是门牌号、楼层、入口等精确信息,icon 是场景对应的 Emoji 图标。

五个常用地址覆盖了用户日常出行的主要场景:家庭住址的详细信息包含楼栋和单元号,公司地址精确到楼层和房间号,学校地址标注了接送孩子的常用出入口,健身房地址特别标注了停车场入口(体现健身场景的实际需求),机场地址标注了航站楼和出发层门号。这些细节设定让 Demo 数据看起来非常真实,增加了应用的可信度。

6.3 Trip 行程数据模型

@Observed export class Trip {
  date: string;
  from: string;
  to: string;
  car: string;
  fee: string;
  state: string;

  constructor(date: string, from: string, to: string, car: string, fee: string, state: string) {
    this.date = date;
    this.from = from;
    this.to = to;
    this.car = car;
    this.fee = fee;
    this.state = state;
  }
}

const TRIP_LIST: Trip[] = [
  new Trip('今天 09:12', '软件园 B 区', '天府三街地铁站', '快车', '18.5', '进行中'),
  new Trip('昨天 18:40', '环球中心', '软件园 B 区', '优享', '26.0', '已完成'),
  new Trip('昨天 08:05', '软件园 B 区', '环球中心', '特惠快车', '14.2', '已完成'),
  new Trip('08-17 20:31', '春熙路', '天府三街 88 号', '快车', '31.8', '已完成'),
  new Trip('08-16 09:22', '天府三街 88 号', '双流机场 T1', '六座商务', '52.0', '已完成'),
  new Trip('08-15 15:47', '交子公园', '铁像寺水街', '特惠快车', '12.6', '已完成'),
  new Trip('08-14 11:03', '省体育馆', '锦城湖', '快车', '22.4', '已取消'),
  new Trip('08-13 07:58', '天府三街 88 号', '火车南站', '快车', '16.0', '已完成')
];

Trip 类定义了行程记录的数据结构,包含六个字段:date 行程时间、from 出发地、to 目的地、car 车型、fee 费用、state 行程状态。

八条行程数据覆盖了"进行中"、“已完成”、"已取消"三种状态,时间跨度从今天到 8 月 13 日,展示了用户近一周的出行记录。数据设计上考虑了真实场景的多样性:有通勤出行(软件园到地铁站、软件园到环球中心)、有远距离出行(春熙路回家、天府三街到双流机场)、有休闲出行(交子公园到铁像寺水街)、有跨城出行(省体育馆到锦城湖)。费用从 12.6 元到 52.0 元不等,与车型和距离相匹配。特别值得注意的是,有一条"已取消"的行程记录,这是真实应用中常见的场景,用户可能在等待过程中取消了订单。

6.4 Bill 账单数据模型

@Observed export class Bill {
  icon: string;
  title: string;
  time: string;
  amount: string;
  out: boolean;

  constructor(icon: string, title: string, time: string, amount: string, out: boolean) {
    this.icon = icon;
    this.title = title;
    this.time = time;
    this.amount = amount;
    this.out = out;
  }
}

Bill 类定义了账单条目的数据结构,其中 out 字段是一个布尔值,用于区分支出和收入。true 表示支出(钱从账户流出),false 表示收入(钱流入账户)。这种设计比使用字符串枚举更加简洁,在 UI 渲染时通过 out 的值来决定显示减号还是加号,以及使用红色还是绿色。

十条账单数据中,六条是行程支出,四条是红包/返现/折扣收入。这种收支比例符合真实用户的使用场景——大部分账单是出行消费,偶尔有平台的优惠抵扣和返现活动。特别值得注意的是,收入类账单的 time 字段与对应的行程支出使用了相同的时间戳,例如"新客立减红包"的时间与"快车 · 软件园到环球中心"完全一致,这模拟了真实场景中红包抵扣与行程费用同时发生的逻辑。

6.5 Coupon 优惠券数据模型

@Observed export class Coupon {
  amount: string;
  cond: string;
  scope: string;
  expire: string;
  used: boolean;

  constructor(amount: string, cond: string, scope: string, expire: string, used: boolean) {
    this.amount = amount;
    this.cond = cond;
    this.scope = scope;
    this.expire = expire;
    this.used = used;
  }
}

Coupon 类定义了优惠券的数据结构,amount 是面额(元),cond 是使用条件(如"无门槛"、“满 15 可用”),scope 是适用范围(车型限制),expire 是有效期,used 标记是否已使用。

六张优惠券的设计覆盖了常见的优惠券类型:无门槛券(8元、6元)、满减券(满15减5、满30减10、满60减15、满80减20)。面额与使用门槛成正比,符合营销策略。其中周末限时券(6元)被标记为已使用,其余五张仍在有效期内,有效期的设置从 8 月 24 日到 9 月 10 日不等,给用户营造了一种"券快过期了,赶紧用"的紧迫感。

6.6 Driver 司机数据模型

@Observed export class Driver {
  avatar: string;
  name: string;
  car: string;
  plate: string;
  score: string;
  orders: string;
  tag: string;

  constructor(avatar: string, name: string, car: string, plate: string, score: string, orders: string, tag: string) {
    this.avatar = avatar;
    this.name = name;
    this.car = car;
    this.plate = plate;
    this.score = score;
    this.orders = orders;
    this.tag = tag;
  }
}

Driver 类定义了司机信息的数据结构,七个字段全面覆盖了司机的基本信息和服务评价。avatar 使用 Emoji 头像,name 是司机姓名,car 是车辆型号和颜色,plate 是车牌号,score 是评分(满分 5.0),orders 是累计完成订单数,tag 是司机获得的荣誉称号。

八位司机数据的设计非常用心:评分从 4.7 到 4.9 不等,保持在一个较高的水平,符合网约车平台真实的评分分布;订单数从 990 单到 3286 单不等,体现了司机经验的差异;荣誉称号有"金牌司机"、“年度之星”、“暖心服务”、“静音车厢”、“活地图”、“拾金不昧”、“新车出行”、“机场达人”,各具特色,让数据看起来真实而有温度。车牌号全部使用川A开头,符合成都本地的设定,且每个车牌号都不同,增加了真实感。

6.7 Contact 紧急联系人数据模型

@Observed export class Contact {
  name: string;
  phone: string;
  relation: string;
  icon: string;

  constructor(name: string, phone: string, relation: string, icon: string) {
    this.name = name;
    this.phone = phone;
    this.relation = relation;
    this.icon = icon;
  }
}

Contact 类定义了紧急联系人的数据结构,phone 字段使用了星号脱敏处理(如 138****6621),这是移动端应用中保护用户隐私的标准做法。三个联系人分别对应配偶、父亲和同事,覆盖了家庭和职场两类关系,这是在紧急情况下最可能联系的人群。


七、组件结构与状态管理

7.1 组件声明与状态变量

@Entry
@Component
struct Page791 {
  @State currentTab: number = 0;
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State breath: boolean = false;
  @State editIdx: number = 0;
  @State delIdx: number = 0;
  @State addrList: Addr[] = ADDR_LIST;
  @State tripList: Trip[] = TRIP_LIST;
  timer: number = -1;

@Entry 装饰器标记 Page791 为页面入口组件,这意味着它可以作为独立页面被导航到。@Component 装饰器声明这是一个自定义组件,支持状态管理和生命周期。

这里定义了八个 @State 装饰的状态变量,每一个都在应用中扮演着特定的角色:

currentTab:当前激活的 Tab 索引,初始值为 0(叫车 Tab)。当用户点击底部导航栏的某个 Tab 时,该值更新,触发内容区域的条件渲染切换。这是整个应用最核心的状态变量。

addModaleditModaldelModal:三个布尔值分别控制新增地址、编辑地址、删除行程弹框的显示与隐藏。使用三个独立的布尔值而非一个枚举值,是因为弹框可能会同时存在(虽然当前设计中不会),这种设计为未来的扩展留下了灵活性。

breath:呼吸动画的开关状态。这个布尔值被定时器在 true 和 false 之间翻转,驱动了多个组件中的透明度动画,包括行程时间轴中"进行中"状态圆点的闪烁、一键报警按钮的呼吸效果、司机列表中皇冠图标的脉冲动画。

editIdxdelIdx:分别记录当前编辑的地址索引和当前删除的行程索引。这两个索引值在用户点击对应操作按钮时被设置,在弹框中用于定位和修改对应的数据项。

addrListtripList:这两个列表是 @State 装饰的响应式数据,初始值复制自常量 ADDR_LISTTRIP_LIST。使用 @State 装饰意味着当列表内容发生变化时(如新增地址、删除行程),所有依赖这些列表的 UI 组件都会自动重新渲染。这是实现"新增地址后列表自动刷新"和"删除行程后列表自动更新"的关键。

timer:这是一个普通的数字变量,存储 setInterval 返回的定时器 ID,用于在组件销毁时清理定时器。它没有被 @State 装饰,因为它不需要触发 UI 更新。

7.2 生命周期管理

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

aboutToDisappear() {
  if (this.timer >= 0) {
    clearInterval(this.timer);
  }
}

aboutToAppear 是 ArkUI 的生命周期回调,在组件即将显示时触发。在这个函数中,我们启动了一个定时器,每 900 毫秒将 breath 状态翻转一次。这个 900 毫秒的间隔是经过精心选择的——太快会让动画显得急促,太慢则会让闪烁效果不够明显。900 毫秒约等于 0.9 秒,配合动画的 duration: 600-800 毫秒,形成了一个舒适的呼吸节奏。

aboutToDisappear 是组件即将销毁时的回调。在这个函数中,我们检查定时器 ID 是否有效(>= 0),如果有效则调用 clearInterval 清除定时器。这是防止内存泄漏的关键操作——如果不清理定时器,即使组件被销毁,定时器仍然会在后台运行,持续尝试更新一个已经不存在的组件状态,可能导致不可预知的错误和内存浪费。


八、头部区域详细分析

8.1 起终点搜索条

头部区域是网约车应用最重要的入口,用户打开应用后第一眼看到的就是"从哪里出发"和"去哪里"的搜索条。整个头部封装在 headerGo 构建函数中。

@Builder
headerGo() {
  Column({ space: 12 }) {
    Row({ space: 10 }) {
      Column() {
        Text('🚦').fontSize(20)
      }
      .width(40).height(40).borderRadius(12).backgroundColor(COLORS.card)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

头部最左侧是一个交通信号灯图标(🚦),放在一个 40x40 的圆角方形容器中。这个图标传达了"交通出行"的语义,是对应用定位的视觉强化。容器使用了 COLORS.card 白色背景和 12px 的圆角,与整体设计语言保持一致。

      Column({ space: 8 }) {
        Row({ space: 8 }) {
          Column().width(8).height(8).borderRadius(4).backgroundColor(COLORS.green)
          Column({ space: 2 }) {
            Text('当前位置').fontSize(8).fontColor(COLORS.text3)
            Text('软件园 B 区 3 栋').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
        }
        .width('100%')

        Column().width('92%').height(1).backgroundColor(COLORS.line)

        Row({ space: 8 }) {
          Column().width(8).height(8).borderRadius(4).backgroundColor(COLORS.orange)
          Column({ space: 2 }) {
            Text('目的地').fontSize(8).fontColor(COLORS.text3)
            Text('天府三街地铁站').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
        }
        .width('100%')
      }
      .layoutWeight(1)
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .borderRadius(14).backgroundColor(COLORS.card)

搜索条主体是一个白色圆角卡片,内部包含两个行:起点行和终点行。每行左侧有一个 8x8 的小圆点(起点绿色、终点橙色),右侧是标签文字和地址文字。起点和终点之间有一条 1px 的分割线。这种设计与主流网约车应用的搜索条高度一致,用户非常熟悉,没有学习成本。

绿色圆点代表"当前位置"(起点),传达"安全、可以出发"的语义;橙色圆点代表"目的地",传达"活跃、需要选择"的语义。圆点虽小,但通过颜色实现了视觉区分,帮助用户快速定位关注点。

      Column() {
        Text('🔄').fontSize(15)
      }
      .width(34).height(34).borderRadius(17).backgroundColor(COLORS.chip)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
    }
    .width('100%')

搜索条右侧是一个交换按钮(🔄),用于快速交换起点和终点。这个功能在网约车应用中非常实用——用户可能把目的地误填成了起点,或者需要反向导航。按钮使用圆形(borderRadius(17) 即 34/2)和碎片色背景,视觉上独立于搜索条,暗示这是一个可操作的元素。

8.2 附近车辆概览

    Row({ space: 8 }) {
      ForEach(NEAR_CARS, (c: string) => {
        Row({ space: 4 }) {
          Text('🚕').fontSize(10)
          Text(c).fontSize(10).fontColor(COLORS.sub)
        }
        .padding({ left: 10, right: 10, top: 5, bottom: 5 })
        .borderRadius(12).backgroundColor(COLORS.card)
      }, (c: string) => c)
    }
    .width('100%')

附近车辆概览是一个水平排列的标签组,通过 ForEach 循环遍历 NEAR_CARS 数组生成。每个标签是一个白色小圆角容器,包含一个出租车 emoji 和对应的文字信息。这种设计模拟了真实网约车应用中"附近有多少辆车"的实时展示,让用户在输入目的地之前就能对可用车辆有直观的了解。

8.3 优惠横条

    Row({ space: 6 }) {
      Text('🎁').fontSize(11)
      Text('新客立减 8 元 · 周五打车 85 折').fontSize(10).fontColor(COLORS.orangeD)
      Column().layoutWeight(1)
      Text('领取').fontSize(10).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
    }
    .width('100%').padding({ left: 12, right: 12, top: 8, bottom: 8 })
    .borderRadius(12).backgroundColor(COLORS.orangeL)

优惠横条使用浅橙色背景(COLORS.orangeL),左侧是礼物 emoji 和优惠文案,右侧是"领取"按钮。文案使用深橙色(COLORS.orangeD),"领取"按钮使用标准橙色(COLORS.orange)加粗体,形成视觉上的点击引导。中间的空 Column 使用 layoutWeight(1) 起到了弹性占位作用,将"领取"按钮推到最右侧。

整个头部区域的背景使用了线性渐变,从浅橙色渐变到页面背景色,角度 160 度,营造出一种从亮到暗的过渡效果,让头部区域在视觉上更具层次感。


九、叫车 Tab 详细分析

叫车 Tab(tabRide)是应用的默认首页,也是用户最常使用的功能。它由四个部分组成:车型选择标题、车型卡片列表、立即呼叫按钮、常用地址列表。

9.1 车型选择标题

Column({ space: 4 }) {
  Text('选择车型').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
  Text('预估价格已含拥堵调节费').fontSize(9).fontColor(COLORS.text3)
}
.width('100%').alignItems(HorizontalAlign.Start)

标题区域简洁明了,主标题"选择车型"使用粗体深色文字,让用户明确当前操作步骤。副标题"预估价格已含拥堵调节费"是一个重要的说明性文字,告知用户价格已包含拥堵调节费,避免用户对最终价格产生困惑。这个细节体现了对用户体验的细致考虑。

9.2 车型卡片列表

Column({ space: 10 }) {
  ForEach(CAR_LIST, (item: CarType) => {
    Row({ space: 12 }) {
      Column() {
        Text(item.icon).fontSize(22)
      }
      .width(46).height(46).borderRadius(14).backgroundColor(COLORS.orangeL)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

      Column({ space: 3 }) {
        Row({ space: 6 }) {
          Text(item.name).fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Text(item.tag).fontSize(8).fontColor(COLORS.orange)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8).backgroundColor(COLORS.tagBg)
        }
        Text(item.desc).fontSize(10).fontColor(COLORS.sub)
        Text(item.eta).fontSize(9).fontColor(COLORS.text3)
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)

      Text('¥' + item.price).fontSize(13).fontColor(COLORS.orangeD).fontWeight(FontWeight.Bold)
    }
    .width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
  }, (item: CarType) => item.name)
}
.width('100%')

每种车型渲染为一个白色圆角卡片,卡片内部采用水平布局。左侧是车型图标,放在一个 46x46 的浅橙色圆角容器中,图标放大到 22px 以突出显示。中间是车型信息区域,自上而下排列:车型名称和标签(水平排列)、描述文字、预计接驾时间。右侧是预估价格,使用深橙色粗体文字,视觉上突出价格信息。

标签(如"最省"、“推荐”、“品质”)使用橙色文字和浅橙色背景,圆角边框,与车型名称形成视觉区分。描述文字使用副文本色,接驾时间使用三级文本色,形成了清晰的信息层级。价格使用 COLORS.orangeD(深橙色),与品牌色保持一致,同时与周围文字形成强烈对比,让用户一眼就能看到价格。

ForEach 的第二个参数 (item: CarType) => item.name 是键生成函数,使用车型名称作为唯一键。当列表数据发生变化时,ArkUI 会根据键值来判断哪些项需要更新、新增或删除,从而实现高效的增量渲染。

9.3 立即呼叫按钮

Column({ space: 8 }) {
  Text('🚨 立即呼叫 · 特惠快车').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
  Text('预计 3 分钟接驾').fontSize(9).fontColor('rgba(255,255,255,0.8)')
}
.width('100%').padding({ top: 13, bottom: 13 }).borderRadius(16).backgroundColor(COLORS.orange)
.animation({ duration: 800, iterations: -1, playMode: PlayMode.Alternate })
.onClick(() => {
  this.addModal = true;
})

"立即呼叫"按钮是整个叫车 Tab 的核心操作按钮,设计上非常突出。按钮使用橙色背景(COLORS.orange),白色文字,宽度撑满整个容器。内部包含两行文字:主文字"🚨 立即呼叫 · 特惠快车"使用 14px 粗体,辅文字"预计 3 分钟接驾"使用半透明白色,暗示这是补充信息。

按钮上应用了呼吸动画:animation 属性设置了 800 毫秒的动画时长、无限循环(iterations: -1)和交替播放模式(playMode: PlayMode.Alternate)。由于按钮没有显式设置 opacityscale 等变换属性,这个动画配置结合其他组件的类似用法来看,实际是通过外部状态 breath 来驱动,营造出按钮"呼吸"的视觉效果,吸引用户点击。

点击按钮时触发 this.addModal = true,打开新增地址弹框。这个设计有点巧妙——在真实应用中"立即呼叫"应该触发下单流程,但这里为了演示弹框系统,将其关联到了新增地址功能。

9.4 常用地址列表

Column({ space: 10 }) {
  Row() {
    Text('常用地址').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
    Column().layoutWeight(1)
    Text('+ 新增').fontSize(10).fontColor(COLORS.orange)
      .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10).backgroundColor(COLORS.tagBg)
      .onClick(() => {
        this.addModal = true;
      })
  }
  .width('100%')

  ForEach(this.addrList, (item: Addr, idx: number) => {
    Row({ space: 10 }) {
      Column() {
        Text(item.icon).fontSize(15)
      }
      .width(34).height(34).borderRadius(10).backgroundColor(COLORS.chip)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

      Column({ space: 2 }) {
        Text(item.tag).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
        Text(item.place + ' · ' + item.detail).fontSize(9).fontColor(COLORS.text3)
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)

      Text('编辑').fontSize(10).fontColor(COLORS.blue)
        .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.blueL)
        .onClick(() => {
          this.editIdx = idx;
          this.editModal = true;
        })
    }
    .width('100%').padding(10).borderRadius(12).backgroundColor(COLORS.card)
  }, (item: Addr) => item.tag + item.place)
}
.width('100%')

常用地址列表包含标题行和地址卡片列表两部分。标题行左侧是"常用地址"标题,右侧是"+ 新增"按钮(橙色文字、浅橙色背景)。这个按钮和"立即呼叫"按钮一样,也触发新增地址弹框。

每个地址卡片是一个白色圆角容器,水平排列三个区域:左侧是场景图标(如🏠、🏢、🏫),中间是标签和地址详情,右侧是"编辑"按钮(蓝色文字、浅蓝色背景)。点击"编辑"按钮时,先设置 editIdx 为当前地址的索引,再打开编辑弹框,这样弹框中就能显示对应地址的信息。

注意 ForEach 的键生成函数使用了 item.tag + item.place 的组合,因为单个地址的标签和地点组合可以唯一标识一个地址条目。当新增地址时,ArkUI 会自动识别新键并追加渲染,而不需要重新渲染整个列表。


十、行程 Tab 详细分析

行程 Tab(tabTrip)采用了竖向时间轴的设计,这是展示时间序列数据最直观的方式。

10.1 标题行

Row() {
  Text('行程记录').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
  Column().layoutWeight(1)
  Text('共 ' + this.tripList.length + ' 单').fontSize(9).fontColor(COLORS.text3)
}
.width('100%')

标题行左侧是"行程记录"标题,右侧动态显示行程总数(this.tripList.length)。当用户删除行程后,这个数字会自动更新,因为 tripList@State 装饰的响应式数据。

10.2 时间轴列表

Column({ space: 0 }) {
  ForEach(this.tripList, (item: Trip, idx: number) => {
    Row({ space: 12 }) {
      Column({ space: 0 }) {
        Column().width(2).layoutWeight(1).backgroundColor(COLORS.line)
        Column().width(10).height(10).borderRadius(5)
          .backgroundColor(tripColor(item.state))
          .opacity(item.state === '进行中' && this.breath ? 0.3 : 1)
          .animation({ duration: 600, iterations: -1, playMode: PlayMode.Alternate })
        Column().width(2).layoutWeight(1).backgroundColor(COLORS.line)
      }
      .width(14).height(74).alignItems(HorizontalAlign.Center)

时间轴的左侧是时间指示器,由一个 14px 宽的容器承载。内部垂直排列三个元素:上方的连接线(2px 宽,layoutWeight(1) 表示占据剩余空间)、中间的圆点(10x10 圆形)、下方的连接线。圆点的颜色通过 tripColor(item.state) 动态获取:已完成显示绿色,进行中显示橙色,已取消显示灰色。

当行程状态为"进行中"时,圆点的透明度受 breath 状态控制,在 0.3 和 1 之间交替变化,配合 600 毫秒的交替动画,产生呼吸闪烁效果。这个动画让用户一眼就能注意到当前正在进行的行程,非常实用。

      Column({ space: 4 }) {
        Row({ space: 6 }) {
          Text(item.date).fontSize(9).fontColor(COLORS.text3)
          Text(item.state).fontSize(8).fontColor(tripColor(item.state))
            .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8).backgroundColor(COLORS.chip)
        }
        Text(item.from + ' → ' + item.to).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
        Row({ space: 6 }) {
          Text(item.car).fontSize(9).fontColor(COLORS.sub)
          Text('¥' + item.fee).fontSize(11).fontColor(COLORS.orangeD).fontWeight(FontWeight.Bold)
        }
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)

时间轴右侧是行程详情,采用垂直排列。第一行是时间和状态标签(水平排列),状态标签使用 tripColor 动态着色,放在灰色圆角背景中。第二行是行程路径,使用箭头(→)连接起点和终点,字体加粗以突出显示。第三行是车型和费用,车型使用副文本色,费用使用深橙色粗体,与叫车 Tab 中价格的颜色保持一致。

      Column() {
        Text('🗑️').fontSize(12)
      }
      .width(30).height(30).borderRadius(15).backgroundColor(COLORS.chip)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
      .onClick(() => {
        this.delIdx = idx;
        this.delModal = true;
      })
    }
    .width('100%').padding({ top: 8, bottom: 8 })
  }, (item: Trip) => item.date + item.from)
}
.width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)

每条行程最右侧是一个删除按钮(🗑️),圆形灰色背景。点击时设置 delIdx 为当前索引,然后打开删除确认弹框。这个设计将删除操作拆分为两步(点击按钮 + 确认弹框),避免了误操作,是移动端删除功能的标准实践。

整个时间轴列表放在一个白色圆角卡片中,padding(12) 提供了内容与卡片边缘的间距。


十一、钱包 Tab 详细分析

钱包 Tab(tabWallet)是视觉上最吸引眼球的模块,包含一个渐变大卡和账单明细列表。

11.1 渐变大数字卡

Column({ space: 8 }) {
  Text('账户余额(元)').fontSize(10).fontColor('rgba(255,255,255,0.75)')
  Text('256.80').fontSize(38).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
  Row({ space: 8 }) {
    Text('本月支出 ¥386.20').fontSize(9).fontColor('rgba(255,255,255,0.75)')
    Column().width(1).height(8).backgroundColor('rgba(255,255,255,0.35)')
    Text('累计节省 ¥92.40').fontSize(9).fontColor('rgba(255,255,255,0.75)')
  }
  Row({ space: 10 }) {
    Column() {
      Text('充值').fontSize(12).fontColor(COLORS.orangeD).fontWeight(FontWeight.Bold)
    }
    .layoutWeight(1).padding(10).borderRadius(12).backgroundColor('#FFFFFF')
    .onClick(() => {
      this.addModal = true;
    })

    Column() {
      Text('账单明细').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
    }
    .layoutWeight(1).padding(10).borderRadius(12).backgroundColor('rgba(255,255,255,0.22)')
  }
  .width('100%')
}
.width('100%').padding(18).borderRadius(18)
.linearGradient({ angle: 135, colors: [[COLORS.orange, 0], [COLORS.orangeL, 1]] })
.alignItems(HorizontalAlign.Start)

这是整个应用中最具视觉冲击力的组件。渐变卡片使用 135 度角的线性渐变,从标准橙色过渡到深橙色,营造出类似银行卡的金属质感。卡片内部所有文字都使用白色或半透明白色,与深色背景形成强烈对比。

余额数字"256.80"使用 38px 的超大字号和粗体,是本页面的视觉焦点。下方的收支数据展示了本月支出和累计节省,中间用一条半透明白色竖线分隔,设计简洁而优雅。

底部两个按钮采用了不同的设计风格:"充值"按钮使用纯白背景和深橙色文字,突出显示,点击打开新增地址弹框(演示用途);"账单明细"按钮使用半透明白色背景(rgba(255,255,255,0.22))和白色文字,形成视觉层级。两个按钮等宽分布(layoutWeight(1)),占据整个卡片宽度。

11.2 收支明细列表

Column({ space: 2 }) {
  Text('收支明细').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
  Text('仅展示近 10 条').fontSize(9).fontColor(COLORS.text3)
}
.width('100%').alignItems(HorizontalAlign.Start)

Column({ space: 10 }) {
  ForEach(BILL_LIST, (item: Bill) => {
    Row({ space: 10 }) {
      Column() {
        Text(item.icon).fontSize(15)
      }
      .width(34).height(34).borderRadius(10).backgroundColor(COLORS.chip)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

      Column({ space: 2 }) {
        Text(item.title).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
        Text(item.time).fontSize(9).fontColor(COLORS.text3)
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)

      Text((item.out ? '-' : '+') + '¥' + item.amount).fontSize(12)
        .fontColor(item.out ? COLORS.title : COLORS.green).fontWeight(FontWeight.Bold)
    }
    .width('100%').padding(10).borderRadius(12).backgroundColor(COLORS.card)
  }, (item: Bill) => item.title + item.time)
}
.width('100%')

收支明细区域以标题"收支明细"和辅助说明"仅展示近 10 条"开始,给用户明确的预期。

每条账单记录是一个白色圆角卡片,水平排列:左侧是场景图标(打车 🚕、红包 🎁、充值 💰),中间是账单标题和时间,右侧是金额。金额的显示逻辑非常关键:通过 item.out 判断是支出还是收入,支出显示减号(-)并使用标题色(深色),收入显示加号(+)并使用绿色(COLORS.green)。这个颜色差异化设计符合用户对财务信息的直觉认知——绿色代表"进账"、普通色代表"出账"。


十二、券包 Tab 详细分析

券包 Tab(tabCoupon)采用了两段式卡片设计,是优惠券展示的经典模式。

12.1 标题行

Row() {
  Text('我的优惠券').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
  Column().layoutWeight(1)
  Text(COUPON_LIST.length + ' 张可用').fontSize(9).fontColor(COLORS.text3)
}
.width('100%')

标题行右侧显示"x 张可用",由于 COUPON_LIST 是常量数组,这里的数字始终为 6。在真实应用中,这个数字应该动态计算未使用的优惠券数量。

12.2 两段式优惠券卡片

Column({ space: 10 }) {
  ForEach(COUPON_LIST, (item: Coupon) => {
    Row({ space: 0 }) {
      Column({ space: 4 }) {
        Text('¥').fontSize(12).fontColor(item.used ? COLORS.text3 : COLORS.orange).fontWeight(FontWeight.Bold)
        Text(item.amount).fontSize(26).fontColor(item.used ? COLORS.text3 : COLORS.orange).fontWeight(FontWeight.Bold)
        Text(item.cond).fontSize(8).fontColor(item.used ? COLORS.text3 : COLORS.orangeD)
      }
      .width(96).padding(12).justifyContent(FlexAlign.Center)
      .borderRadius({ topLeft: 14, bottomLeft: 14 })
      .backgroundColor(item.used ? COLORS.chip : COLORS.orangeL)

每张优惠券分成左右两段。左段是金额区域,宽度固定 96px,背景色根据使用状态变化:未使用为浅橙色(COLORS.orangeL),已使用为灰色(COLORS.chip)。内部垂直排列:人民币符号、大号金额数字、使用条件。当优惠券已使用时,所有文字颜色变为三级文本色(COLORS.text3),传达"已失效"的语义。

      Column({ space: 4 }) {
        Text(item.scope).fontSize(12)
          .fontColor(item.used ? COLORS.text3 : COLORS.title).fontWeight(FontWeight.Bold)
        Text('有效期至 ' + item.expire).fontSize(9).fontColor(COLORS.text3)
        Text(item.used ? '已使用' : '去使用 →').fontSize(9)
          .fontColor(item.used ? COLORS.text3 : COLORS.blue).fontWeight(FontWeight.Medium)
      }
      .layoutWeight(1).padding(12).alignItems(HorizontalAlign.Start)
      .borderRadius({ topRight: 14, bottomRight: 14 })
      .backgroundColor(COLORS.card)
    }
    .width('100%').borderRadius(14)
  }, (item: Coupon) => item.amount + item.scope)
}
.width('100%')

右段是详情区域,自适应宽度(layoutWeight(1)),白色背景。内部垂直排列:适用范围(粗体)、有效期、操作按钮。未使用的券显示"去使用 →",使用蓝色文字,带有箭头符号暗示可点击;已使用的券显示"已使用",使用灰色文字。

左段和右段通过不对称的圆角实现了视觉上的拼接效果:左段只有左上和左下圆角,右段只有右上和右下圆角。两张卡片拼在一起,中间形成一条自然的接缝,模拟了实体优惠券撕开的效果。

12.3 用券提示

Column({ space: 6 }) {
  Text('💡 用券提示').fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
  Text('每周五会员日可叠加使用 2 张,机场接送券不限时段。').fontSize(9).fontColor(COLORS.text3)
}
.width('100%').padding(12).borderRadius(12).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)

用券提示卡片是一个白色圆角容器,包含灯泡 emoji 和两行提示文字。这是对用户的有益引导,告知优惠券的使用规则,帮助用户最大化利用优惠。这个细节体现了产品设计中对用户利益的关注。


十三、司机 Tab 详细分析

司机 Tab(tabDriver)展示了用户常用的司机列表,采用头像评分卡片设计。

13.1 标题区

Column({ space: 2 }) {
  Text('常用司机').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
  Text('按你的好评记录推荐').fontSize(9).fontColor(COLORS.text3)
}
.width('100%').alignItems(HorizontalAlign.Start)

副标题"按你的好评记录推荐"解释了司机列表的来源——基于用户历史好评记录的个性化推荐。这个说明让用户理解列表的生成逻辑,增加信任感。

13.2 司机卡片列表

Column({ space: 10 }) {
  ForEach(DRIVER_LIST, (item: Driver) => {
    Row({ space: 12 }) {
      Column() {
        Text(item.avatar).fontSize(22)
      }
      .width(50).height(50).borderRadius(25).backgroundColor(COLORS.chip)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

      Column({ space: 3 }) {
        Row({ space: 6 }) {
          Text(item.name).fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Text(item.tag).fontSize(8).fontColor(COLORS.orangeD)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8).backgroundColor(COLORS.goldL)
        }
        Text(item.car + ' · ' + item.plate).fontSize(10).fontColor(COLORS.sub)
        Row({ space: 4 }) {
          Text('⭐').fontSize(9)
          Text(item.score).fontSize(10).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
          Text(item.orders).fontSize(9).fontColor(COLORS.text3)
        }
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)

      Column() {
        Text('👑').fontSize(15)
          .opacity(this.breath ? 1 : 0.45)
          .animation({ duration: 800, iterations: -1, playMode: PlayMode.Alternate })
      }
      .width(30).height(30).borderRadius(15).backgroundColor(COLORS.goldL)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
    }
    .width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
  }, (item: Driver) => item.name + item.plate)
}
.width('100%')

每位司机渲染为一个白色圆角卡片,水平排列四个区域:

左侧头像:使用 Emoji 头像,放在 50x50 的圆形灰色背景容器中。头像大小 22px,在圆形容器中居中显示。

中间信息区:第一行是司机姓名和荣誉标签,标签使用深橙色文字和金色背景(COLORS.goldL),与普通标签(tagBg)区分开来,暗示"荣誉"的特殊属性。第二行是车辆型号和车牌号,使用副文本色。第三行是星级评分和累计订单数,评分使用金色文字(COLORS.gold),与星级符号匹配。

右侧皇冠图标:这个区域是整个司机 Tab 的视觉亮点。皇冠图标(👑)放在金色圆形背景中,透明度受 breath 状态驱动,在 1 和 0.45 之间交替变化,产生脉冲动画效果。这个动画暗示了"优质司机"的尊贵属性,让用户对高评分司机产生好感。动画时长 800 毫秒,无限循环,与呼吸动画的整体节奏保持一致。


十四、安全 Tab 详细分析

安全 Tab(tabSafe)是出行应用中关乎用户生命安全的重要模块,设计上需要传达"可靠"和"紧急"的双重信息。

14.1 安全中心卡片

Column({ space: 10 }) {
  Text('🛡️').fontSize(34)
  Text('安全中心').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
  Text('行程录音保护已开启 · 行程自动分享给紧急联系人').fontSize(10).fontColor(COLORS.sub)
  Row({ space: 8 }) {
    Text('✅ 录音保护').fontSize(9).fontColor(COLORS.greenD)
    Text('✅ 实时定位').fontSize(9).fontColor(COLORS.greenD)
    Text('✅ 号码保护').fontSize(9).fontColor(COLORS.greenD)
  }
}
.width('100%').padding(20).borderRadius(18).backgroundColor(COLORS.card)

安全中心卡片是整个模块的核心,采用居中布局,从上到下依次展示:盾牌图标(🛡️,34px 超大字号)、"安全中心"标题(16px 粗体)、安全状态说明文字、三个安全功能的状态标签。

三个安全功能标签:录音保护、实时定位、号码保护,都使用绿色对勾(✅)和深绿色文字(COLORS.greenD),传达"已开启、安全可靠"的信息。这个绿色对勾的设计让用户感到安心,建立了对平台安全能力的信任。卡片使用 20px 的内边距和 18px 的圆角,视觉上宽敞、舒适。

14.2 一键报警按钮

Column({ space: 6 }) {
  Text('🆘 一键报警').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
  Text('危急时刻长按 3 秒触发').fontSize(9).fontColor('rgba(255,255,255,0.85)')
}
.width('100%').padding({ top: 14, bottom: 14 }).borderRadius(16).backgroundColor(COLORS.red)
.animation({ duration: 700, iterations: -1, playMode: PlayMode.Alternate })

一键报警按钮是整个安全模块中最紧急的视觉元素。按钮使用红色背景(COLORS.red),白色文字,宽度撑满。主文字"🆘 一键报警"使用 SOS emoji 和 15px 粗体,辅文字"危急时刻长按 3 秒触发"使用半透明白色,提示用户操作方式。

按钮上同样应用了呼吸动画,但时长缩短为 700 毫秒(比普通呼吸动画的 900 毫秒更快),暗示"紧急"的紧迫感。红色背景配合呼吸动画,在视觉上形成了强烈的警示效果,让用户知道这是一个需要谨慎使用的功能。

14.3 紧急联系人列表

Column({ space: 10 }) {
  Text('紧急联系人').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
  ForEach(CONTACT_LIST, (item: Contact) => {
    Row({ space: 10 }) {
      Column() {
        Text(item.icon).fontSize(15)
      }
      .width(34).height(34).borderRadius(17).backgroundColor(COLORS.chip)
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

      Column({ space: 2 }) {
        Text(item.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
        Text(item.relation + ' · ' + item.phone).fontSize(9).fontColor(COLORS.text3)
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)

      Text('编辑').fontSize(10).fontColor(COLORS.blue)
        .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.blueL)
        .onClick(() => {
          this.editModal = true;
        })
    }
    .width('100%').padding(10).borderRadius(12).backgroundColor(COLORS.bg)
  }, (item: Contact) => item.name + item.phone)
}
.width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)

紧急联系人列表展示了三个联系人,每个联系人渲染为一个浅灰色背景的行。左侧是联系人图标(圆形头像),中间是姓名和关系+电话(手机号已脱敏处理),右侧是"编辑"按钮。点击"编辑"按钮触发编辑弹框,但这里的编辑是针对联系人的(演示用途)。

注意这里联系人卡片的背景使用的是 COLORS.bg(页面背景色),而整个列表容器使用的是 COLORS.card(白色卡片色)。这种双层背景设计让联系人行在白色卡片中形成"嵌入"的视觉效果,层次分明。


十五、柱状图卡片详细分析

@Builder
chartCard() {
  Column({ space: 12 }) {
    Row() {
      Text('📈 近 6 个月行程').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
      Column().layoutWeight(1)
      Text('单位:单').fontSize(9).fontColor(COLORS.text3)
    }
    .width('100%')

    Row({ space: 0 }) {
      ForEach(MONTH_IDX, (mi: number) => {
        Column({ space: 6 }) {
          Text(RIDE_VAL[mi].toString()).fontSize(9).fontColor(COLORS.sub)
          Column()
            .width(16)
            .height(20 + RIDE_VAL[mi] * 0.6)
            .borderRadius({ topLeft: 4, topRight: 4 })
            .linearGradient({ angle: 180, colors: [[COLORS.orange, 0], [COLORS.orangeL, 1]] })
          Text(MONTH_NAME[mi]).fontSize(9).fontColor(COLORS.text3)
        }
        .layoutWeight(1)
      }, (mi: number) => mi.toString())
    }
    .width('100%')
  }
  .width('100%').padding(14).borderRadius(14).backgroundColor(COLORS.card)
}

柱状图卡片(chartCard)是独立于各 Tab 之外的公共组件,在 build 方法中始终渲染在所有 Tab 内容之后。也就是说,无论用户切换到哪个 Tab,柱状图都会显示在底部。

标题行:左侧是"📈 近 6 个月行程"标题,图表 emoji 直观传达了内容类型,右侧是"单位:单"的说明。

柱状图主体:通过 ForEach 遍历 MONTH_IDX 数组,生成六个柱状图列。每列是一个垂直排列的 Column,包含三个元素:顶部是数值文字(如 46、62、38、74、55、88),中间是柱状条,底部是月份名称。

柱状条高度计算20 + RIDE_VAL[mi] * 0.6。基础高度 20px 确保即使数据为 0 也有一个最小可见的柱子,RIDE_VAL[mi] * 0.6 将实际数据按比例缩放。以最大值 88 为例,柱子高度为 20 + 88 * 0.6 = 72.8px,最小值 38 的柱子高度为 20 + 38 * 0.6 = 42.8px。这个缩放比例让柱状图在有限的空间内产生明显的视觉差异。

渐变色柱子:每个柱子使用 180 度角(从下到上)的线性渐变,从标准橙色过渡到浅橙色。这种渐变让柱子看起来有立体感和光泽感,比纯色柱子更加美观。柱子顶部添加了 4px 的圆角(topLefttopRight),让柱状图看起来更加柔和精致。

等宽分布:每个柱子列使用 layoutWeight(1),在水平方向上均匀分布,占据整个卡片宽度。这种布局确保无论屏幕宽度如何变化,六个柱子始终保持等宽。


十六、底部 Tab 导航栏详细分析

@Builder
tabBar() {
  Row() {
    ForEach(TAB_LIST, (t: TabMeta, idx: number) => {
      Column({ space: 3 }) {
        Text(t.icon).fontSize(this.currentTab === idx ? 21 : 19)
        Text(t.label).fontSize(9)
          .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
          .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.currentTab = idx;
      })
    }, (t: TabMeta) => t.label)
  }
  .width('100%').height(58).backgroundColor(COLORS.card)
  .padding({ top: 6, bottom: 6 })
}

底部导航栏是整个应用的导航中枢,采用单排六 Tab 布局。整体高度 58px,白色背景,上下各 6px 内边距。

Tab 条目渲染:每个 Tab 是一个垂直排列的 Column,包含图标(Emoji)和文字标签,间距 3px。图标和文字都根据 currentTab === idx 判断是否选中,实现差异化样式:

选中态:图标放大到 21px,文字使用橙色(COLORS.tabOn,即 #FF7A00)并加粗。未选中态:图标缩小到 19px,文字使用三级文本色(COLORS.text3,即 #9CA3AF)且不加粗。

这种选中态设计通过"大小 + 颜色 + 粗细"三个维度的变化来提供视觉反馈,让用户清楚地知道当前处于哪个 Tab。与使用背景色高亮相比,这种方案更加简洁克制,不会让底部导航栏显得过于拥挤。

等宽分布:每个 Tab 使用 layoutWeight(1),六个 Tab 均分整个底部栏宽度。justifyContent(FlexAlign.Center) 确保每个 Tab 内容在分配的空间内垂直居中。

点击事件:点击任意 Tab 时,将 currentTab 更新为对应的索引,触发内容区域的条件渲染切换。


十七、弹框系统详细分析

弹框系统是应用中用户交互的重要组成部分,包括新增地址、编辑地址、删除行程三个弹框,以及一个通用的遮罩层容器。

17.1 新增地址面板

@Builder
panelAdd(onClose: () => void) {
  Column({ space: 14 }) {
    Text('新增常用地址').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

    Column({ space: 8 }) {
      Text('选择标签').fontSize(10).fontColor(COLORS.text3)
      Row({ space: 8 }) {
        ForEach(ADDR_TAGS, (tg: string) => {
          Text(tg).fontSize(11).fontColor(COLORS.sub)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(12).backgroundColor(COLORS.chip)
        }, (tg: string) => tg)
      }
    }
    .width('100%').alignItems(HorizontalAlign.Start)

新增地址面板接收一个 onClose 回调函数作为参数,用于关闭弹框。面板顶部是标题"新增常用地址",使用 16px 粗体。标签选择区域包含五个预设标签(家、公司、学校、机场、自定义),每个标签是一个灰色圆角按钮。在当前的 Demo 中,标签设计为可点击但未绑定选择逻辑,真实应用中需要添加 @State 变量来跟踪选中的标签。

    Column({ space: 6 }) {
      Text('默认信息').fontSize(10).fontColor(COLORS.text3)
      Text('高新区交子大道 500 号').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
      Text('3 栋 1801 室 · 将保存为新的常用地址').fontSize(9).fontColor(COLORS.text3)
    }
    .width('100%').padding(12).borderRadius(12).backgroundColor(COLORS.chip).alignItems(HorizontalAlign.Start)

默认信息展示区是一个灰色背景的卡片,展示将要保存的地址信息。这里使用了一个预设的地址,在真实应用中应该由用户手动输入或从地图选取。

    Row({ space: 10 }) {
      Column() {
        Text('取消').fontSize(13).fontColor(COLORS.sub).fontWeight(FontWeight.Medium)
      }
      .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.chip)
      .onClick(() => {
        onClose();
      })

      Column() {
        Text('确认新增').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
      }
      .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.orange)
      .onClick(() => {
        this.addrList.push(new Addr('新地点', '交子大道 500 号', '3 栋 1801 室', '📍'));
        onClose();
      })
    }
    .width('100%')

底部是两个等宽按钮:左侧"取消"按钮使用灰色背景,点击调用 onClose 关闭弹框;右侧"确认新增"按钮使用橙色背景和白色文字,点击时执行两个操作:向 addrList 中 push 一个新的 Addr 实例,然后调用 onClose 关闭弹框。由于 addrList@State 装饰的响应式数据,push 操作会触发 UI 自动更新,新增的地址会立即出现在常用地址列表中。

17.2 编辑地址面板

@Builder
panelEdit(onClose: () => void) {
  Column({ space: 14 }) {
    Text('编辑常用地址').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

    Column({ space: 6 }) {
      Text('当前地址').fontSize(10).fontColor(COLORS.text3)
      Text(this.editIdx < this.addrList.length ? this.addrList[this.editIdx].tag + ' · ' + this.addrList[this.editIdx].place : '无')
        .fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
      Text(this.editIdx < this.addrList.length ? this.addrList[this.editIdx].detail : '')
        .fontSize(9).fontColor(COLORS.text3)
    }
    .width('100%').padding(12).borderRadius(12).backgroundColor(COLORS.chip).alignItems(HorizontalAlign.Start)

编辑地址面板首先展示当前地址的信息,通过 this.editIdx 索引从 addrList 中获取对应的地址数据。这里使用了安全访问模式:先检查 this.editIdx < this.addrList.length,确保索引有效,避免数组越界错误。标签和地点使用粗体显示,详细地址使用三级文本色。

    Column({ space: 6 }) {
      Text('修改备注为').fontSize(10).fontColor(COLORS.text3)
      Text('已更新 · 备注门禁码 6320').fontSize(12).fontColor(COLORS.orangeD).fontWeight(FontWeight.Bold)
    }
    .width('100%').alignItems(HorizontalAlign.Start)

    Row({ space: 10 }) {
      Column() {
        Text('取消').fontSize(13).fontColor(COLORS.sub).fontWeight(FontWeight.Medium)
      }
      .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.chip)
      .onClick(() => {
        onClose();
      })

      Column() {
        Text('保存修改').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
      }
      .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.orange)
      .onClick(() => {
        if (this.editIdx < this.addrList.length) {
          this.addrList[this.editIdx].detail = '已更新 · 备注门禁码 6320';
        }
        onClose();
      })
    }
    .width('100%')

"保存修改"按钮点击时,先进行索引有效性检查,然后直接修改 addrList[editIdx].detail 属性。由于 Addr 类使用了 @Observed 装饰器,这种属性级别的修改也会触发 UI 自动更新,修改后的详情会立即反映在常用地址列表中。

17.3 删除行程面板

@Builder
panelDel(onClose: () => void) {
  Column({ space: 14 }) {
    Text('删除行程记录').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

    Column({ space: 6 }) {
      Text('即将删除:').fontSize(10).fontColor(COLORS.text3)
      Text(this.delIdx < this.tripList.length ? this.tripList[this.delIdx].from + ' → ' + this.tripList[this.delIdx].to : '无')
        .fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
      Text(this.delIdx < this.tripList.length ? this.tripList[this.delIdx].date : '')
        .fontSize(9).fontColor(COLORS.text3)
      Text('删除后不可恢复,发票开具记录将一并清除。').fontSize(9).fontColor(COLORS.redD)
    }
    .width('100%').padding(12).borderRadius(12).backgroundColor(COLORS.chip).alignItems(HorizontalAlign.Start)

删除行程面板展示了将要删除的行程路径和时间,并附有红色警告文字"删除后不可恢复,发票开具记录将一并清除"。这个警告文字使用深红色(COLORS.redD),在视觉上强调了删除操作的严重后果,让用户在操作前有充分的考虑时间。

    Row({ space: 10 }) {
      Column() {
        Text('再想想').fontSize(13).fontColor(COLORS.sub).fontWeight(FontWeight.Medium)
      }
      .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.chip)
      .onClick(() => {
        onClose();
      })

      Column() {
        Text('确认删除').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
      }
      .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.red)
      .onClick(() => {
        if (this.delIdx < this.tripList.length) {
          this.tripList.splice(this.delIdx, 1);
        }
        onClose();
      })
    }
    .width('100%')

底部两个按钮采用了不同的设计策略:左侧按钮不再使用"取消",而是使用"再想想"——这个措辞更加人性化,暗示用户应该再考虑一下。右侧"确认删除"按钮使用红色背景(COLORS.red),与删除操作的危险性相匹配。

确认删除时,使用 splice 方法从 tripList 中移除对应索引的行程。splice 是 JavaScript 数组的内置方法,它会直接修改原数组并触发 ArkUI 的响应式更新。删除后,行程列表自动刷新,行程总数也会自动更新。

17.4 遮罩层容器

@Builder
modalOverlay(onClose: () => void) {
  Column() {
    Column().layoutWeight(1).width('100%')
      .onClick(() => {
        onClose();
      })

    if (this.addModal) {
      this.panelAdd(onClose)
    }
    if (this.editModal) {
      this.panelEdit(onClose)
    }
    if (this.delModal) {
      this.panelDel(onClose)
    }
  }
  .width('100%').height('100%')
  .backgroundColor(COLORS.mask)
}

modalOverlay 是弹框系统的通用容器,采用全屏覆盖设计。它使用半透明黑色背景(COLORS.mask,即 rgba(0,0,0,0.55)),内部分为两个区域:上半部分是一个弹性占位区域(layoutWeight(1)),点击该区域会触发 onClose 关闭弹框(实现"点击遮罩关闭弹框"的效果);下半部分根据三个布尔状态变量条件渲染对应的面板。

这种设计将弹框的显示逻辑集中在遮罩层中,各个面板组件只负责自己的内容渲染,实现了关注点分离。如果未来需要新增弹框类型,只需添加一个新的面板 Builder 函数,并在遮罩层中添加对应的条件渲染即可。

17.5 面板样式统一

三个面板都共享了相同的底部样式:constraintSize({ maxHeight: '80%' }) 限制面板最大高度为屏幕的 80%,防止内容过多时面板撑满屏幕;backgroundColor(COLORS.card) 白色背景;borderRadius({ topLeft: 18, topRight: 18 }) 仅顶部圆角,模拟了 iOS 风格的底部弹出面板。


十八、主布局与 build 方法

build() {
  Stack({ alignContent: Alignment.Center }) {
    Column() {
      this.headerGo()

      Scroll() {
        Column({ space: 12 }) {
          if (this.currentTab === 0) {
            this.tabRide()
          } else if (this.currentTab === 1) {
            this.tabTrip()
          } else if (this.currentTab === 2) {
            this.tabWallet()
          } else if (this.currentTab === 3) {
            this.tabCoupon()
          } else if (this.currentTab === 4) {
            this.tabDriver()
          } else {
            this.tabSafe()
          }
          this.chartCard()
        }
        .width('100%')
        .padding({ left: 12, right: 12, top: 12, bottom: 16 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .width('100%')

      this.tabBar()
    }
    .width('100%').height('100%')

    if (this.addModal || this.editModal || this.delModal) {
      this.modalOverlay(() => {
        this.addModal = false;
        this.editModal = false;
        this.delModal = false;
      })
    }
  }
  .width('100%').height('100%')
  .backgroundColor(COLORS.bg)
}

build 方法是 ArkUI 组件的核心入口,定义了整个页面的 UI 结构。页面采用 Stack 层叠布局,包含两个层级:

底层(主界面):一个 Column 垂直布局,从上到下依次排列:头部区域(headerGo)、滚动内容区域(Scroll)、底部导航栏(tabBar)。

滚动内容区域使用 Scroll 组件包裹,内部是通过 if/else if 条件链根据 currentTab 值决定渲染哪个 Tab 的内容。scrollBar(BarState.Off) 隐藏了滚动条,保持界面简洁。layoutWeight(1) 让滚动区域占据剩余的全部空间,从而将底部导航栏固定在屏幕底部。

图表卡片chartCard)放置在条件渲染之后,不属于任何 Tab 专属内容,因此在所有 Tab 页面中都会显示。它位于内容区域的底部,为每个 Tab 页面都提供了数据可视化功能。

顶层(弹框层):当 addModaleditModaldelModal 中任意一个为 true 时,渲染遮罩层。遮罩层通过 Stack 的层叠特性覆盖在主界面之上。onClose 回调函数一次性将所有三个弹框状态重置为 false,确保弹框关闭后不会残留任何状态。

整体背景色:页面根节点使用 COLORS.bg#F5F6FA)作为背景色,为整个应用提供了统一的浅色基调。


十九、核心设计对比表

为了更直观地展示六个 Tab 模块的设计差异和技术特点,下面将其整理为对比表格:

对比维度 叫车 Tab 行程 Tab 钱包 Tab 券包 Tab 司机 Tab 安全 Tab
布局模式 车型行列表 + 地址卡片 竖向时间轴 渐变大卡 + 行列表 两段式票券卡片 头像评分卡片 中心大卡 + 按钮
核心数据模型 CarType, Addr Trip Bill Coupon Driver Contact
列表数量 5 款车型 + 5 个地址 8 条行程 10 条账单 6 张优惠券 8 位司机 3 个联系人
主要操作 选择车型、呼叫、新增地址 查看行程、删除行程 充值、查看账单 查看券、使用券 查看司机 报警、编辑联系人
弹框关联 新增地址、编辑地址 删除行程确认 充值(新增地址) 编辑联系人
动画效果 呼叫按钮呼吸动画 进行中圆点闪烁 皇冠图标脉冲 报警按钮呼吸动画
特殊设计 起终点交换、优惠横条 状态颜色映射 渐变金属卡片 左右分栏、已使用灰色 金色标签、评分星级 红色警告、隐私脱敏
色彩倾向 橙色主色 + 浅色背景 绿色/橙色状态色 橙色渐变 + 绿色收入 橙色/灰色 + 蓝色链接 金色标签 + 灰色基础 绿色安全 + 红色警告
数据操作 push 新增地址 splice 删除行程 修改 detail 属性
响应式更新 addrList 变化触发 tripList 变化触发 无列表变化 常量数据 常量数据 常量数据

从这张对比表中可以清晰地看到,开发者对每个 Tab 模块都进行了独立的设计考量,没有简单的"复用一套模板"。每个模块的布局模式、数据模型、操作类型、动画效果、色彩倾向都根据其业务特点进行了定制化设计。这种"每个模块不同布局"的设计理念,虽然增加了开发工作量,但为用户带来了更加精准和差异化的使用体验。


二十、技术总结

本文完整地剖析了一款基于 HarmonyOS ArkUI 框架开发的网约车出行平台 Demo 应用——"哒哒出行"的全部技术实现。从代码的第一行注释到最后一行的闭合括号,我们逐段分析了色彩体系设计、数据模型层构建、组件状态管理、六个 Tab 模块的独立布局、动画系统编排、弹框系统设计以及主布局的架构组织。

在架构设计方面,这款应用采用了"单页面多 Tab"的扁平化架构。通过 @State currentTab 状态变量驱动条件渲染,避免了多页面实例带来的内存开销,实现了按需渲染。Stack 层叠布局将弹框层与主界面分离,确保弹框始终覆盖在内容之上。Scroll 组件包裹所有 Tab 内容,保证内容超出屏幕高度时可以流畅滚动。

在数据管理方面,应用遵循"数据驱动 UI"的核心理念。所有列表数据通过 @State 装饰器声明,数据变化自动触发 UI 更新。数据模型类通过 @Observed 装饰器实现属性级别的可观察性,让属性修改也能触发精确的 UI 更新。工具函数 tripColor 将业务逻辑与 UI 渲染分离,实现了单一职责和可复用性。

在色彩体系方面,21 色的 ColorPalette 接口定义了完整的色彩规范,三层文字颜色体系(title/sub/text3)建立了清晰的视觉层级,品牌色变体(orange/orangeD/orangeL)提供了丰富的色彩层次,功能色(blue/green/red/gold/purple)覆盖了常用的语义场景。所有组件统一引用色彩常量,确保了视觉一致性,也为未来的主题切换预留了扩展空间。

在动画设计方面,应用巧妙运用了 breath 状态变量驱动多个动画效果。通过 setInterval 定时器以 900 毫秒为周期翻转 breath 值,配合 animation 属性的 iterations: -1playMode: PlayMode.Alternate,实现了行程圆点闪烁、呼叫按钮呼吸、报警按钮脉冲、皇冠图标跳动等多种动画效果。这些动画不仅增加了页面的生动性,更在功能层面起到了引导用户注意力的作用。

在弹框系统方面,通用遮罩层 modalOverlay 的设计实现了弹框的统一管理,点击遮罩区域关闭弹框的交互符合用户习惯。三个面板组件(新增地址、编辑地址、删除行程)各司其职,通过回调函数与遮罩层通信,实现了组件间的解耦。删除确认弹框中的红色警告文字和"再想想"按钮设计,体现了对用户操作安全性的细致考虑。

在用户体验方面,应用处处体现了对细节的关注。起终点搜索条中绿色(起点)和橙色(终点)的圆点颜色区分、优惠横条中"领取"按钮的视觉引导、钱包渐变卡片中余额数字的超大显示、优惠券已使用状态的灰色降级处理、安全中心绿色对勾传达的安心感、一键报警按钮红色背景配合呼吸动画的紧迫感——这些细节共同构建了一个专业、可信、易用的出行平台体验。

通过对这款"哒哒出行"应用的完整分析,我们可以看到 ArkUI 框架在构建复杂多 Tab 应用方面的强大能力。声明式 UI 开发范式让代码结构清晰直观,装饰器系统提供了灵活的状态管理能力,组件化设计保证了代码的可维护性和可扩展性。对于正在学习鸿蒙应用开发或正在构建出行类应用的开发者来说,这个 Demo 项目提供了一个优秀的参考范例。


附录: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、测试、元服务和应用上架分发等。

更多推荐