科技蓝与信号绿的B端叙事——HarmonyOS ArkUI 智慧停车六场景车场管理组件化实践
一、技术前言
在万物互联的智能时代,HarmonyOS 以其分布式软总线、分布式数据管理、分布式任务调度等核心能力,正在重塑移动应用的开发范式。而 ArkUI 作为 HarmonyOS 原生 UI 开发框架,以声明式开发为核心思想,为开发者提供了一套简洁高效、跨设备统一的 UI 开发体系。
ArkUI 框架采用了 TS/ETS 扩展的声明式开发范式,开发者通过结构化的声明式语法描述 UI 界面,框架则负责底层渲染与状态管理。其核心特性包括:@Component 装饰器定义自定义组件、@Entry 装饰器标记页面入口、@State 装饰器管理组件内部状态、@Observed 装饰器实现嵌套对象的响应式监听,以及 @Builder 装饰器封装可复用的 UI 构建函数。这些特性共同构成了 ArkUI 强大而灵活的开发体系。
在 B 端智慧停车领域,用户对界面的信息密度、操作效率和视觉专业性有着更高的要求。传统的列表式布局已无法满足复杂业务场景的需求,我们需要一种能够在有限屏幕空间内呈现丰富信息层级、同时保持视觉美感和交互流畅的界面设计方案。
本文将深入剖析一款名为「停易达」的智慧停车应用核心页面,该页面采用深色主题配合科技蓝(#3B82F6)与信号绿(#22C55E)的双色视觉体系,构建了六个截然不同的 Tab 场景——车场横滑大卡、车辆图标清单、订单状态时间轴、优惠虚线大卡、月卡渐变大数字卡、我的中心大卡。通过逐段拆解源码,我们将一同探索 ArkUI 声明式开发在 B 端复杂场景下的最佳实践,领略组件化、数据驱动、动画交互等技术手段如何共同塑造一个高品质的智慧停车用户体验。






二、整体架构流程图
在深入代码细节之前,我们先从宏观视角审视当前页面的整体架构。该页面采用典型的"单页多场景"架构设计,通过底部 Tab 栏在六个业务场景之间无缝切换,同时共享头部区域和图表卡片等公共组件。
架构解读
从架构图中可以清晰地看到,当前页面采用了分层设计思想,自上而下分为四个层次:
入口层是整个页面的起点,由 @Entry 装饰的 Page808 结构体承担,它是页面的根组件,负责协调各个子模块的组装与调度。
状态管理层是页面的"大脑中枢",通过 @State 装饰器维护着当前 Tab 索引、三个弹窗的开关状态、呼吸动画的布尔标记,以及六大业务数据列表。所有的视图渲染都以这些状态为"单一数据源",当状态发生变化时,ArkUI 框架会自动触发相关视图的重新渲染,这正是声明式开发的核心魅力所在。
视图渲染层是用户直接感知的界面呈现部分,包含了头部区域、Tab 内容区、图表卡片、底部 Tab 栏和弹窗系统五大模块。其中 Tab 内容区根据 currentTab 的值动态切换六个不同的 Builder 函数,实现了"一个页面、六种场景"的灵活架构。
数据模型层是业务数据的结构化抽象,六个 @Observed 装饰的类分别对应车场、车辆、订单、优惠券、月卡和用户六个业务实体。这些模型类不仅定义了数据结构,还通过 @Observed 装饰器赋予了响应式能力,确保数据变化能够及时反映到界面上。
工具与常量层则提供了全局可复用的色彩定义、Tab 元数据、辅助常量数组和颜色映射函数,是整个页面的"基础设施"。
三、色彩体系设计
色彩是界面视觉的灵魂。对于 B 端智慧停车应用而言,深色主题不仅能够降低眼部疲劳、提升专业感,更能让关键数据信息在深色背景上更加突出。当前源码构建了一套完整而严谨的色彩体系。
色彩接口定义
interface ColorPalette {
bg: string; card: string; chip: string;
title: string; sub: string; text3: string;
blue: string; blueD: string; green: string; greenD: string;
orange: string; red: string; line: string; tabOn: string; mask: string;
}
这段代码定义了 ColorPalette 接口,它规范了色彩体系的全部字段。接口的设计体现了清晰的分层思路:背景层(bg/card/chip)、文字层(title/sub/text3)、主题色层(blue/blueD/green/greenD)、功能色层(orange/red)、以及辅助色层(line/tabOn/mask)。这种结构化的色彩定义方式,使得整个应用的视觉风格保持高度一致,同时也便于后续的主题扩展和维护。
色彩常量赋值
const COLORS: ColorPalette = {
bg: '#0B1120', card: '#1E293B', chip: '#1E3A5F',
title: '#F1F5F9', sub: '#94A3B8', text3: '#64748B',
blue: '#3B82F6', blueD: '#2563EB',
green: '#22C55E', greenD: '#16A34A',
orange: '#F59E0B', red: '#EF4444',
line: '#334155', tabOn: '#3B82F6',
mask: 'rgba(0,0,0,0.6)',
};
COLORS 常量是色彩体系的具体实现,每一个颜色值都经过精心挑选:
- 背景三层级:
bg(#0B1120) 是最深的页面底色,接近于午夜蓝黑;card(#1E293B) 是卡片背景色,比底色稍亮,形成视觉层次;chip(#1E3A5F) 是标签/小圆块背景,带有科技蓝调,用于强调交互元素。 - 文字三层级:
title(#F1F5F9) 是最亮的标题文字,接近纯白;sub(#94A3B8) 是次要文字,灰蓝色调;text3(#64748B) 是最弱的辅助文字,用于非关键信息。 - 主题双色:科技蓝
blue(#3B82F6) 与深色调blueD(#2563EB) 构成蓝色系;信号绿green(#22C55E) 与深色调greenD(#16A34A) 构成绿色系。两组颜色各有深浅两个色阶,可用于渐变、悬停态等多种视觉场景。 - 功能色:
orange(#F59E0B) 用于优惠券等营销元素,red(#EF4444) 用于错误、删除等警示操作。 - 辅助色:
line(#334155) 是分割线颜色,tabOn(#3B82F6) 是 Tab 选中色,mask是弹窗遮罩的半透明黑色。
这套色彩体系的精妙之处在于,它并非简单的"深色+亮色"二元对立,而是通过背景、卡片、标签三个深度层级,配合标题、次要、辅助三个文字亮度层级,构建了丰富的视觉纵深。同时,科技蓝与信号绿的双色主题,既传达了科技感与专业性,又蕴含了畅通、高效的积极意象,与智慧停车"让停车更便捷"的产品理念高度契合。
四、Tab元数据与辅助数据
在数据驱动的 UI 开发中,合理的常量与元数据设计能够大幅提升代码的可维护性和可扩展性。当前源码中定义了丰富的 Tab 元数据和辅助常量数组,它们是界面渲染的"原材料"。
Tab元数据结构
interface TabMeta { name: string; icon: Resource; color: string; }
const TAB_LIST: TabMeta[] = [
{ name: '车场', icon: $r('app.media.ic_public_0'), color: COLORS.blue },
{ name: '车辆', icon: $r('app.media.ic_public_1'), color: COLORS.green },
{ name: '订单', icon: $r('app.media.ic_public_2'), color: COLORS.orange },
{ name: '优惠', icon: $r('app.media.ic_public_3'), color: COLORS.blueD },
{ name: '月卡', icon: $r('app.media.ic_public_4'), color: COLORS.greenD },
{ name: '我的', icon: $r('app.media.ic_public_5'), color: COLORS.blue },
];
TabMeta 接口定义了底部 Tab 栏每个选项的元数据结构,包含名称、图标和主题色三个属性。TAB_LIST 数组则按顺序定义了六个 Tab:车场(蓝)、车辆(绿)、订单(橙)、优惠(深蓝)、月卡(深绿)、我的(蓝)。
这种数据驱动的设计模式有诸多优势:首先,Tab 的数量、顺序、名称、图标都可以通过修改数组轻松调整,无需改动布局代码;其次,每个 Tab 都有独立的主题色配置,为后续的 Tab 切换动画或主题色联动提供了数据基础;最后,配合 ForEach 循环渲染,底部 Tab 栏的代码量与 Tab 数量解耦,新增 Tab 只需追加数组元素即可。
辅助常量数组
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const PARK_FEE_VAL: number[] = [186, 220, 168, 245, 290, 312];
这三组常量服务于停车费用柱状图。MONTH_IDX 提供了循环索引,MONTH_NAME 存储月份标签,PARK_FEE_VAL 则是各月的费用数值。三组数组通过索引一一对应,形成了图表的数据源。这种"平行数组"的设计在简单图表场景下非常实用,避免了引入复杂的图表库或数据模型。
const NEAR_NAME: string[] = ['国贸', '万达', '金融街', '三里屯', '中关村', '望京'];
const NEAR_AVAIL: number[] = [45, 12, 88, 23, 156, 67];
这两组常量用于头部区域的"附近空位"迷你柱状图,分别存储了六个热门商圈的名称和空余车位数量。头部区域集成这样一个小型数据可视化组件,能够让用户在进入应用的第一时间就掌握周边停车资源的分布情况,体现了 B 端产品"信息前置"的设计理念。
const DASH_IDX: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
DASH_IDX 是一个包含十个元素的索引数组,它的作用颇为巧妙——用于循环渲染优惠券卡片中间的虚线分割线。通过十个细高的小方块纵向排列,配合适当的间距,模拟出了虚线效果。这是一种纯 CSS/布局层面的虚线实现方案,无需依赖边框样式的 dashed 属性,具有更好的可控性和跨平台一致性。
const QUICK_ENTRIES: string[] = ['📍 找车位', '🚗 我的车辆', '🎫 领券', '📅 续月卡'];
快捷入口数组定义了车场 Tab 下方的四个快捷操作按钮。这些入口是用户最高频的操作路径,将其提炼为独立的快捷入口区域,能够有效提升操作效率。
const CAR_COLORS: string[] = ['⚪ 珍珠白', '⚫ 耀夜黑', '🔵 极光蓝', '🔴 烈焰红', '🟢 星际绿'];
const VEH_BRANDS: string[] = ['特斯拉', '比亚迪', '蔚来', '小鹏', '理想', '大众'];
车辆颜色和品牌数组用于添加车辆弹窗中的选项展示。每个颜色名称前面都带有对应颜色的 emoji 图标,这种图文结合的方式既直观又生动,能够帮助用户快速识别和选择。
const MINE_FUNCS: string[][] = [['🅿️', '我的车场'], ['🚗', '我的车辆'], ['💳', '支付方式'], ['📞', '在线客服']];
MINE_FUNCS 是一个二维数组,存储了"我的"页面中的四个功能入口。每个元素由图标 emoji 和功能名称组成。使用二维数组而非对象数组,在这种简单场景下能够减少代码量,同时保持数据结构的清晰度。
五、工具函数
工具函数是代码库中的"瑞士军刀",它们虽小,却承担着重要的逻辑转换职责。当前源码中定义了两个颜色映射工具函数,分别用于订单状态和优惠券状态的颜色计算。
订单状态配色函数
function orderColor(s: string): string {
if (s === '停车中') return COLORS.blue;
if (s === '已结算') return COLORS.green;
if (s === '已取消') return COLORS.red;
return COLORS.text3;
}
orderColor 函数接收一个订单状态字符串,返回对应的颜色值。它的逻辑清晰直白:
- 停车中 → 科技蓝(#3B82F6):蓝色代表进行中、动态的状态,车辆正在场内停放,符合"进行时"的语义。
- 已结算 → 信号绿(#22C55E):绿色代表完成、成功的状态,订单已支付结算,是一个正向的结果。
- 已取消 → 警示红(#EF4444):红色代表终止、异常的状态,订单被取消,需要用户特别关注。
- 未知状态 → 辅助灰(#64748B):作为默认兜底,确保函数始终返回有效颜色。
这个函数的应用场景非常广泛:订单时间轴上的状态圆点颜色、订单卡片右上角的状态标签背景色、订单金额的文字颜色等,都通过调用这个函数来获取颜色。这种"单一数据源"的设计确保了同一状态在不同位置的颜色表现完全一致,避免了视觉不一致的问题。同时,如果未来需要调整某个状态的配色,只需修改这一处函数即可,维护成本极低。
优惠券状态配色函数
function couponColor(c: boolean): string {
return c ? COLORS.text3 : COLORS.orange;
}
couponColor 函数更加简洁,它接收一个布尔值表示优惠券是否已领取,返回对应的颜色:已领取返回辅助灰色,未领取返回警示橙色。
这个设计蕴含了深刻的产品思维:未领取的优惠券用醒目的橙色吸引用户注意,刺激领取转化;已领取的优惠券则用灰色弱化处理,减少视觉干扰。这种"强引导+弱干扰"的色彩策略,是 B 端产品中常见的运营设计手法。
值得注意的是,两个工具函数的命名都采用了"名词+Color"的模式,清晰地表达了函数的用途是"为某个业务对象计算颜色"。这种命名规范在团队协作中能够有效降低沟通成本,让代码的可读性和可维护性都得到提升。
六、数据模型层
数据模型是应用的骨架,它定义了业务数据的结构和行为。当前源码通过六个 @Observed 装饰的类,构建了完整的智慧停车业务数据模型。@Observed 装饰器是 ArkUI 响应式系统的重要组成部分,它使得类的实例属性变化能够被 UI 框架监听,从而触发视图的自动更新。
6.1 ParkingItem 车场模型
@Observed export class ParkingItem {
name: string; address: string; price: number; total: number; available: number; distance: number; icon: string;
constructor(name: string, address: string, price: number, total: number, available: number, distance: number, icon: string) {
this.name = name; this.address = address; this.price = price; this.total = total;
this.available = available; this.distance = distance; this.icon = icon;
}
}
ParkingItem 类描述了一个停车场的完整信息,包含七个字段:
- name:车场名称,如"国贸中心停车场",是车场的核心标识。
- address:车场地址,提供位置信息,帮助用户确认目的地。
- price:每小时停车价格,单位为元,是用户选择车场的重要决策因素。
- total:总车位数量,反映车场规模。
- available:空余车位数量,是实时性最强的字段,直接影响用户的停车决策。
- distance:距离当前位置的距离,单位为公里,配合导航功能使用。
- icon:车场图标(emoji),用于视觉区分不同类型的车场。
对应的模拟数据 PARKING_LIST 包含六条记录,覆盖了国贸、万达、金融街、三里屯、中关村、望京六个北京核心商圈的停车场,价格从 6 元到 15 元不等,车位规模从 240 到 580 个,空余车位数量也各有差异。这种多样化的模拟数据能够很好地展示横滑大卡在不同数据情况下的呈现效果。
6.2 VehicleItem 车辆模型
@Observed export class VehicleItem {
plate: string; brand: string; color: string; isDefault: boolean;
constructor(plate: string, brand: string, color: string, isDefault: boolean) {
this.plate = plate; this.brand = brand; this.color = color; this.isDefault = isDefault;
}
}
VehicleItem 类描述了用户名下的车辆信息,包含四个字段:
- plate:车牌号,如"京A·8F6K9",是车辆的唯一标识。
- brand:品牌型号,如"特斯拉 Model 3",描述车辆的品牌和具体型号。
- color:车身颜色,如"珍珠白",是车辆的外观特征。
- isDefault:是否为默认车辆,布尔值,用于标记用户最常用的车辆。
VEHICLE_LIST 模拟数据包含四辆车,涵盖了特斯拉、比亚迪、蔚来、小鹏等主流新能源品牌,体现了智慧停车应用面向中高端车主的用户定位。其中第一辆车被设为默认车辆,这在实际应用中通常用于自动填充车牌、优先展示等场景。
6.3 OrderItem 订单模型
@Observed export class OrderItem {
parking: string; plate: string; duration: string; fee: number; status: string; time: string;
constructor(parking: string, plate: string, duration: string, fee: number, status: string, time: string) {
this.parking = parking; this.plate = plate; this.duration = duration;
this.fee = fee; this.status = status; this.time = time;
}
}
OrderItem 类描述了一笔停车订单的完整信息,包含六个字段:
- parking:停车场所,即订单关联的停车场名称。
- plate:车牌号,标识停车的车辆。
- duration:停车时长,如"2小时15分",以字符串形式存储,便于直接展示。
- fee:停车费用,单位为元,是订单的核心金额字段。
- status:订单状态,包括"停车中"“已结算”"已取消"三种。
- time:订单时间,如"08-31 14:25",记录订单的起始或完成时间。
ORDER_LIST 模拟了六条订单记录,时间跨度从 8 月 26 日到 8 月 31 日,涵盖了三种订单状态。特别值得注意的是"已取消"状态的订单费用为 0,在展示时会显示"¥0 已退款",这种细节处理体现了对用户体验的深度考量。
6.4 CouponItem 优惠券模型
@Observed export class CouponItem {
amount: number; desc: string; condition: string; expiry: string; claimed: boolean;
constructor(amount: number, desc: string, condition: string, expiry: string, claimed: boolean) {
this.amount = amount; this.desc = desc; this.condition = condition;
this.expiry = expiry; this.claimed = claimed;
}
}
CouponItem 类描述了一张优惠券的信息,包含五个字段:
- amount:优惠金额,单位为元,是优惠券最核心的价值表达。
- desc:优惠券描述,如"停车立减券""工作日停车券"等,说明优惠券的类型和用途。
- condition:使用条件,如"满20元可用""周一至周五可用"等,说明使用限制。
- expiry:有效期截止日期,格式为"YYYY-MM-DD"。
- claimed:是否已领取,布尔值,标记用户的领取状态。
COUPON_LIST 包含六张不同类型的优惠券,金额从 5 元到 50 元不等,使用条件各异,既有满减券也有时段券,既有日券也有月券,形成了丰富的优惠券产品矩阵。其中两张标记为已领取,用于展示"已领取"状态的视觉效果。
6.5 CardItem 月卡模型
@Observed export class CardItem {
balance: number; total: number; used: number; period: string;
constructor(balance: number, total: number, used: number, period: string) {
this.balance = balance; this.total = total; this.used = used; this.period = period;
}
}
CardItem 类描述了月卡/储值卡的使用情况,包含四个字段:
- balance:剩余额度,可以是剩余天数或剩余金额。
- total:总额度,与 balance 对应。
- used:已使用额度,用于计算使用进度。
- period:卡的类型名称,如"8月月卡"“储值卡”“2024年卡”。
CARD_LIST 包含三张卡:8月月卡(剩余 18 天/共 30 天)、储值卡(剩余 320 元/共 500 元)、2024年卡(剩余 180 天/共 365 天)。三种不同类型的卡展示了月卡模块的灵活性,既支持按天计费的月卡/年卡,也支持按金额计费的储值卡。
6.6 UserItem 用户模型
@Observed export class UserItem {
name: string; phone: string; member: string; points: number;
constructor(name: string, phone: string, member: string, points: number) {
this.name = name; this.phone = phone; this.member = member; this.points = points;
}
}
UserItem 类描述了用户的基本信息,包含四个字段:
- name:用户姓名。
- phone:手机号码,中间四位用星号脱敏处理,保护用户隐私。
- member:会员等级,如"钻石会员"“金牌会员”“银牌会员”。
- points:积分数量,反映用户的活跃度和消费累积。
USER_LIST 包含三位用户,分别对应钻石、金牌、银牌三个会员等级,积分从 8860 到 3180 递减。这种分级会员体系是 B 端产品常用的用户运营手段,通过等级权益差异激励用户持续消费。
六个数据模型各司其职,共同构成了智慧停车应用的核心业务数据层。每个模型都使用 @Observed 装饰器,确保数据变化能够被 ArkUI 框架精准捕获并触发视图更新,这是声明式开发中数据驱动视图的基础保障。
七、组件主体结构
了解了数据层之后,我们将目光转向视图层的核心——Page808 组件主体。这是整个页面的入口组件,由 @Entry 和 @Component 双重装饰器标记。
组件状态定义
@Entry
@Component
struct Page808 {
@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 parkingList: ParkingItem[] = PARKING_LIST;
@State vehicleList: VehicleItem[] = VEHICLE_LIST;
@State orderList: OrderItem[] = ORDER_LIST;
@State couponList: CouponItem[] = COUPON_LIST;
@State cardList: CardItem[] = CARD_LIST;
@State userList: UserItem[] = USER_LIST;
组件的状态变量可以分为四类:
导航状态:currentTab 记录当前选中的 Tab 索引,初始值为 0(车场 Tab)。这是整个页面最核心的状态变量,它决定了主体内容区展示哪个 Tab 的内容。
弹窗状态:addModal、editModal、delModal 三个布尔值分别控制添加、编辑、删除三个弹窗的显示与隐藏。editIdx 和 delIdx 则分别记录当前正在编辑或删除的车辆索引,初始值为 -1 表示没有选中项。这种"状态变量+索引变量"的组合模式,是列表项弹窗操作的经典实现方式。
动画状态:breath 是一个布尔值,配合 timer 定时器实现呼吸动画效果。每秒翻转一次布尔值,驱动相关 UI 元素的缩放动画,营造出"呼吸"般的韵律感。
业务数据状态:六个 @State 装饰的数组分别对应六个业务模型的列表数据。它们初始化为对应的模拟数据常量,同时因为使用了 @State 装饰,当数组内容发生变化(如添加车辆、删除车辆、领取优惠券)时,相关视图会自动更新。
生命周期函数
aboutToAppear() {
this.timer = setInterval(() => { this.breath = !this.breath; }, 1000);
}
aboutToDisappear() { clearInterval(this.timer); }
ArkUI 提供了丰富的组件生命周期回调。这里使用了两个关键的生命周期函数:
aboutToAppear:在组件即将出现时调用,这里启动了一个 1000 毫秒间隔的定时器,每秒翻转一次breath的值,从而驱动呼吸动画持续运行。aboutToDisappear:在组件即将消失时调用,这里清除了定时器,避免内存泄漏。这是一个非常重要的最佳实践——凡是在组件中创建的定时器、事件监听等资源,都必须在组件销毁时及时释放。
主体布局结构
build() {
Column() {
this.headerParking()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabParking()
} else if (this.currentTab === 1) {
this.tabVehicle()
} else if (this.currentTab === 2) {
this.tabOrder()
} else if (this.currentTab === 3) {
this.tabCoupon()
} else if (this.currentTab === 4) {
this.tabCard()
} 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() 方法是组件的核心渲染函数,它返回一个 Column 布局作为根容器,整个页面采用经典的"上中下"三段式结构:
顶部:headerParking() 头部区域,展示当前停车场信息、搜索栏和附近空位图表。头部下方有一条 Divider 分割线,将头部与内容区视觉上分隔开。
中部:Scroll 包裹的可滚动内容区,占据剩余空间(layoutWeight(1))。内容区内部又分为两部分:上半部分是根据 currentTab 动态切换的 Tab 内容,通过 if-else 条件渲染实现六个 Tab 的切换;下半部分是固定的 chartCard() 图表卡片,无论当前在哪个 Tab 都展示停车费用趋势图。
这种"Tab 内容 + 公共图表"的布局设计颇具巧思——图表作为跨场景的数据分析工具,不随 Tab 切换而消失,用户可以在任何场景下随时查看费用趋势,提升了数据的可达性。
底部:tabBar() 底部 Tab 栏,提供六个 Tab 的切换入口。
弹窗层:三个 if 条件渲染的弹窗组件,分别对应添加、编辑、删除三种操作弹窗。当对应的布尔状态为 true 时,弹窗才会被渲染到界面上。每个弹窗接收一个关闭回调函数,用于在弹窗内部关闭自身。
整个页面的背景色设置为深色背景 COLORS.bg,高度铺满整个屏幕。这种"深色背景 + 亮色卡片"的视觉层次,是 B 端深色主题的经典布局模式。
八、头部区域详解
头部区域是用户进入应用后最先看到的部分,承担着信息展示、搜索导航和数据概览等多重功能。当前页面的头部区域设计紧凑而富有层次,我们来逐一解析。
@Builder
headerParking() {
Column() {
Row() {
Stack() {
Text('🅿️')
.fontSize(30)
}
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor(COLORS.chip)
.scale(this.breath ? { x: 1.05, y: 1.05 } : { x: 1, y: 1 })
.animation({ duration: 800, curve: Curve.EaseInOut })
头部的第一行是当前停车场信息行,采用 Row 横向布局,从左到右依次是:停车场图标、停车场名称与车牌信息、停车状态标签。
最左侧的停车场图标是一个圆形的 Stack 容器,内含一个 30px 的停车 emoji。这个图标最大的亮点是呼吸动画效果:通过 .scale() 方法绑定 breath 状态,当 breath 为 true 时放大到 1.05 倍,为 false 时恢复原始大小。配合 .animation() 设置的 800 毫秒缓动动画,就形成了一种柔和的"呼吸"缩放效果。
呼吸动画在 B 端产品中的运用非常巧妙:它不像闪烁动画那样刺眼,也不像静态图标那样平淡,而是以一种温和的节奏吸引用户的注意力,传达出"当前正在进行中"的状态感。在停车场景下,这种呼吸效果恰好隐喻着车辆"在场、在位"的实时状态。
Column() {
Text('国贸中心停车场')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Row() {
Text('京A·8F6K9')
.fontSize(12)
.fontColor(COLORS.sub)
Text(' · ')
.fontSize(12)
.fontColor(COLORS.text3)
Text('已停 2小时15分')
.fontSize(12)
.fontColor(COLORS.sub)
}
.margin({ top: 4 })
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start)
图标右侧是停车场信息列,采用 Column 纵向布局。上方是 17px 粗体的停车场名称,使用最亮的标题色;下方是一行次要信息,包含车牌号和已停车时长,用 12px 的次要文字色展示,中间用点号分隔。
这种"主标题+副标题"的信息层级设计,让用户一眼就能抓住核心信息(当前所在停车场),同时也能快速获取辅助信息(哪辆车、停了多久)。信息的优先级通过字号、字重和颜色三个维度共同区分,视觉层次非常清晰。
Blank()
Text('停车中')
.fontSize(11)
.fontColor(COLORS.card)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
.backgroundColor(COLORS.blue)
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
最右侧是"停车中"状态标签,一个蓝色背景的圆角胶囊按钮。标签文字使用卡片色(深色),在蓝色背景上形成鲜明对比。Blank() 组件将状态标签推到最右侧,实现了两端对齐的布局效果。
Row() {
Text('🔍').fontSize(16)
Text('搜索车场 / 车牌 / 订单')
.fontSize(13)
.fontColor(COLORS.text3)
.margin({ left: 8 })
.layoutWeight(1)
Text('🎚️').fontSize(16)
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.margin({ left: 16, right: 16 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
头部的第二行是搜索栏。这是一个典型的"搜索图标 + 占位文字 + 筛选图标"的搜索框设计。搜索框的背景色使用了页面底色 COLORS.bg,在卡片背景上形成了"凹陷"的视觉效果,暗示这是一个可输入的区域。20px 的大圆角让搜索框看起来更加圆润友好。
值得注意的是搜索提示文案"搜索车场 / 车牌 / 订单",它明确告知用户搜索功能支持的范围,降低了用户的认知负担。右侧的筛选图标(🎚️)则暗示了搜索还支持高级筛选功能。
Row() {
Text('📍 附近空位')
.fontSize(12)
.fontColor(COLORS.sub)
Blank()
Row() {
ForEach(MONTH_IDX, (idx: number) => {
Column() {
Column()
.width(10)
.height(NEAR_AVAIL[idx] * 0.3)
.backgroundColor(COLORS.green)
.borderRadius(5)
Text(NEAR_NAME[idx])
.fontSize(8)
.fontColor(COLORS.text3)
.margin({ top: 4 })
}
.margin({ right: 8 })
.alignItems(HorizontalAlign.Center)
})
}
.alignItems(VerticalAlign.Bottom)
.height(50)
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 12 })
.alignItems(VerticalAlign.Center)
}
.backgroundColor(COLORS.card)
}
头部的第三行是"附近空位"迷你柱状图,这是整个头部最具特色的设计。左侧是标题文字,右侧是一个由六个绿色小柱子组成的迷你柱状图,每个柱子代表一个商圈的空余车位数量。
柱子的高度通过 NEAR_AVAIL[idx] * 0.3 动态计算,空余车位越多,柱子越高。这种数据可视化的呈现方式,比单纯的数字列表更加直观,用户一眼就能看出哪个商圈车位最充足。
整个头部区域被包裹在一个 Column 中,背景色为卡片色 COLORS.card,与页面底色形成对比。三行内容从上到下依次排列,间距合理,信息密度高但不拥挤,展现了 B 端产品头部区域的典型设计范式。
九、车场Tab——横滑大卡
作为六个 Tab 中的第一个,车场 Tab 承载着"找车位"这一核心用户需求。它采用了横向滑动的大卡片布局,在有限的屏幕宽度内展示更多的停车场信息。
@Builder
tabParking() {
Column() {
Row() {
Text('🅿️ 附近车场').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('列表 ›').fontSize(13).fontColor(COLORS.blue)
}
.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })
车场 Tab 的标题栏采用了经典的"左标题+右操作"布局。左侧是带 emoji 图标的"附近车场"标题,右侧是蓝色的"列表 ›"文字链接,暗示用户可以点击进入完整的列表视图。这种"概览卡片 + 更多入口"的设计模式,在移动端 B 端产品中非常常见——既在首页提供了核心信息的快速预览,又为需要更多信息的用户保留了深入探索的入口。
Scroll() {
Row() {
ForEach(this.parkingList, (item: ParkingItem) => {
Column() {
Stack() { Text(item.icon).fontSize(40) }
.width('100%').height(70).borderRadius(12).backgroundColor(COLORS.chip)
.alignContent(Alignment.Center)
Text(item.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 8 })
Text(item.address).fontSize(11).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 2 })
核心的横滑卡片区域使用 Scroll 组件包裹,设置 scrollDirection(ScrollDirection.Horizontal) 实现横向滚动。内部是一个 Row 横向排列多个卡片。
每张卡片是一个固定宽度为 180px 的 Column,从上到下包含以下元素:
- 车场图标区:顶部是一个 70px 高的图标区域,背景色为 chip 色,中间是 40px 的 emoji 图标。大尺寸的图标让每张卡片都有强烈的视觉识别度。
- 车场名称:15px 粗体标题,单行显示,超出部分用省略号截断。
- 车场地址:11px 次要文字,同样单行省略。
Row() {
Text('空位 ').fontSize(11).fontColor(COLORS.sub)
Text(item.available.toString()).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.green)
Text(' / ' + item.total.toString()).fontSize(11).fontColor(COLORS.text3)
}.margin({ top: 8 })
Row() {
Column().layoutWeight(item.available).height(6).backgroundColor(COLORS.green).borderRadius(3)
Column().layoutWeight(item.total - item.available).height(6).backgroundColor(COLORS.line).borderRadius(3)
}.width('100%').margin({ top: 6 })
空位信息是卡片的核心数据区。上方用"空位 X / 总数 Y"的格式展示,其中可用车位数量用 15px 粗体绿色突出显示,形成视觉焦点。下方是一个进度条样式的可视化呈现,通过两个 Column 配合 layoutWeight 实现比例分配:绿色部分代表已空位,灰色部分代表已占用。
这种"数字+进度条"的双重呈现方式,既满足了精确数据的需求,又提供了直观的比例感知,是数据展示的最佳实践之一。
Row() {
Text('¥' + item.price.toString() + '/时').fontSize(12).fontColor(COLORS.blue)
Blank()
Text(item.distance.toString() + 'km').fontSize(11).fontColor(COLORS.text3)
}.width('100%').margin({ top: 8 })
Text('🧭 导航前往').fontSize(12).fontColor(COLORS.card).textAlign(TextAlign.Center)
.width('100%').padding({ top: 6, bottom: 6 }).backgroundColor(COLORS.blue)
.borderRadius(10).margin({ top: 8 })
}
.width(180).padding(12).margin({ right: 12 }).backgroundColor(COLORS.card).borderRadius(16)
}, (item: ParkingItem) => item.name)
}.padding({ left: 16, right: 16 })
}
.scrollBar(BarState.Off).scrollDirection(ScrollDirection.Horizontal).width('100%')
卡片底部是价格信息和导航按钮。价格(蓝色)和距离(灰色)分别位于左右两侧,形成信息对称。最底部是一个全宽的蓝色"导航前往"按钮,白色文字居中,是卡片的主要操作入口。
每张卡片的圆角为 16px,内边距 12px,右侧有 12px 的间距,整体视觉风格圆润而现代。卡片与卡片之间的间距保证了横滑时的视觉呼吸感,不会显得过于拥挤。
Row() {
ForEach(QUICK_ENTRIES, (item: string) => {
Text(item).fontSize(12).fontColor(COLORS.blue)
.padding({ left: 10, right: 10, top: 6, bottom: 6 }).backgroundColor(COLORS.chip)
.borderRadius(16).margin({ right: 8, top: 8 })
})
}.width('100%').padding({ left: 16, right: 16, bottom: 16 })
}.width('100%')
}
车场 Tab 的底部是四个快捷入口标签,它们像 chip 一样横向排列,chip 色背景、蓝色文字、16px 圆角。这四个快捷入口(找车位、我的车辆、领券、续月卡)是用户最高频的操作,将其放在车场 Tab 的底部,能够有效缩短用户的操作路径。
横滑大卡的布局模式非常适合车场这种"信息密度高、需要对比选择"的场景。用户可以通过横向滑动快速浏览多个停车场,每张卡片都是一个完整的信息单元,包含了用户决策所需的全部信息:位置、价格、空位、距离。这种设计既保证了信息的完整性,又提供了流畅的交互体验。
十、车辆Tab——图标清单
车辆 Tab 以清单形式展示用户名下的所有车辆,支持添加、编辑、删除和设为默认等操作,是车辆管理的核心界面。
@Builder
tabVehicle() {
Column() {
Row() {
Text('🚗 我的车辆').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('+ 添加车辆').fontSize(13).fontColor(COLORS.green).onClick(() => { this.addModal = true; })
}.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })
车辆 Tab 的标题栏与车场 Tab 结构类似,但右侧的操作按钮变成了绿色的"+ 添加车辆"。点击该按钮会将 addModal 状态设为 true,从而弹出添加车辆的弹窗。这里用绿色而非蓝色,暗示"添加"是一个正向操作,符合绿色代表"成功/新增"的语义惯例。
ForEach(this.vehicleList, (item: VehicleItem, idx: number) => {
Row() {
Stack() { Text('🚗').fontSize(22) }
.width(48).height(48).borderRadius(24).backgroundColor(COLORS.chip).alignContent(Alignment.Center)
Column() {
Text(item.plate).fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Row() {
Text(item.brand).fontSize(12).fontColor(COLORS.sub)
Text(' · ').fontSize(12).fontColor(COLORS.text3)
Text(item.color).fontSize(12).fontColor(COLORS.sub)
}.margin({ top: 4 })
}.margin({ left: 12 }).alignItems(HorizontalAlign.Start).layoutWeight(1)
车辆列表使用 ForEach 循环渲染,每一行是一个 Row 横向布局的车辆卡片。最左侧是一个 48px 的圆形图标容器,内含汽车 emoji。中间是车辆信息列:上方是粗体的车牌号(主信息),下方是品牌和颜色(次信息)。layoutWeight(1) 让信息列占据剩余空间,确保右侧操作按钮有固定位置。
Text(item.isDefault ? '默认' : '设为默认').fontSize(11)
.fontColor(item.isDefault ? COLORS.card : COLORS.text3)
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(12)
.backgroundColor(item.isDefault ? COLORS.green : COLORS.chip)
.onClick(() => {
for (let i = 0; i < this.vehicleList.length; i++) {
this.vehicleList[i].isDefault = (i === idx);
}
})
"设为默认"按钮是车辆列表中的核心交互元素。它的外观会根据当前车辆是否为默认车辆而变化:如果是默认车辆,显示绿色背景、深色文字的"默认"标签;如果不是默认车辆,显示 chip 色背景、灰色文字的"设为默认"标签。
点击事件的处理逻辑非常经典:通过一个 for 循环遍历所有车辆,将当前点击的车辆设为默认(isDefault = true),其余车辆设为非默认(isDefault = false)。这种"排他性"的设置逻辑确保了始终只有一辆默认车辆,符合业务规则。
由于 vehicleList 是 @State 装饰的状态变量,且 VehicleItem 类被 @Observed 装饰,所以当 isDefault 属性发生变化时,界面会自动更新——被点击的车辆按钮变成绿色"默认"样式,其他车辆按钮恢复为灰色"设为默认"样式。这就是声明式开发的魅力:开发者只需要关心数据状态的改变,而无需手动操作 DOM 更新视图。
Text('✏️').fontSize(16).margin({ left: 8 }).onClick(() => {
this.editIdx = idx; this.editModal = true;
})
Text('🗑️').fontSize(16).margin({ left: 8 }).onClick(() => {
this.delIdx = idx; this.delModal = true;
})
}
.width('100%').padding(16).margin({ left: 16, right: 16, bottom: 8 })
.backgroundColor(COLORS.card).borderRadius(16)
}, (item: VehicleItem) => item.plate)
}.width('100%').padding({ bottom: 16 })
}
最右侧是编辑(✏️)和删除(🗑️)两个操作图标。点击编辑图标时,会记录当前索引到 editIdx 并打开编辑弹窗;点击删除图标时,记录索引到 delIdx 并打开删除确认弹窗。
这种"先记录索引、再打开弹窗"的模式是列表项操作的标准实现方式。弹窗组件通过读取对应的索引变量来获取当前操作的数据项,从而实现弹窗内容与列表项的联动。
车辆 Tab 的整体风格简洁实用,每一行都是一个完整的车辆信息单元,左侧图标、中间信息、右侧操作的三段式布局清晰明了。操作按钮使用 emoji 图标而非文字,既节省了空间,又保持了视觉的轻快感。
十一、订单Tab——状态时间轴
订单 Tab 采用了时间轴的视觉呈现方式,将停车订单按时间顺序排列,通过左侧的时间轴线和状态圆点直观地展示订单的状态和进程。
@Builder
tabOrder() {
Column() {
Row() {
Text('📋 停车订单').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('全部 ›').fontSize(13).fontColor(COLORS.blue)
}.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })
订单 Tab 的标题栏延续了统一的设计风格,左侧是"📋 停车订单"标题,右侧是蓝色的"全部 ›"入口。
ForEach(this.orderList, (item: OrderItem, idx: number) => {
Row() {
Column() {
Column().width(2).height(idx === 0 ? 0 : 24).backgroundColor(COLORS.line)
Stack() { Text('●').fontSize(14).fontColor(orderColor(item.status)) }
.width(24).height(24).borderRadius(12).backgroundColor(COLORS.card).alignContent(Alignment.Center)
Column().width(2).height(idx === this.orderList.length - 1 ? 0 : 24).backgroundColor(COLORS.line)
}.alignItems(HorizontalAlign.Center)
时间轴的核心在于左侧的轴线构造。每个订单项的左侧是一个 Column,包含三个部分:
- 上连接线:一根 2px 宽、24px 高的竖线。第一条记录的上连接线高度为 0,因为它是时间轴的起点。
- 状态圆点:一个 24px 的圆形
Stack,内含一个圆点符号(●)。圆点的颜色通过orderColor(item.status)动态计算,不同的订单状态显示不同的颜色。 - 下连接线:同样是 2px 宽、24px 高的竖线。最后一条记录的下连接线高度为 0,因为它是时间轴的终点。
三根元素垂直排列,中间的圆形圆点将上下连接线"断开",形成了时间轴的经典视觉效果。圆点的颜色随状态变化(蓝/绿/红),让用户一眼就能识别订单的状态,这是时间轴设计中"状态可视化"的关键手法。
Column() {
Row() {
Text(item.parking).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(orderColor(item.status))
}.width('100%')
右侧的订单信息列分为三行。第一行是停车场名称和状态标签的组合:停车场名称左对齐,占据主要空间;状态标签右对齐,使用与状态圆点相同的背景色(通过 orderColor 函数获取),白色文字。这种"圆点+标签"的双色对应设计,强化了状态的视觉一致性。
Row() {
Text(item.plate).fontSize(12).fontColor(COLORS.sub)
Text(' · ').fontSize(12).fontColor(COLORS.text3)
Text(item.time).fontSize(12).fontColor(COLORS.sub)
}.margin({ top: 4 })
第二行是车牌号和订单时间,使用次要文字色展示,中间用点号分隔。这行信息提供了订单的辅助上下文。
Row() {
Text('时长 ' + item.duration).fontSize(12).fontColor(COLORS.sub)
Blank()
Text(item.status === '已取消' ? '¥0 已退款' : '¥' + item.fee.toString())
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(orderColor(item.status))
}.width('100%').margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12, top: 4, bottom: 4 })
}.width('100%').padding({ left: 16, right: 16 })
}, (item: OrderItem) => item.parking + item.time)
}.width('100%').padding({ bottom: 16 })
}
第三行是停车时长和费用。左侧是停车时长,右侧是费用金额。费用金额使用粗体展示,颜色同样通过 orderColor 函数与状态保持一致。
特别值得注意的是对"已取消"订单的处理:当订单状态为"已取消"时,费用显示为"¥0 已退款"而非具体金额。这种细节处理体现了产品的人性化关怀——已取消的订单不产生实际费用,用"已退款"的明确表述避免用户困惑。
时间轴布局非常适合订单、日志等具有时间顺序和状态流转的业务场景。它通过视觉化的时间线,将抽象的时间概念转化为具象的空间概念,让用户能够直观地感知订单的时间顺序和状态变化。同时,状态颜色的一致性(圆点+标签+金额同色)进一步强化了信息的结构化表达。
十二、优惠Tab——虚线大卡
优惠 Tab 以优惠券卡片的形式展示可用的停车优惠券,采用经典的"左右分栏+虚线分割"的券卡设计风格。
@Builder
tabCoupon() {
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 的标题栏有一个微妙的变化:右侧的"我的券包 ›"文字使用了橙色而非蓝色。这是因为优惠券本身是营销属性的功能,用橙色这种更具活力和吸引力的颜色,能够更好地激发用户的领取欲望。这也是色彩体系中 orange 功能色的典型应用场景。
ForEach(this.couponList, (item: CouponItem, idx: number) => {
Row() {
Column() {
Row() {
Text('¥').fontSize(14).fontColor(couponColor(item.claimed)).margin({ bottom: 12 })
Text(item.amount.toString()).fontSize(40).fontWeight(FontWeight.Bold)
.fontColor(couponColor(item.claimed))
}.alignItems(VerticalAlign.Bottom)
Text('立减').fontSize(10).fontColor(couponColor(item.claimed))
}.width(100).alignItems(HorizontalAlign.Center)
每张优惠券卡片采用左右分栏的 Row 布局。左侧是金额区,固定宽度 100px,居中展示优惠金额。金额区包含三行元素:
- 金额数字:40px 粗体大字,是整个卡片最醒目的元素。金额数字前面有一个 14px 的"¥"符号,底部对齐,形成"小字+大字"的排版层次。
- "立减"文字:10px 的小字,标注金额的性质。
整个金额区的颜色都通过 couponColor(item.claimed) 动态计算:未领取的优惠券显示橙色,已领取的显示灰色。这种整体变色的处理方式,让"已领取"状态的视觉弱化效果非常明显。
Column() {
ForEach(DASH_IDX, (d: number) => {
Column().width(1).height(4).backgroundColor(COLORS.line).margin({ bottom: 2 })
})
}.width(1).height(80).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
中间的虚线分割线是券卡设计的标志性元素。这里的实现方式非常巧妙:用 ForEach 循环 DASH_IDX 数组(包含 10 个元素),渲染 10 个 1px 宽、4px 高的小方块,每个方块底部有 2px 的间距。10 个方块纵向排列,就形成了由 10 段短线组成的虚线效果。
这种"用循环渲染小方块模拟虚线"的实现方案虽然略显"原始",但却有其独特的优势:它不依赖边框的 dashed 样式属性,因此在不同平台、不同设备上的表现完全一致,具有更好的可控性和可预测性。同时,虚线的长度、间距、粗细都可以通过调整参数精确控制,设计自由度更高。
Column() {
Text(item.desc).fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(item.claimed ? COLORS.text3 : COLORS.title)
Text(item.condition).fontSize(12).fontColor(COLORS.sub).margin({ top: 4 })
Row() {
Text('有效期至 ' + item.expiry).fontSize(10).fontColor(COLORS.text3)
Blank()
Text(item.claimed ? '已领取' : '立即领取').fontSize(11)
.fontColor(item.claimed ? COLORS.text3 : COLORS.card)
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
.backgroundColor(couponColor(item.claimed))
.onClick(() => { if (!item.claimed) { this.couponList[idx].claimed = true; } })
}.width('100%').margin({ top: 8 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12, right: 12 })
右侧是优惠券的描述和操作区,占据剩余空间。从上到下依次是:
- 优惠券名称:15px 粗体,未领取时用标题色(亮色),已领取时变灰。
- 使用条件:12px 次要文字,说明使用限制。
- 有效期和领取按钮:底部一行,左侧是有效期信息,右侧是领取按钮。
领取按钮的交互逻辑简洁明了:点击"立即领取"按钮时,先判断 item.claimed 是否为 false(防止重复点击),如果未领取,则将对应优惠券的 claimed 属性设为 true。由于 CouponItem 是 @Observed 装饰的类,属性变化会自动触发视图更新——按钮变成"已领取"、整体颜色变灰、金额文字变灰,整张优惠券呈现出"已使用"的视觉状态。
}
.width('100%').height(96).padding({ left: 8, right: 8 })
.margin({ left: 16, right: 16, bottom: 8 }).backgroundColor(COLORS.card).borderRadius(16)
}, (item: CouponItem) => item.desc + item.expiry)
}.width('100%').padding({ bottom: 16 })
}
整张优惠券卡片固定高度 96px,16px 圆角,卡片背景色。左右两栏通过虚线分割,形成了经典的"券票"视觉隐喻——用户一眼就能识别这是一张优惠券。
虚线大卡的设计是营销类页面的经典范式。左侧大金额数字抓住眼球,中间虚线分割强化券卡形态,右侧描述和操作按钮提供完整信息和交互。整套设计既符合用户对"优惠券"的认知习惯,又通过深色主题和精致的细节处理,保持了 B 端产品的专业感。
十三、月卡Tab——渐变大数字卡
月卡 Tab 以渐变色彩和大数字为核心视觉元素,展示用户的月卡、储值卡、年卡等卡类服务的使用情况。
@Builder
tabCard() {
Column() {
Row() {
Text('📅 月卡服务').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Blank()
Text('续费 ›').fontSize(13).fontColor(COLORS.green)
}.width('100%').padding({ left: 16, right: 16, top: 16, bottom: 8 })
月卡 Tab 的标题栏右侧是绿色的"续费 ›“入口,绿色寓意"持续、增值”,与续费的语义相符。
ForEach(this.cardList, (item: CardItem, idx: number) => {
Stack() {
Column() {
Text(item.period).fontSize(12).fontColor(COLORS.sub)
Row() {
Text(item.balance.toString()).fontSize(56).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(idx === 1 ? ' 元' : ' 天').fontSize(16).fontColor(COLORS.sub).margin({ bottom: 10 })
}.alignItems(VerticalAlign.Bottom).margin({ top: 8 })
月卡卡片的核心是大数字的余额展示。每张卡片的最上方是卡类型名称(如"8月月卡"“储值卡”),下方是巨大的余额数字——56px 粗体白色文字,是整个界面中字号最大的元素之一。数字后面跟着单位,单位文字只有 16px,底部对齐,形成强烈的大小对比。
单位的判断逻辑值得关注:idx === 1 ? ' 元' : ' 天'。也就是说,第二张卡(储值卡)的单位是"元",其他卡(月卡、年卡)的单位是"天"。这种设计反映了月卡模块支持两种计费模式——按天计费的周期卡和按金额计费的储值卡。
Row() {
Text('已用 ' + item.used.toString()).fontSize(11).fontColor(COLORS.sub)
Text(' / 总额 ' + item.total.toString()).fontSize(11).fontColor(COLORS.sub)
}.margin({ top: 4 })
Row() {
Column().layoutWeight(item.used).height(8).borderRadius(4).backgroundColor(COLORS.title)
Column().layoutWeight(item.total - item.used).height(8).borderRadius(4).backgroundColor(COLORS.line)
}.width('100%').margin({ top: 8 })
大数字下方是使用进度的双重展示:文字形式的"已用 X / 总额 Y"和进度条形式的可视化呈现。进度条同样使用 layoutWeight 实现比例分配,已用部分用亮色(标题色)填充,未用部分用灰色线条色填充。
Row() {
Text('立即续费').fontSize(12).fontColor(idx % 2 === 0 ? COLORS.blue : COLORS.green)
.padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14).backgroundColor(COLORS.title)
Blank()
Text('查看明细 ›').fontSize(11).fontColor(COLORS.sub)
}.width('100%').margin({ top: 12 })
}.width('100%').alignItems(HorizontalAlign.Center)
}
卡片底部是操作区域:左侧是"立即续费"按钮,右侧是"查看明细 ›"文字链接。续费按钮采用反色设计——白色背景、彩色文字,文字颜色根据卡片索引奇偶性选择蓝色或绿色。
.width('100%').padding(20).margin({ left: 16, right: 16, bottom: 8 }).borderRadius(16)
.linearGradient({
angle: 135,
colors: idx % 2 === 0
? [[COLORS.blue, 0.0], [COLORS.blueD, 1.0]]
: [[COLORS.green, 0.0], [COLORS.greenD, 1.0]]
})
}, (item: CardItem) => item.period)
}.width('100%').padding({ bottom: 16 })
}
月卡卡片最具特色的是渐变背景。通过 .linearGradient() 方法设置了 135 度角的线性渐变,偶数索引的卡片使用蓝色系渐变(从 #3B82F6 到 #2563EB),奇数索引的卡片使用绿色系渐变(从 #22C55E 到 #16A34A)。
135 度的渐变角度意味着颜色从左上角向右下角过渡,这种角度的渐变在视觉上更加自然流畅,避免了水平或垂直渐变的单调感。每种颜色都使用了"浅色到深色"的色阶组合,渐变效果柔和而富有层次。
渐变大数字卡的设计风格是金融类、会员类产品的常见手法。大数字传递"额度感",渐变色传递"价值感",两者结合营造出一种"尊贵、有价值"的视觉氛围。同时,蓝绿交替的渐变配色也避免了多张卡片排列时的视觉单调。
十四、我的Tab——中心大卡
"我的"Tab 是用户的个人中心,以用户头像为中心,向下辐射出会员信息、功能入口和用户列表等内容,整体呈现出"中心聚焦"的视觉格局。
@Builder
tabMine() {
Column() {
Stack() {
Column() { Text('🧑').fontSize(48) }
.width(100).height(100).borderRadius(50).backgroundColor(COLORS.chip)
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.scale(this.breath ? { x: 1.03, y: 1.03 } : { x: 1, y: 1 })
.animation({ duration: 1000, curve: Curve.EaseInOut })
}.width('100%').height(120).alignContent(Alignment.Center).margin({ top: 24 })
"我的"Tab 最上方是用户头像区域。一个 100px 的圆形头像容器,内含 48px 的用户 emoji。头像同样应用了呼吸动画,但与头部图标的呼吸动画有所不同:
- 缩放比例:头像的缩放比例是 1.03 倍,比头部图标的 1.05 倍略小,因为头像尺寸更大,过大的缩放会显得夸张。
- 动画时长:头像的动画时长是 1000ms,比头部的 800ms 稍慢,节奏更加舒缓。
这种"同一动画、不同参数"的细节处理,体现了对视觉节奏的精细把控。不同大小、不同位置的元素,呼吸的"频率"和"幅度"应该有所区别,才能营造出自然和谐的整体效果。
Text('石天雄').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.title).margin({ top: 12 })
Row() {
Text('钻石会员').fontSize(12).fontColor(COLORS.card)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10).backgroundColor(COLORS.blue)
Text(' · ').fontSize(13).fontColor(COLORS.text3)
Text('8860 积分').fontSize(13).fontColor(COLORS.green)
}.margin({ top: 6 })
头像下方是用户姓名和会员信息。用户名使用 22px 粗体,是整个页面中最大的文字之一,凸显了"我的"页面的个人属性。
会员信息行包含三个元素:蓝色背景的"钻石会员"标签(钻石会员是最高等级,用主题蓝色彰显尊贵感)、分隔点号、绿色的积分数字(积分用绿色寓意"增长、累积")。
Row() {
ForEach(MINE_FUNCS, (func: string[]) => {
Column() {
Stack() { Text(func[0]).fontSize(24) }
.width(48).height(48).borderRadius(24).backgroundColor(COLORS.chip).alignContent(Alignment.Center)
Text(func[1]).fontSize(11).fontColor(COLORS.sub).margin({ top: 6 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}.width('100%').padding({ left: 16, right: 16 }).margin({ top: 24 })
功能入口区横向排列了四个功能按钮,每个按钮都是"上方图标+下方文字"的纵向布局,使用 layoutWeight(1) 平均分配宽度。四个功能分别是:我的车场、我的车辆、支付方式、在线客服。
这种"宫格式"功能入口布局是个人中心页面的经典设计模式,它将高频功能以可视化的方式呈现给用户,入口清晰、操作便捷。
ForEach(this.userList, (item: UserItem, idx: number) => {
Row() {
Stack() { Text(idx === 0 ? '🧑' : (idx === 1 ? '👨' : '👩')).fontSize(20) }
.width(36).height(36).borderRadius(18).backgroundColor(COLORS.chip).alignContent(Alignment.Center)
Column() {
Text(item.name + ' · ' + item.member).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(item.phone).fontSize(11).fontColor(COLORS.sub).margin({ top: 2 })
}.margin({ left: 8 }).alignItems(HorizontalAlign.Start).layoutWeight(1)
Blank()
Text(item.points.toString() + ' 分').fontSize(13).fontColor(COLORS.green)
}.width('100%').padding(12).margin({ left: 16, right: 16, top: 8 })
.backgroundColor(COLORS.card).borderRadius(12)
}, (item: UserItem) => item.name)
}.width('100%').padding({ bottom: 16 }).alignItems(HorizontalAlign.Center)
}
页面底部是用户列表,展示了三位不同等级的用户信息。每条记录采用左中右三段式布局:左侧头像、中间姓名会员等级和手机号、右侧积分数。
一个有趣的细节是头像的差异化:第一位用户用 🧑 图标,第二位用 👨 图标,第三位用 👩 图标,通过不同的 emoji 暗示用户的性别差异。虽然这只是模拟数据的细节处理,但这种"列表项差异化"的设计思维值得借鉴——在真实产品中,可以根据用户的性别、头像、等级等属性,让列表中的每一项都有独特的视觉标识,提升界面的丰富度和识别效率。
"我的"Tab 的整体布局呈现出"上宽下紧"的视觉节奏:顶部是大尺寸的头像和用户名,空间留白较多;中部是四个功能入口,布局紧凑;底部是用户列表,信息密度最高。这种"由疏到密"的布局节奏,引导用户的视线从上到下自然流动,符合视觉设计的"瀑布流"原则。
十五、图表卡片
图表卡片是六个 Tab 共享的公共组件,位于 Tab 内容区的下方,展示近 6 个月的停车费用趋势。它是用户进行消费复盘和费用管理的重要数据参考。
@Builder
chartCard() {
Column() {
Row() {
Text('📊 近6月停车费用')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
Blank()
Text('单位: 元')
.fontSize(11)
.fontColor(COLORS.text3)
}
.width('100%')
图表卡片的标题栏左侧是"📊 近6月停车费用"标题,右侧是"单位: 元"的单位说明。单位说明使用最小号的辅助文字色,既提供了必要的信息,又不会干扰主标题的视觉焦点。
Row() {
ForEach(MONTH_IDX, (idx: number) => {
Column() {
Text(PARK_FEE_VAL[idx].toString())
.fontSize(10)
.fontColor(COLORS.sub)
.margin({ bottom: 4 })
Column()
.width(24)
.height((PARK_FEE_VAL[idx] - 100) * 0.8)
.linearGradient({
angle: 90,
colors: [[COLORS.blue, 0.0], [COLORS.green, 1.0]]
})
.borderRadius(12)
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)
}
图表主体是一个横向排列的柱状图,使用 Row 包裹六个柱子,每个柱子占据等宽的 layoutWeight(1)。每个柱子是一个 Column,包含三个部分:
- 数值标签:柱子顶部显示具体的费用数值,10px 次要文字色。
- 柱状图主体:24px 宽的柱子,高度通过
(PARK_FEE_VAL[idx] - 100) * 0.8计算。这里减去 100 是为了让数值差异更加明显——如果所有数值都在 100 以上,直接用数值作为高度会导致柱子普遍偏高,差异不明显。乘以 0.8 是缩放系数,将数值映射到合适的像素高度范围。 - 月份标签:柱子底部显示月份名称,10px 辅助文字色。
柱子的视觉效果是整个图表最大的亮点:它使用了 90 度角的蓝绿渐变色。90 度角意味着颜色从左到右水平渐变,左侧是科技蓝(#3B82F6),右侧是信号绿(#22C55E)。这种蓝绿渐变的配色方案与产品的双色主题高度一致,同时也赋予了柱状图更加丰富的视觉层次。
柱子的圆角设置为 12px,也就是柱子宽度(24px)的一半。这意味着柱子的顶部和底部都是完全的半圆形,形成了"胶囊"形状的柱状图。这种圆润的柱状图风格比传统的直角柱子更加柔和友好,符合现代 B 端产品的设计审美。
.width('100%')
.padding(16)
.margin({ left: 16, right: 16, bottom: 16 })
.backgroundColor(COLORS.card)
.borderRadius(16)
}
整个图表卡片被包裹在一个 16px 圆角的卡片容器中,卡片色背景,四周有 16px 的内边距。与其他卡片保持一致的视觉风格,确保了整体界面的统一性。
图表卡片的设计虽然简洁,却蕴含了丰富的设计考量:数据与标签的位置关系、渐变色的方向选择、圆角的大小比例、高度的计算方式……每一个细节都经过精心调校,才能在不依赖专业图表库的情况下,实现如此精致的数据可视化效果。这也展示了 ArkUI 声明式布局的强大表现力——通过基础组件的灵活组合,可以构建出各种复杂的自定义 UI。
十六、底部Tab栏
底部 Tab 栏是整个页面的导航枢纽,它承载着六个业务场景之间的切换功能,是用户最高频交互的区域之一。
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column() {
Image(tab.icon).width(22).height(22)
.fillColor(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(56).backgroundColor(COLORS.card).padding({ top: 8, bottom: 8 })
}
底部 Tab 栏的实现简洁而高效。外层是一个 56px 高的 Row,卡片色背景,上下各 8px 的内边距。内部通过 ForEach 循环 TAB_LIST 数组,渲染六个 Tab 项。
每个 Tab 项是一个纵向布局的 Column,包含图标和文字两部分:
- 图标:22x22px 的
Image组件,通过.fillColor()方法设置图标颜色。选中状态使用主题蓝色,未选中状态使用辅助灰色。 - 文字:10px 的 Tab 名称,颜色规则与图标一致——选中蓝色,未选中灰色。
每个 Tab 项使用 layoutWeight(1) 平均分配宽度,确保六个 Tab 在底部栏中等宽排列。点击事件的处理非常直接:将 currentTab 设为当前索引值即可。
整个 Tab 栏的设计遵循了"少即是多"的原则:
- 视觉简洁:只有图标和文字两个元素,没有多余的装饰线条或背景色块。
- 状态明确:选中与未选中状态通过颜色差异清晰区分,蓝色表示激活,灰色表示未激活。
- 操作便捷:每个 Tab 的点击区域占据整个等宽列,即使用户点击文字或图标之外的区域也能触发切换,降低了点击的精准度要求。
- 数据驱动:Tab 的数量和内容完全由
TAB_LIST数组决定,新增或修改 Tab 只需调整数组数据,无需改动布局代码。
底部 Tab 栏作为整个页面的导航核心,其设计的好坏直接影响用户的使用体验。当前的实现方案在视觉简洁性、操作便捷性和代码可维护性之间取得了很好的平衡,是移动端 Tab 栏设计的优秀范例。
十七、弹窗系统
弹窗是移动端交互中不可或缺的组件,用于承载需要用户特别关注的临时性操作或确认信息。当前源码实现了三个功能弹窗:添加车辆、编辑车辆和删除确认,它们共同构成了完整的车辆管理弹窗体系。
弹窗遮罩层
@Builder
modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
在介绍具体弹窗之前,先来看看 modalOverlay 这个通用的遮罩层组件。它是一个铺满全屏的 Column,背景色为半透明黑色(rgba(0,0,0,0.6)),点击遮罩层会触发传入的 onClose 回调关闭弹窗。
将遮罩层抽离为独立的 @Builder 函数是一个很好的代码复用实践。三个弹窗都需要遮罩层,如果每个弹窗都写一遍遮罩层代码,不仅代码冗余,而且如果后续需要修改遮罩层的样式或交互,需要在三个地方同步修改,容易出错。抽离之后,只需要维护一份代码,三个弹窗共享使用。
添加车辆弹窗
@Builder
panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('🚗 添加车辆')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
添加车辆弹窗使用 Stack 布局实现居中效果——底层是遮罩层,上层是居中的弹窗内容。弹窗内容是一个宽度为 80% 的 Column 卡片,卡片色背景,20px 圆角,20px 内边距。
弹窗标题为"🚗 添加车辆",18px 粗体白色文字,醒目明确。
Text('车牌号')
.fontSize(13)
.fontColor(COLORS.sub)
.margin({ top: 16 })
Text('京A·XXXXX')
.fontSize(14)
.fontColor(COLORS.text3)
.padding(12)
.backgroundColor(COLORS.bg)
.borderRadius(8)
.width('100%')
.margin({ top: 4 })
车牌号输入区域采用"标签+输入框"的经典布局。标签在上,13px 次要文字色;输入框在下,深色背景(比卡片更深,形成凹陷感),8px 圆角,12px 内边距。这里用 Text 组件模拟输入框的视觉效果,在实际项目中可以替换为 TextInput 组件。
Text('品牌型号')
.fontSize(13)
.fontColor(COLORS.sub)
.margin({ top: 12 })
Row() {
ForEach(VEH_BRANDS, (b: string) => {
Text(b)
.fontSize(11)
.fontColor(COLORS.sub)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(COLORS.chip)
.borderRadius(10)
.margin({ right: 6 })
})
}
.margin({ top: 4 })
品牌型号区域展示了六个汽车品牌的 chip 标签,横向排列,用户可以点击选择。chip 样式与页面其他地方的 chip 保持一致——chip 色背景、次要文字色、10px 圆角。
车辆颜色区域与品牌型号区域结构相同,展示五个颜色选项,每个颜色前面都有对应的 emoji 图标,直观生动。
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS.sub)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
.onClick(() => { onClose(); })
Text('保存')
.fontSize(14)
.fontColor(COLORS.card)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.backgroundColor(COLORS.green)
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.vehicleList.push(new VehicleItem('京A·9X2K8', '理想 L7', '珍珠白', false));
onClose();
})
}
.margin({ top: 20 })
弹窗底部是操作按钮区,包含"取消"和"保存"两个按钮。取消按钮是深色背景、灰色文字的"幽灵按钮"样式,保存按钮是绿色背景、白色文字的"主按钮"样式。绿色作为"保存/确认"操作的颜色,符合绿色代表"成功、正向"的语义。
点击保存按钮时,会向 vehicleList 数组中 push 一个新的 VehicleItem 对象,然后关闭弹窗。由于 vehicleList 是 @State 装饰的状态变量,数组变化会自动触发车辆列表的重新渲染,新添加的车辆会立即出现在车辆 Tab 的列表中。
编辑车辆弹窗
@Builder
panelEdit(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('✏️ 编辑车辆')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
编辑车辆弹窗的结构与添加弹窗类似,但标题变为"✏️ 编辑车辆"。内容区域展示了车牌号、品牌型号、车辆颜色三个字段的当前值——这些值通过 this.editIdx 索引从 vehicleList 中读取。
Text('设为默认车辆')
.fontSize(13)
.fontColor(COLORS.sub)
.margin({ top: 12 })
Row() {
Text(this.editIdx >= 0 && this.vehicleList[this.editIdx].isDefault ? '当前默认' : '设为默认')
.fontSize(13)
.fontColor(this.editIdx >= 0 && this.vehicleList[this.editIdx].isDefault ? COLORS.green : COLORS.text3)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(COLORS.bg)
.borderRadius(16)
.onClick(() => {
if (this.editIdx >= 0) {
for (let i = 0; i < this.vehicleList.length; i++) {
this.vehicleList[i].isDefault = (i === this.editIdx);
}
}
})
}
.margin({ top: 4 })
编辑弹窗比添加弹窗多了一个"设为默认车辆"的选项。这个选项的显示状态与车辆列表中的默认按钮逻辑一致——如果当前车辆已经是默认车辆,显示绿色的"当前默认";如果不是,显示灰色的"设为默认"。点击后的处理逻辑也与列表中的按钮相同,通过循环将当前车辆设为默认。
编辑弹窗的底部按钮是"取消"和"更新",更新按钮使用蓝色而非绿色。这是因为"添加"是创建新事物,用绿色代表"新生";“编辑"是修改已有事物,用蓝色代表"操作”。颜色的细微差异传递了不同的语义信息。
删除确认弹窗
@Builder
panelDel(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column() {
Text('🗑️ 确认删除')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.title)
.margin({ top: 8 })
Text('确定要删除这辆车吗?')
.fontSize(14)
.fontColor(COLORS.sub)
.margin({ top: 12 })
Text(this.delIdx >= 0 ? this.vehicleList[this.delIdx].plate : '')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS.red)
.margin({ top: 4 })
删除确认弹窗的设计最为简洁,但也最需要谨慎。它包含三个关键信息:
- 标题:“🗑️ 确认删除”,明确告知用户当前操作的性质。
- 提示语:“确定要删除这辆车吗?”,用问句形式引发用户确认。
- 车牌号:用红色粗体展示即将删除的车辆车牌号,让用户再次确认操作对象。红色在这里起到了警示作用,提醒用户这是一个危险操作。
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS.sub)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor(COLORS.bg)
.borderRadius(20)
.onClick(() => { onClose(); })
Text('删除')
.fontSize(14)
.fontColor(COLORS.card)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor(COLORS.red)
.borderRadius(20)
.margin({ left: 16 })
.onClick(() => {
if (this.delIdx >= 0) { this.vehicleList.splice(this.delIdx, 1); }
onClose();
})
}
.margin({ top: 24 })
底部的操作按钮中,"删除"按钮使用了醒目的红色背景。红色作为警示色,再次强调了操作的危险性,防止用户误操作。点击删除按钮时,通过 splice 方法从 vehicleList 数组中移除对应索引的元素,然后关闭弹窗。列表会自动更新,被删除的车辆从界面上消失。
三个弹窗各司其职,共同构成了车辆管理的完整操作闭环:添加(创建)→ 编辑(更新)→ 删除(删除),正好对应 CRUD 中的 CUD 操作。每个弹窗都遵循统一的设计规范——遮罩层居中、卡片式布局、明确的标题、清晰的操作按钮,确保了用户体验的一致性。
十八、功能模块对比表
为了更直观地理解六个 Tab 模块的设计差异和各自特点,下面从布局模式、核心视觉元素、数据模型、交互特点、色彩主调等维度进行全面对比。
| 对比维度 | 车场 Tab | 车辆 Tab | 订单 Tab | 优惠 Tab | 月卡 Tab | 我的 Tab |
|---|---|---|---|---|---|---|
| 布局模式 | 横向滑动大卡 | 纵向列表清单 | 时间轴纵向列表 | 虚线券卡纵向列表 | 渐变卡片纵向列表 | 中心聚焦式布局 |
| 核心视觉 | 横滑卡片+进度条 | 左图标+右操作 | 左侧时间轴+状态圆点 | 虚线分割+大金额 | 蓝绿渐变+大数字 | 圆形头像+功能宫格 |
| 数据模型 | ParkingItem | VehicleItem | OrderItem | CouponItem | CardItem | UserItem |
| 数据字段数 | 7 个 | 4 个 | 6 个 | 5 个 | 4 个 | 4 个 |
| 滚动方向 | 横向(Horizontal) | 纵向(跟随外层 Scroll) | 纵向 | 纵向 | 纵向 | 纵向 |
| 交互操作 | 导航前往 | 添加/编辑/删除/设默认 | 查看详情(入口) | 立即领取 | 立即续费/查看明细 | 功能入口跳转 |
| 色彩主调 | 科技蓝 | 信号绿 | 状态多色(蓝/绿/红) | 警示橙 | 蓝绿渐变交替 | 科技蓝+信号绿 |
| 卡片圆角 | 16px | 16px | 无独立卡片 | 16px | 16px | 12px(列表项) |
| 信息密度 | 高(7字段+按钮) | 中(4字段+3操作) | 中高(6字段) | 中(5字段) | 低(大数字为主) | 中低 |
| 动画效果 | 无 | 无 | 无 | 无 | 无 | 头像呼吸动画 |
| 特色技术点 | ScrollDirection.Horizontal 横滑 | @Observed 响应式增删改 | 时间轴线条构造 | ForEach 虚线模拟 | linearGradient 渐变 | 居中布局+宫格入口 |
| 对应颜色函数 | 无 | 无 | orderColor | couponColor | 无 | 无 |
对比分析
从对比表中可以看出,六个 Tab 模块虽然共享同一套色彩体系和设计语言,但每个模块都有其独特的设计语言和技术实现方式:
布局多样性:六个 Tab 采用了六种完全不同的布局模式——横滑大卡、图标清单、时间轴、虚线券卡、渐变大卡、中心聚焦。这种"每个 Tab 一种布局"的设计方式,虽然增加了开发工作量,但极大地丰富了界面的视觉层次,避免了用户在不同 Tab 之间切换时的审美疲劳。
信息密度梯度:六个 Tab 的信息密度从高到低呈现出自然的梯度。车场 Tab 和订单 Tab 信息密度最高,需要展示完整的业务数据;月卡 Tab 和我的 Tab 信息密度最低,以大数字或大头像为视觉焦点。这种密度梯度的设计,让用户在不同 Tab 之间切换时能够体验到"松紧交替"的视觉节奏。
交互深度差异:车辆 Tab 的交互最丰富(增删改查+设默认),因为它是一个典型的数据管理模块;优惠 Tab 的交互最聚焦(只有领取一个操作),因为它的核心目标是促进转化;车场 Tab 和月卡 Tab 的交互居中,以浏览和核心操作为主。
色彩策略各有侧重:车场 Tab 以蓝色为主色调,传达"科技、专业"的感觉;车辆 Tab 以绿色为主色调,传达"畅通、便捷"的感觉;订单 Tab 使用多色状态标识,强调信息的状态区分;优惠 Tab 用橙色激发用户的领取欲望;月卡 Tab 用渐变色营造价值感;我的 Tab 则是蓝绿双色并用,体现综合性。
这种"和而不同"的模块化设计,是 B 端复杂应用界面设计的精髓所在——既保持整体视觉风格的一致性,又让每个业务模块都有其独特的辨识度和最适合的信息呈现方式。
十九、总结与展望
核心价值总结
通过对「停易达」智慧停车应用核心页面的深度源码剖析,我们可以清晰地看到一个高品质 B 端移动应用是如何通过 ArkUI 声明式开发框架构建起来的。整个页面的设计与实现体现了以下几方面的核心价值:
1. 组件化的架构思维
整个页面采用了高度组件化的架构设计。六个 Tab 分别封装为独立的 @Builder 函数,头部、图表、Tab 栏、弹窗也都各自独立。这种"分而治之"的组件化思路,使得代码结构清晰、职责单一、易于维护。每个组件只关注自己的渲染逻辑,通过状态变量进行通信,符合声明式开发的最佳实践。
2. 数据驱动的渲染模式
从 Tab 栏的 TAB_LIST 数组到六个业务数据列表,从图表的 PARK_FEE_VAL 到虚线的 DASH_IDX,整个页面的 UI 渲染几乎完全由数据驱动。ForEach 循环配合 @State 和 @Observed,实现了"数据变、视图变"的响应式渲染。开发者只需要维护数据状态,无需关心 DOM 操作,大幅提升了开发效率和代码可靠性。
3. 系统化的色彩体系
16 种颜色构成的完整色彩体系,涵盖了背景、文字、主题色、功能色、辅助色等各个层面。每种颜色都有明确的语义定义和使用场景,蓝色代表科技与进行中,绿色代表成功与畅通,橙色代表营销与吸引,红色代表警示与危险。系统化的色彩管理确保了界面视觉的一致性和专业性。
4. 多样化的布局表达
六个 Tab 六种布局,横滑大卡、图标清单、时间轴、虚线券卡、渐变大卡、中心聚焦——每一种布局都精准匹配了对应业务场景的信息展示需求。这种多样化的布局表达能力,展示了 ArkUI 声明式布局的强大灵活性:只需通过基础组件的组合,就能构建出各种复杂的自定义 UI 效果,无需依赖第三方 UI 库。
5. 精细化的交互体验
呼吸动画、状态颜色联动、列表项操作弹窗、领取状态即时反馈……每一个交互细节都经过精心设计。特别是呼吸动画的运用,用最简单的 setInterval + 布尔翻转 + scale 动画,实现了富有生命力的视觉效果,成本极低却效果显著。这种"低成本、高收益"的微交互设计思路,值得在更多场景中借鉴。
技术亮点回顾
回顾整个源码,有几个技术亮点特别值得关注:
呼吸动画的实现:通过 setInterval(1000ms) 定时翻转 breath 布尔值,配合 .scale() 和 .animation() 实现元素的缩放呼吸效果。实现方式极简,但效果出众,是"少即是多"设计理念的完美体现。
虚线分割的模拟:用 ForEach 循环渲染 10 个小方块来模拟虚线效果,不依赖边框的 dashed 属性,具有更好的跨平台一致性和可控性。这种"用基础组件模拟高级效果"的思路,展现了 ArkUI 开发的创造性。
渐变柱状图:使用 linearGradient 实现蓝绿渐变的柱状图,90 度水平渐变、12px 大圆角、数值标签置顶——纯手写的图表却有着不亚于专业图表库的视觉效果。
状态颜色函数:orderColor 和 couponColor 两个工具函数,将业务状态与视觉颜色的映射关系集中管理,确保了颜色使用的一致性,也降低了后续维护的成本。
未来展望
站在当前的实现基础上,我们可以对未来的演进方向做出以下展望:
组件化的进一步深化:当前页面的所有 Builder 函数都写在同一个组件内,对于约 900 行的页面来说已经相当不错。但随着业务复杂度的增加,可以考虑将每个 Tab 抽离为独立的自定义组件(单独的 @Component 类),甚至拆分为独立的文件。这样不仅能进一步降低单个文件的复杂度,也便于团队协作开发。
状态管理的升级:当前使用 @State 进行组件内状态管理,对于单页面应用已经足够。但如果未来扩展为多页面应用,且需要在多个页面之间共享车辆、订单等数据,则可以引入 @Provide/@Consume 或 AppStorage 等更高级的状态管理方案。
真实数据接入:当前所有数据都是前端模拟的静态数据。在实际项目中,需要通过网络请求从后端获取真实数据。可以引入基于 Promise 的异步请求封装,配合 aboutToAppear 生命周期函数实现页面加载时的数据拉取,同时添加加载状态和错误处理。
动画效果的丰富:当前页面只有呼吸动画一种动效。未来可以增加更多的交互动效,如 Tab 切换时的内容淡入淡出、列表项的入场动画、弹窗的缩放弹出动画等。合理的动效能够提升界面的流畅感和品质感,但也要注意"过犹不及",避免过度动画干扰用户。
无障碍适配:B 端产品尤其需要重视无障碍体验。可以为图标按钮添加 accessibilityLabel,为图片添加内容描述,确保色盲用户能够通过其他维度(如图标形状、文字标签)区分不同的状态和功能。
多端适配:HarmonyOS 的核心优势在于分布式和多端协同。未来可以考虑将该页面适配到平板、车机等不同设备形态上,利用 ArkUI 的响应式布局能力,实现一次开发、多端部署。
「停易达」智慧停车应用的核心页面,以科技蓝与信号绿为视觉主线,通过六个风格迥异却又和谐统一的 Tab 场景,展现了 ArkUI 声明式开发在 B 端复杂应用中的强大表现力。从系统化的色彩体系到数据驱动的渲染模式,从多样化的布局表达到精细化的交互体验,每一处设计都凝聚着对 B 端产品用户体验的深度思考。
在 HarmonyOS 生态蓬勃发展的今天,掌握 ArkUI 声明式开发不仅仅是掌握一门新的 UI 框架,更是拥抱一种全新的开发思维——用声明替代命令,用数据驱动视图,用组件化解复杂。希望本文的深度剖析,能够为正在探索 ArkUI 开发的开发者们提供一些启发和参考,也期待未来能看到更多基于 ArkUI 的优秀 B 端应用涌现出来。
附录: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 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 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 版本编写,不同版本界面可能存在细微差异。
更多推荐


所有评论(0)