停易达智慧停车平台(HarmonyOS API 24)架构详解:单入口 + 六Tab差异化布局 + Stack弹窗叠加,入口组件 Page812 统领全应用。
停易达智慧停车平台(HarmonyOS API 24)架构详解:单入口 + 六Tab差异化布局 + Stack弹窗叠加,入口组件 Page812 统领全应用。
本文基于 HarmonyOS NEXT + ArkTS 声明式开发框架,完整实现一个「停易达」智慧停车主题 App,涵盖配色系统、状态管理、六Tab差异化布局、停车概览头部、呼吸灯动效、车位占用进度条、时间轴预约、车牌卡管理、消费柱状图、钱包渐变卡、设置开关清单、车辆数据增删改、Stack弹窗叠加、TextInput输入等核心知识点,并附完整源码与运行效果截图。
一、引言:智慧停车的数字化浪潮
在移动互联网与智能设备高速发展的今天,智慧停车已经从单纯的线下找车位,演变为一种高度数字化、流程化、个性化的智慧交通新形态。停车,以其"找位难、缴费烦、管理乱"的痛点,正在成为城市交通治理的关键环节。从商场地下车库到写字楼停车场,从住宅小区路面车位到医院专属停车区,每一个停车场都在经历从"人工管理"到"智慧运营"的数字化转型。用户一句"附近有没有车位",就能在几秒内获取周边车场的空位数量、价格、距离,并一键预约、无感缴费,这种便捷性是传统停车管理难以企及的。然而,智慧停车也面临着车位数据实时性、多车场互联互通、用户体验一致性等现实挑战。
正是在这样的背景下,停易达类智慧停车应用应运而生,并迅速成为智慧交通时代的数字基础设施。一个优秀的智慧停车应用,不仅需要覆盖从"附近车位"到"设置偏好"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到精致而高效,让用户在指尖滑动间便能轻松完成找位、预约、缴费、管理的全流程。
本文将深入剖析一个基于鸿蒙 ArkUI 声明式开发框架构建的停易达智慧停车应用。该应用以"附近车位"为核心,横向扩展出预约、车辆、记录、钱包、设置六大功能模块,每个模块采用完全不同的布局风格——附近车位横向车位卡、预约时间轴、车辆车牌卡、记录柱状图+列表、钱包余额+流水、设置开关清单,形成了一个完整闭环的智慧停车生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、Stack弹窗叠加、呼吸灯动效、车辆数据增删改、TextInput输入、车位占用进度条等关键技术点。

二、应用整体架构概览
整个应用采用了"单入口 + 六Tab差异化布局 + Stack弹窗叠加"的智慧交通应用架构。入口组件 Page812 作为整个应用的根容器,负责管理当前激活的 Tab 状态、车辆列表数据、预约列表数据以及三个弹窗的显隐状态。
应用一共划分为六个一级页面:附近车位(NEARBY)、预约(RESERVE)、车辆(VEHICLE)、记录(RECORD)、钱包(WALLET)以及设置(SETTINGS)。与之前项目"五个页面布局风格统一或半差异化"不同,本应用的六个 Tab 每个布局完全不同——附近车位采用横向车位卡(含车位占用进度条)、预约采用时间轴布局、车辆采用车牌卡管理、记录采用消费柱状图+停车记录列表、钱包采用渐变余额卡+流水明细、设置采用开关清单,这种"一页一型"的差异化设计最大程度还原了真实智慧停车 App 的交互体验。
在数据层面,应用定义了一套完整的类型系统,包括色彩主题、停车场元数据、预约元数据、车辆元数据、停车记录元数据、流水元数据等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类为载体(ParkingLot/Reservation/Vehicle/ParkRecord/Transaction),配合 8 个停车场、6 条预约、3 辆车、8 条停车记录、8 条流水的模拟数据,模拟了一个真实的智慧停车数据环境。所有数据类都带有 constructor 构造函数,使用 new ParkingLot(...) 方式实例化。
在交互层面,应用通过 Stack 布局叠加实现了三类弹窗的显示:添加车辆、修改车辆、删除车辆。弹窗以 Stack 叠加全屏半透明遮罩 + 居中卡片的形式呈现,zIndex 999 确保弹窗在最上层,遮罩点击可关闭。与之前项目"纯展示型弹窗"不同,本应用的弹窗真正实现了车辆数据的增删改——添加车辆会向 vehicleList 头部 unshift 新车辆(含 3 个 TextInput 输入框)、修改车辆会修改数组指定元素(含 2 个 TextInput 输入框)、删除车辆会 splice 删除指定元素(二次确认),配合 @State 响应式刷新,形成了完整的车辆管理 CRUD 闭环。

在视觉层面,应用独创了"停车概览头部"设计——顶部固定显示当前定位(📍万象城)+ 四栏统计(8附近车场/258可用车位/¥5均价/¥328钱包余额,竖线分隔)+ 优惠券横条(⚡新用户送¥30停车券 + 🔒自动缴费免扫码),无装饰带动效,信息密度高,直接还原真实智慧停车 App 的头部体验。应用还独创了"呼吸灯"动效——通过 aboutToAppear 中 setInterval 700ms 切换 breath 状态,可用车位标签、未完成预约状态、无感停车状态都使用 opacity 呼吸灯动画,营造出"车位实时更新、服务在线"的鲜活感。应用还独创了"车位占用进度条"——每个停车场卡片底部有一条 4px 高的进度条,宽度按 available/total 百分比计算,有车位用青蓝色、已满用红色,直观呈现车位占用率。
下面逐页展示应用的六大功能模块运行效果。
三、预约页:时间轴布局的预约记录
预约页以"预约记录"为核心,独创"时间轴"布局,是六个 Tab 中布局最独特的页面之一。

页面顶部是标题"📅 预约记录 · 时间轴"。下方遍历 resList 6 条预约,每条预约采用时间轴布局:左侧是日期时间列(宽度 52,日期 teal 青蓝加粗 10 号 + 时间 text3 灰色 7 号),中间是一条 2×60 的竖线(line 浅蓝灰),右侧是预约信息卡(停车场名 maxLines 1 + 🚗车牌 + ⏱时长 + 状态标签)。状态标签通过 resStatusColor 动态着色:待开始橙色、使用中绿色、已完成灰色、已取消红色,未完成的预约(待开始/使用中)状态标签带呼吸灯 opacity 动画,提醒用户关注。
6 条预约从 08-21 到 08-10 按时间倒序排列,状态涵盖待开始(1条,万象城,14:00-16:00,2小时)、使用中(1条,环球中心,10:00-12:30,2.5小时,绿色呼吸灯)、已完成(4条)、已取消(1条,春熙路,19:00-22:00,3小时,红色),预约车辆涵盖川A·DM856(默认车辆,5条)和川A·X1234(1条),完整还原了用户的预约历史。时间轴布局让预约的时间顺序一目了然,竖线连接形成连续的时间脉络。
四、车辆页:车牌卡管理与CRUD闭环
车辆页以"我的车辆"为核心,采用"车牌卡"布局,支持添加、修改、删除操作,真正实现了车辆管理的 CRUD 闭环。

页面顶部是标题行"🚗 我的车辆 · N辆" + +添加按钮(chip 青蓝底,含+图标和"添加"文字),点击添加按钮触发 addModal 添加车辆弹窗。下方遍历 vehicleList,每辆车采用车牌卡布局:上方是车辆信息行(图标 60×60 圆角16 + 车型+颜色 + 默认标签 teal 底白字,仅默认车辆显示),下方是车牌操作行(🪪车牌 blue 蓝色加粗 16 号 + ✏️编辑按钮 + 🗑️删除按钮)。
3 辆车覆盖川A·DM856(理想 L7,白色,默认车辆,🚙)、川A·X1234(比亚迪 汉,红色,非默认,🚗)、川A·T8899(小鹏 G6,灰色,非默认,🚐),默认车辆带 teal "默认"标签,车牌蓝色加粗醒目,符合真实车牌的视觉认知。
点击 ✏️ 触发 editModal 修改车辆弹窗,包含 2 个 TextInput 输入框(车牌号、车型,预填当前车辆信息,可编辑)+ 取消/保存按钮,确认后修改 vehicleList[editIdx] 的 plate 和 model 字段。点击 🗑️ 触发 delModal 删除车辆弹窗,二次确认设计(“「川A·DM856」将从列表移除”)+ 再想想/确认删除按钮,确认后 vehicleList.splice(delIdx, 1) 删除指定车辆。点击 +添加触发 addModal 添加车辆弹窗,包含 3 个 TextInput 输入框(车牌号、车型、颜色)+ 取消/确认添加按钮,确认后 vehicleList.unshift(new Vehicle(…)) 向头部添加新车辆。三个弹窗都使用 Stack 叠加 + mask 遮罩(点击关闭)+ zIndex 999,TextInput 支持实时输入,@State 响应式刷新,真正实现了车辆管理的完整 CRUD 闭环。
五、记录页:消费柱状图与停车记录列表
记录页以"停车记录"为核心,独创"近6月消费柱状图 + 最近停车记录列表"的复合布局。

页面顶部是标题"📋 停车记录 · 近6月消费(元)“。下方第一部分是近6月消费柱状图卡片,遍历 FEE_DATA [0, 0, 42, 68, 35, 56] 对应 MONTHS [‘3月’,‘4月’,‘5月’,‘6月’,‘7月’,‘8月’],柱子高度 = value(0 消费显示”—“占位,柱子高度 3px),8 月(index 5)56 元最高用 teal 青蓝色突出显示,其余月份用 blue 蓝色,柱子顶部显示金额数字,底部显示月份。柱状图卡片高度 125,alignItems Bottom 让柱子从底部生长,直观呈现近半年的停车消费趋势。3 月和 4 月消费为 0(显示”—"),5 月开始有消费,符合"新用户逐步启用"的真实使用轨迹。
下方第二部分是"最近的停车记录"列表,遍历 RECORD_LIST 8 条记录,每条记录卡片包含:🅿️图标 38×38(dark 浅蓝底)+ 记录信息列(停车场名 maxLines 1 + 🚗车牌 + ⏱时长)+ 金额日期列(费用 orange 橙色加粗 13 号 + 日期 text3 灰色 8 号)。
8 条停车记录从 08-20 到 08-05 按时间倒序排列,停车场涵盖万象城(¥10,2小时)、环球中心(¥18,3.5小时)、国际中心(¥64,8小时,金额最高)、银泰城(¥11,1.5小时)、锦江小区(¥12,4小时)、华西医院(¥8,2小时)、春熙路(¥18,3小时)、会展中心(¥15,5小时),停车时长从 1.5 小时到 8 小时不等,费用从 ¥8 到 ¥64 不等,完整还原了用户的停车消费历史。
六、钱包页:渐变余额卡与流水明细
钱包页以"停车钱包"为核心,独创"渐变余额卡 + 流水明细列表"的复合布局,是六个 Tab 中视觉最精致的页面。

页面顶部是标题"💳 停车钱包"。下方第一部分是钱包渐变卡,使用 linearGradient 135 度渐变(从 #0B7D7D 深青蓝到 #0EA5A5 青蓝),卡片包含:💳图标 56×56 圆形(半透明白底 rgba(255,255,255,0.2))+ 余额信息列("钱包余额"浅青文字 10 号 + "¥328.50"白色加粗 26 号)+ 底部操作按钮行("充值"半透明白底加粗 + "账单"半透明白底 15% 透明度),两个按钮 layoutWeight 均分,圆角 10。渐变卡从深青蓝到青蓝的 135 度斜向渐变,营造出银行卡般的精致质感,白色余额数字在渐变背景上醒目突出。
下方第二部分是"流水明细"列表,遍历 TXN_LIST 8 条流水,每条流水行包含:图标 36×36(充值💰/停车费🅿️,dark 浅蓝底)+ 类型日期列(类型加粗 11 号 + 日期 text3 8 号)+ 金额余额列(金额 feeColor 动态着色:充值 green 绿色、停车费 orange 橙色,加粗 12 号 + “余 ¥xxx” text3 8 号)。
8 条流水从 08-20 到 08-10 按时间倒序排列,类型涵盖充值(2条,+¥200/+¥100,绿色)和停车费(6条,-¥10/-¥18/-¥64/-¥11/-¥12/-¥8,橙色),余额从 ¥328.50 逐步追溯到 ¥143.50,完整还原了用户的钱包收支历史。feeColor 函数按流水类型动态着色(充值绿色/停车费橙色/其余蓝色),让收入和支出一目了然。
七、设置页:开关清单与偏好管理
设置页以"设置"为核心,采用"开关清单"布局,是六个 Tab 中最简洁的页面。

页面顶部是标题"⚙️ 设置 · 偏好与安全"。下方是设置列表卡片,包含 7 个设置项,每项采用"图标 + 名称 + 状态"的横向布局,项与项之间用 Divider 分隔(line 浅蓝灰):
- 🔔 车位提醒 — 右侧"开启"teal 青蓝加粗
- 🅿️ 自动缴费 — 右侧"已绑定"teal 青蓝
- 🗺️ 导航偏好 — 右侧"高德地图"sub 灰蓝
- 🌙 夜间模式 — 右侧"跟随系统"sub 灰蓝
- 📱 无感停车 — 右侧"已开通"green 绿色,带呼吸灯 opacity 动画(0.6↔1),营造"服务在线"的鲜活感
- 🎫 我的卡券 — 右侧"5张"orange 橙色
- ❓ 帮助与反馈 — 右侧">"text3 灰色箭头
每个设置项 padding 上下 12,左侧 4,图标 18 号,名称 11 号主色,layoutWeight(1) 让名称占据中间空间,右侧状态文字 9 号。设置列表卡片白色背景圆角 14,padding 左右 16。页面底部是版本号"v3.2.1 · 停易达智慧停车",text3 灰色 9 号,居中对齐,margin 顶部 16 底部 8。
开关清单布局让设置项一目了然,状态文字用不同颜色区分(teal 已开启/已绑定、green 已开通、orange 卡券数量、sub 偏好设置、text3 箭头),无感停车的呼吸灯动效让静态的设置页面也有了"服务在线"的鲜活感。
八、色彩主题系统:天蓝底与青蓝深蓝的智慧交通视觉语言
8.1 色彩接口定义
任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个 ColorPalette 接口,将整个应用所需的色彩抽象为一个结构化的契约:
interface ColorPalette {
bg: string; card: string; dark: string; darkL: string;
title: string; sub: string; text3: string;
teal: string; tealD: string; blue: string; blueD: string;
red: string; orange: string; green: string; purple: string;
line: string; tabOn: string; mask: string; chip: string;
}
- bg:页面整体背景色 #EDF6FB,天蓝底,营造智慧交通的清爽科技感。
- teal:主色调 #0EA5A5,青蓝,用于按钮、强调文字、激活 Tab、车位进度条、价格等高优先级视觉元素,是智慧停车的标志性色彩。
- blue:辅助色 #1E88E5,蓝色,与青蓝形成同色系深浅对比,用于车牌、柱状图、车场类型等元素,营造"科技、信任"的氛围。
- title:标题色 #0D3859,深蓝,用于主标题文字,与天蓝底形成高对比度。
- mask:遮罩色 rgba(13,56,89,0.55),深蓝半透明,用于弹窗遮罩。
- chip:标签底色 rgba(14,165,165,0.10),青蓝 10% 透明度,用于类型标签、优惠券标签背
8.2 色彩实例化
const COLORS: ColorPalette = {
bg: '#EDF6FB', card: '#FFFFFF', dark: '#DDEEF6', darkL: '#CCE0EC',
title: '#0D3859', sub: '#4A7A99', text3: '#8FB3C9',
teal: '#0EA5A5', tealD: '#0B7D7D', blue: '#1E88E5', blueD: '#0D47A1',
red: '#E74C3C', orange: '#FF9F43', green: '#26AE60', purple: '#7B2CBF',
line: '#D5E5EE', tabOn: '#0EA5A5',
mask: 'rgba(13,56,89,0.55)', chip: 'rgba(14,165,165,0.10)'
};
主色 #0EA5A5 是一种清爽的青蓝色,既契合"智慧停车"主题(科技、清爽、信任的标志性色彩),又具有现代、专业的气质。辅助色 #1E88E5 蓝色与青蓝形成同色系深浅对比,呼应车牌、科技感的色彩。bg #EDF6FB 天蓝底与主色同冷色系,营造出智慧交通的清爽科技氛围。值得注意的是,本应用的动态颜色函数返回的是多色系(青蓝/蓝/绿/红/橙/紫),用于不同业务维度的语义着色——停车场类型按类型着色(商场蓝/写字楼青蓝/住宅绿/医院红/其余紫),预约状态按状态着色(待开始橙/使用中绿/已完成灰/已取消红),流水类型按类型着色(充值绿/停车费橙/其余蓝),形成了"以色定类型、以色定状态"的高级渲染模式。钱包渐变卡使用 linearGradient 135 度从 #0B7D7D 深青蓝到 #0EA5A5 青蓝,营造出银行卡般的精致质感。这种将所有色彩集中管理的做法保证了全局一致性,支持主题切换,降低了维护成本。

九、类型定义:构建严谨的数据契约
应用通过一组带 constructor 的 @Observed 类,为每一类业务对象建立了明确的数据契约。
9.1 停车场、预约、车辆元数据
@Observed export class ParkingLot {
id: number; name: string; dist: string; available: number; total: number;
price: string; type: string; icon: string;
constructor(...) { ... }
}
@Observed export class Reservation {
id: number; lot: string; plate: string; date: string; time: string; duration: string; status: string;
constructor(...) { ... }
}
@Observed export class Vehicle {
plate: string; model: string; color: string; isDefault: boolean; icon: string;
constructor(...) { ... }
}
ParkingLot 描述停车场的 ID、名称、距离、可用车位、总车位、价格、类型、图标,用 available/total 字段计算车位占用率,type 字段区分车场类型。Reservation 描述预约的 ID、停车场、车牌、日期、时间、时长、状态,用 status 字段体现预约流转状态。Vehicle 描述车辆的车牌、车型、颜色、是否默认、图标,用 plate 作为唯一标识,isDefault 区分默认车辆。
9.2 停车记录、流水与图表元数据
@Observed export class ParkRecord {
id: number; lot: string; plate: string; date: string; duration: string; fee: string;
constructor(...) { ... }
}
@Observed export class Transaction {
id: number; type: string; amount: string; date: string; balance: string;
constructor(...) { ... }
}
ParkRecord 描述停车记录的 ID、停车场、车牌、日期、时长、费用,用 fee 字段体现消费金额。Transaction 描述流水的 ID、类型、金额、日期、余额,用 type 字段区分充值/停车费,balance 字段记录交易后余额。所有数据类都带有 constructor 构造函数,使用 new ClassName(...) 方式实例化,配合 @Observed 装饰器实现响应式更新。
十、配置驱动:常量数据与工具函数
10.1 Tab 导航配置
const TAB_LIST: TabMeta[] = [
{ icon: '🅿️', label: '附近' },
{ icon: '📅', label: '预约' },
{ icon: '🚗', label: '车辆' },
{ icon: '📋', label: '记录' },
{ icon: '💳', label: '钱包' },
{ icon: '⚙️', label: '设置' }
];

与之前项目"5 个 Tab"不同,本应用有 6 个 Tab,不单独配置颜色,统一使用 tabOn #0EA5A5 青蓝作为激活色,非激活使用 text3 #8FB3C9 灰色,保持底部导航的简洁统一,让视觉焦点集中在内容区。6 个 Tab 单排排列,每个 layoutWeight 均分宽度。
10.2 月份消费数据与工具函数
const MONTHS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const FEE_DATA: number[] = [0, 0, 42, 68, 35, 56];
function lotTypeColor(t: string): string {
if (t === '商场') { return COLORS.blue; }
if (t === '写字楼') { return COLORS.teal; }
if (t === '住宅') { return COLORS.green; }
if (t === '医院') { return COLORS.red; }
return COLORS.purple;
}
function resStatusColor(s: string): string {
if (s === '待开始') { return COLORS.orange; }
if (s === '使用中') { return COLORS.green; }
if (s === '已完成') { return COLORS.text3; }
return COLORS.red;
}
function feeColor(t: string): string {
if (t === '充值') { return COLORS.green; }
if (t === '停车费') { return COLORS.orange; }
return COLORS.blue;
}
FEE_DATA 近 6 月消费数据 [0,0,42,68,35,56],前两月 0 消费(显示"—"占位),6 月 68 元最高,符合"新用户 5 月开始逐步启用,6 月消费高峰"的真实使用轨迹。三个工具函数实现了语义化着色:lotTypeColor 按车场类型分五档色彩(商场蓝/写字楼青蓝/住宅绿/医院红/其余紫),resStatusColor 按预约状态分四档色彩(待开始橙/使用中绿/已完成灰/已取消红),feeColor 按流水类型分三档色彩(充值绿/停车费橙/其余蓝),实现了"以色定类型、以色定状态"的高级渲染模式。
十一、数据模型:可观察的智慧停车实体
const LOT_LIST: ParkingLot[] = [ new ParkingLot(1, '万象城地下停车场', '280m', 45, 320, '¥5/时', '商场', '🏬'), ... ];
const RES_LIST: Reservation[] = [ new Reservation(1, '万象城地下停车场', '川A·DM856', '08-21', '14:00-16:00', '2小时', '待开始'), ... ];
const VEHICLE_LIST: Vehicle[] = [ new Vehicle('川A·DM856', '理想 L7', '白色', true, '🚙'), ... ];
const RECORD_LIST: ParkRecord[] = [ new ParkRecord(1, '万象城地下停车场', '川A·DM856', '08-20', '2小时', '¥10'), ... ];
const TXN_LIST: Transaction[] = [ new Transaction(1, '充值', '+¥200', '08-20', '¥328.50'), ... ];
五组常量数据承载了停易达的全部业务信息:LOT_LIST 8 个停车场(覆盖商场/写字楼/住宅/医院四大类型,距离从 280m 到 2.0km,可用车位从 0 到 88,价格从 ¥3/时到 ¥8/时)、RES_LIST 6 条预约(状态涵盖待开始/使用中/已完成/已取消)、VEHICLE_LIST 3 辆车(1 辆默认 + 2 辆非默认)、RECORD_LIST 8 条停车记录(费用从 ¥8 到 ¥64)、TXN_LIST 8 条流水(2 条充值 + 6 条停车费)。所有数据使用 new ClassName(...) 构造函数实例化,配合 @Observed 装饰器实现响应式更新。vehicleList 和 resList 使用 @State 装饰,vehicleList 支持运行时增删改(添加 unshift/修改改元素/删除 splice)。
十二、动态颜色与工具函数:数据驱动的视觉映射
应用通过三个工具函数将业务数据映射为视觉色彩,实现"数据即配色"。
function lotTypeColor(t: string): string { /* 按车场类型着色 */ }
function resStatusColor(s: string): string { /* 按预约状态着色 */ }
function feeColor(t: string): string { /* 按流水类型着色 */ }
- lotTypeColor:按停车场类型分五档色彩(商场蓝/写字楼青蓝/住宅绿/医院红/其余紫),“以色定类型”,用户一眼就能从颜色判断停车场类型。
- resStatusColor:按预约状态分四档色彩(待开始橙色/使用中绿色/已完成灰色/已取消红色),“以色定状态”,用户一眼就能从颜色判断预约流转状态。
- feeColor:按流水类型分三档色彩(充值绿色/停车费橙色/其余蓝色),“以色定收支”,用户一眼就能从颜色判断收入或支出。
值得注意的是,本应用的车位占用进度条也使用了动态着色逻辑——available > 0 用 teal 青蓝,available = 0 用 red 红色,进度条宽度按 available/total 百分比计算((item.available / item.total * 100).toFixed(0) + '%'),直观呈现车位占用率。可用车位标签也使用动态着色——available > 0 绿色且带呼吸灯 opacity 动画(0.6↔1),available = 0 红色显示"已满",营造出"车位实时更新"的鲜活感。钱包渐变卡使用 linearGradient 135 度从 #0B7D7D 深青蓝到 #0EA5A5 青蓝,是本应用最精致的视觉元素。
十三、入口组件:应用的骨架与六Tab差异化布局
13.1 组件声明与状态
@Entry
@Component
struct Page812 {
@State tabIdx: number = 0;
@State breath: boolean = false;
@State addModal: boolean = false;
@State editModal: boolean = false;
@State delModal: boolean = false;
@State editIdx: number = 0;
@State delIdx: number = 0;
@State addPlate: string = '';
@State addModel: string = '';
@State addColor: string = '';
@State editPlate: string = '';
@State editModel: string = '';
@State vehicleList: Vehicle[] = VEHICLE_LIST;
@State resList: Reservation[] = RES_LIST;
private timer: number = -1;
}
16 个 @State 状态分五类:导航状态(tabIdx)、动效状态(breath)、弹窗控制状态(addModal/editModal/delModal + editIdx/delIdx)、弹窗输入状态(addPlate/addModel/addColor/editPlate/editModel)、数据状态(vehicleList/resList)。private timer 用于呼吸灯 setInterval 管理。
13.2 生命周期与呼吸灯动效
aboutToAppear(): void { this.timer = setInterval(() => { this.breath = !this.breath; }, 700); }
aboutToDisappear(): void { if (this.timer > 0) { clearInterval(this.timer); } }
aboutToAppear 中启动 setInterval 700ms 切换 breath 状态,实现全局呼吸灯动效。aboutToDisappear 中 clearInterval 清理定时器,避免内存泄漏。这是本应用独创的动效实现方式,可用车位标签、未完成预约状态、无感停车状态都使用 breath 驱动的 opacity 呼吸灯动画(this.breath ? 0.6 : 1 / 0.5 : 1),营造出"车位实时更新、服务在线"的鲜活感。
13.3 停车概览头部(@Builder header)
顶部头部通过 @Builder header() 抽取,包含三层:顶部导航栏(🅿️chip + “停易达 · 智慧停车” + 📍万象城定位)、四栏统计卡(8附近车场 teal / 258可用车位 blue / ¥5均价 green / ¥328钱包余额 orange,竖线分隔)、优惠券横条(⚡新用户送¥30停车券 chip teal底 + 🔒自动缴费免扫码 dark底)。头部无装饰带动效,信息密度高,直接还原真实智慧停车 App 的头部体验。
13.4 底部 Tab 导航与 Stack 弹窗
底部导航通过 @Builder tabBar() 构建,Row 单排 6 Tab,圆角 16 卡片背景,激活态 tabOn 青蓝加粗,非激活 text3 灰色。build() 使用 Stack 布局,主 Column 包含 header + Scroll 内容区(if-else 切换 6 个 @Builder 页面)+ tabBar,三个弹窗(addModal/editModal/delModal)使用 Stack 叠加在最上层,zIndex 999,每个弹窗包含 modalOverlay 遮罩(点击关闭)+ 居中卡片。
十四、关键技术特性对比
| 特性维度 | 实现方式 | 技术要点 | 设计价值 |
|---|---|---|---|
| 色彩管理 | ColorPalette 接口 + COLORS 常量 | 集中定义 20 种语义色,含 mask/chip 半透明色 | 保证视觉一致性,支持主题切换 |
| 数据建模 | @Observed 类 + constructor | 5 个数据类(ParkingLot/Reservation/Vehicle/ParkRecord/Transaction),new 实例化 | 数据与 UI 双向联动,响应式更新 |
| 六Tab差异化 | 每页独立 @Builder 布局 | 附近车位横向卡/预约时间轴/车辆车牌卡/记录柱状图+列表/钱包渐变卡+流水/设置开关清单 | 一页一型,还原真实停车App体验 |
| 停车概览头部 | @Builder header 固定头部 | 当前定位+四栏统计+优惠券横条,无装饰带动效 | 信息密度高,还原真实停车头部 |
| 呼吸灯动效 | setInterval + opacity | aboutToAppear 启动 700ms 切换,可用车位/未完成预约/无感停车呼吸 | 营造车位实时更新、服务在线的鲜活感 |
| 车位占用进度条 | 百分比宽度 + 动态着色 | available/total 百分比宽度,有车位teal/已满red | 直观呈现车位占用率 |
| 真正数据增删改 | vehicleList unshift/改元素/splice | 添加车辆unshift(3个TextInput)、修改车辆改元素(2个TextInput)、删除车辆splice | 完整车辆管理CRUD闭环 |
| TextInput输入 | 弹窗内 TextInput | 添加车辆车牌/车型/颜色3输入,修改车辆车牌/车型2输入 | 真正可编辑的弹窗交互 |
| Stack弹窗叠加 | Stack + zIndex 999 | 三个弹窗Stack叠加,mask遮罩点击关闭 | 弹窗层级清晰,交互灵活 |
| 钱包渐变卡 | linearGradient 135度 | #0B7D7D→#0EA5A5 斜向渐变,半透明白按钮 | 银行卡般的精致质感 |
| 时间轴布局 | 日期+竖线+预约信息 | 左侧日期时间+中间2x60竖线+右侧预约卡 | 预约时间顺序一目了然 |
| 动态颜色 | lotTypeColor/resStatusColor/feeColor | 按车场类型/预约状态/流水类型分档着色 | 以色定类型、以色定状态 |
| 状态管理 | @State + @Observed | 16个状态分五类,vehicleList/resList @State 可修改 | 细粒度状态控制,自动触发渲染 |
| 组件化 | @Component + @Builder | 1个入口组件 + 6个页面@Builder + 3个弹窗@Builder + header/tabBar/modalOverlay | 高内聚低耦合,代码复用率高 |
| 列表渲染 | ForEach + 横向卡/时间轴/列表 | 停车场横向卡、预约时间轴、车辆车牌卡、记录列表、流水列表 | 数据驱动渲染,布局灵活 |
| 数据可视化 | 柱状图 + 进度条 + 四栏统计 | 近6月消费柱状图(0消费显示—)、车位占用进度条、头部四栏统计 | 多维度呈现,直观易读 |
| 文本省略 | maxLines + textOverflow | 停车场名maxLines1、预约停车场名maxLines1 | 长文本优雅截断,布局整齐 |
| 生命周期 | aboutToAppear/aboutToDisappear | setInterval启动/clearInterval清理,避免内存泄漏 | 健壮性强,无资源泄漏 |
| 布局系统 | Column/Row + Stack + Scroll + Flex | 弹性布局、Stack叠加、Scroll纵向滚动、时间轴竖线 | 自适应布局,层次分明 |
完整代码
// ============================================================
// 812 停易达 · 智慧停车(现代行业:智慧交通 / 停车服务)
// 头部样式:停车概览头部(当前定位 + 附近车位统计 + 余额横条)无动画
// 布局风格:6 个 tab 每个布局完全不同
// 附近车位=横向车位卡 / 预约=时间轴 / 车辆=车牌卡
// 记录=柱状图+列表 / 钱包=余额+流水 / 设置=开关清单
// 底部 6 tab 单排
// 主题:天蓝底 + 青蓝 + 深蓝
// ============================================================
interface ColorPalette {
bg: string;
card: string;
dark: string;
darkL: string;
title: string;
sub: string;
text3: string;
teal: string;
tealD: string;
blue: string;
blueD: string;
red: string;
orange: string;
green: string;
purple: string;
line: string;
tabOn: string;
mask: string;
chip: string;
}
const COLORS: ColorPalette = {
bg: '#EDF6FB',
card: '#FFFFFF',
dark: '#DDEEF6',
darkL: '#CCE0EC',
title: '#0D3859',
sub: '#4A7A99',
text3: '#8FB3C9',
teal: '#0EA5A5',
tealD: '#0B7D7D',
blue: '#1E88E5',
blueD: '#0D47A1',
red: '#E74C3C',
orange: '#FF9F43',
green: '#26AE60',
purple: '#7B2CBF',
line: '#D5E5EE',
tabOn: '#0EA5A5',
mask: 'rgba(13,56,89,0.55)',
chip: 'rgba(14,165,165,0.10)'
};
interface TabMeta { icon: string; label: string; }
const TAB_LIST: TabMeta[] = [
{ icon: '🅿️', label: '附近' },
{ icon: '📅', label: '预约' },
{ icon: '🚗', label: '车辆' },
{ icon: '📋', label: '记录' },
{ icon: '💳', label: '钱包' },
{ icon: '⚙️', label: '设置' }
];
const MONTHS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月'];
const FEE_DATA: number[] = [0, 0, 42, 68, 35, 56];
function lotTypeColor(t: string): string {
if (t === '商场') { return COLORS.blue; }
if (t === '写字楼') { return COLORS.teal; }
if (t === '住宅') { return COLORS.green; }
if (t === '医院') { return COLORS.red; }
return COLORS.purple;
}
function resStatusColor(s: string): string {
if (s === '待开始') { return COLORS.orange; }
if (s === '使用中') { return COLORS.green; }
if (s === '已完成') { return COLORS.text3; }
return COLORS.red;
}
function feeColor(t: string): string {
if (t === '充值') { return COLORS.green; }
if (t === '停车费') { return COLORS.orange; }
return COLORS.blue;
}
@Observed export class ParkingLot {
id: number; name: string; dist: string; available: number; total: number;
price: string; type: string; icon: string;
constructor(id: number, name: string, dist: string, available: number, total: number, price: string, type: string, icon: string) {
this.id = id; this.name = name; this.dist = dist; this.available = available;
this.total = total; this.price = price; this.type = type; this.icon = icon;
}
}
@Observed export class Reservation {
id: number; lot: string; plate: string; date: string; time: string; duration: string; status: string;
constructor(id: number, lot: string, plate: string, date: string, time: string, duration: string, status: string) {
this.id = id; this.lot = lot; this.plate = plate; this.date = date; this.time = time;
this.duration = duration; this.status = status;
}
}
@Observed export class Vehicle {
plate: string; model: string; color: string; isDefault: boolean; icon: string;
constructor(plate: string, model: string, color: string, isDefault: boolean, icon: string) {
this.plate = plate; this.model = model; this.color = color; this.isDefault = isDefault; this.icon = icon;
}
}
@Observed export class ParkRecord {
id: number; lot: string; plate: string; date: string; duration: string; fee: string;
constructor(id: number, lot: string, plate: string, date: string, duration: string, fee: string) {
this.id = id; this.lot = lot; this.plate = plate; this.date = date; this.duration = duration; this.fee = fee;
}
}
@Observed export class Transaction {
id: number; type: string; amount: string; date: string; balance: string;
constructor(id: number, type: string, amount: string, date: string, balance: string) {
this.id = id; this.type = type; this.amount = amount; this.date = date; this.balance = balance;
}
}
const LOT_LIST: ParkingLot[] = [
new ParkingLot(1, '万象城地下停车场', '280m', 45, 320, '¥5/时', '商场', '🏬'),
new ParkingLot(2, '国际中心写字楼下', '450m', 12, 180, '¥8/时', '写字楼', '🏢'),
new ParkingLot(3, '春熙路立体停车楼', '600m', 0, 150, '¥6/时', '商场', '🏗️'),
new ParkingLot(4, '华西医院停车场', '850m', 8, 200, '¥4/时', '医院', '🏥'),
new ParkingLot(5, '锦江小区路面车位', '320m', 23, 60, '¥3/时', '住宅', '🏘️'),
new ParkingLot(6, '环球中心地下车库', '1.2km', 67, 450, '¥5/时', '商场', '🌐'),
new ParkingLot(7, '银泰城智慧停车场', '1.5km', 15, 120, '¥7/时', '商场', '🏬'),
new ParkingLot(8, '会展中心P3停车场', '2.0km', 88, 500, '¥3/时', '商场', '🏟️')
];
const RES_LIST: Reservation[] = [
new Reservation(1, '万象城地下停车场', '川A·DM856', '08-21', '14:00-16:00', '2小时', '待开始'),
new Reservation(2, '环球中心地下车库', '川A·DM856', '08-20', '10:00-12:30', '2.5小时', '使用中'),
new Reservation(3, '国际中心写字楼下', '川A·X1234', '08-18', '09:00-18:00', '8小时', '已完成'),
new Reservation(4, '银泰城智慧停车场', '川A·DM856', '08-15', '13:00-15:00', '2小时', '已完成'),
new Reservation(5, '春熙路立体停车楼', '川A·X1234', '08-12', '19:00-22:00', '3小时', '已取消'),
new Reservation(6, '华西医院停车场', '川A·DM856', '08-10', '08:00-10:00', '2小时', '已完成')
];
const VEHICLE_LIST: Vehicle[] = [
new Vehicle('川A·DM856', '理想 L7', '白色', true, '🚙'),
new Vehicle('川A·X1234', '比亚迪 汉', '红色', false, '🚗'),
new Vehicle('川A·T8899', '小鹏 G6', '灰色', false, '🚐')
];
const RECORD_LIST: ParkRecord[] = [
new ParkRecord(1, '万象城地下停车场', '川A·DM856', '08-20', '2小时', '¥10'),
new ParkRecord(2, '环球中心地下车库', '川A·DM856', '08-18', '3.5小时', '¥18'),
new ParkRecord(3, '国际中心写字楼下', '川A·X1234', '08-15', '8小时', '¥64'),
new ParkRecord(4, '银泰城智慧停车场', '川A·DM856', '08-14', '1.5小时', '¥11'),
new ParkRecord(5, '锦江小区路面车位', '川A·DM856', '08-12', '4小时', '¥12'),
new ParkRecord(6, '华西医院停车场', '川A·DM856', '08-10', '2小时', '¥8'),
new ParkRecord(7, '春熙路立体停车楼', '川A·X1234', '08-08', '3小时', '¥18'),
new ParkRecord(8, '会展中心P3停车场', '川A·DM856', '08-05', '5小时', '¥15')
];
const TXN_LIST: Transaction[] = [
new Transaction(1, '充值', '+¥200', '08-20', '¥328.50'),
new Transaction(2, '停车费', '-¥10', '08-20', '¥128.50'),
new Transaction(3, '停车费', '-¥18', '08-18', '¥138.50'),
new Transaction(4, '停车费', '-¥64', '08-15', '¥156.50'),
new Transaction(5, '充值', '+¥100', '08-14', '¥220.50'),
new Transaction(6, '停车费', '-¥11', '08-14', '¥120.50'),
new Transaction(7, '停车费', '-¥12', '08-12', '¥131.50'),
new Transaction(8, '停车费', '-¥8', '08-10', '¥143.50')
];
@Entry
@Component
struct Page812 {
@State tabIdx: number = 0;
@State breath: boolean = false;
@State addModal: boolean = false;
@State editModal: boolean = false;
@State delModal: boolean = false;
@State editIdx: number = 0;
@State delIdx: number = 0;
@State addPlate: string = '';
@State addModel: string = '';
@State addColor: string = '';
@State editPlate: string = '';
@State editModel: string = '';
@State vehicleList: Vehicle[] = VEHICLE_LIST;
@State resList: Reservation[] = RES_LIST;
private timer: number = -1;
aboutToAppear(): void { this.timer = setInterval(() => { this.breath = !this.breath; }, 700); }
aboutToDisappear(): void { if (this.timer > 0) { clearInterval(this.timer); } }
@Builder
header() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Text('🅿️').fontSize(16).padding(6).borderRadius(12).backgroundColor(COLORS.chip)
Text('停易达 · 智慧停车').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('📍 万象城').fontSize(9).fontColor(COLORS.sub)
}
.width('100%')
Column({ space: 12 }) {
Row({ space: 0 }) {
Column({ space: 2 }) {
Text('8').fontSize(20).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
Text('附近车场').fontSize(8).fontColor(COLORS.text3)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(36).backgroundColor(COLORS.line)
Column({ space: 2 }) {
Text('258').fontSize(20).fontColor(COLORS.blue).fontWeight(FontWeight.Bold)
Text('可用车位').fontSize(8).fontColor(COLORS.text3)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(36).backgroundColor(COLORS.line)
Column({ space: 2 }) {
Text('¥5').fontSize(20).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
Text('均价/时').fontSize(8).fontColor(COLORS.text3)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(36).backgroundColor(COLORS.line)
Column({ space: 2 }) {
Text('¥328').fontSize(20).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
Text('钱包余额').fontSize(8).fontColor(COLORS.text3)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 8 }) {
Text('⚡ 新用户送¥30停车券').fontSize(9).fontColor(COLORS.teal)
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.chip)
Text('🔒 自动缴费免扫码').fontSize(9).fontColor(COLORS.sub)
.padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor(COLORS.dark)
}
.width('100%')
}
.width('100%').padding(14).borderRadius(16).backgroundColor(COLORS.card)
.borderWidth(1).borderColor(COLORS.line)
}
.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 12 })
}
@Builder
nearbyTab() {
Column({ space: 10 }) {
Text('🅿️ 附近停车场 · 按距离排序').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
ForEach(LOT_LIST, (item: ParkingLot) => {
Column({ space: 8 }) {
Row({ space: 12 }) {
Text(item.icon).fontSize(28)
.width(52).height(52).borderRadius(14).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
Column({ space: 4 }) {
Text(item.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1)
Row({ space: 6 }) {
Text('📍 ' + item.dist).fontSize(8).fontColor(COLORS.sub)
Text(item.type).fontSize(8).fontColor(lotTypeColor(item.type))
.padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(5).backgroundColor(COLORS.chip)
}
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column({ space: 2 }) {
Text(item.price).fontSize(13).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
Text('/小时').fontSize(8).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row({ space: 8 }) {
Text('可用 ' + item.available + '/' + item.total).fontSize(9)
.fontColor(item.available > 0 ? COLORS.green : COLORS.red)
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.darkL)
.opacity(item.available > 0 && this.breath ? 0.6 : 1).animation({ duration: 600, iterations: 1 })
Column().layoutWeight(1)
if (item.available > 0) {
Text('预约 >').fontSize(9).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
} else {
Text('已满').fontSize(9).fontColor(COLORS.red)
}
}
.width('100%')
Row({ space: 0 }) {
Column()
.width(item.total > 0 ? (item.available / item.total * 100).toFixed(0) + '%' : '0%')
.height(4).borderRadius(2).backgroundColor(item.available > 0 ? COLORS.teal : COLORS.red)
Column().layoutWeight(1)
}
.width('100%').height(4).backgroundColor(COLORS.darkL).borderRadius(2)
}
.width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Start)
}, (item: ParkingLot) => item.id.toString())
}
.width('100%')
}
@Builder
reserveTab() {
Column({ space: 10 }) {
Text('📅 预约记录 · 时间轴').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
ForEach(this.resList, (item: Reservation) => {
Row({ space: 12 }) {
Column({ space: 2 }) {
Text(item.date).fontSize(10).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
Text(item.time).fontSize(7).fontColor(COLORS.text3)
}
.width(52).alignItems(HorizontalAlign.Center)
Column().width(2).height(60).backgroundColor(COLORS.line)
Column({ space: 4 }) {
Text(item.lot).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1)
Row({ space: 6 }) {
Text('🚗 ' + item.plate).fontSize(9).fontColor(COLORS.sub)
Text('⏱ ' + item.duration).fontSize(8).fontColor(COLORS.text3)
}
Text(item.status).fontSize(8).fontColor(resStatusColor(item.status))
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.chip)
.opacity(item.status !== '已完成' && this.breath ? 0.5 : 1).animation({ duration: 600, iterations: 1 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
.width('100%').padding(10).borderRadius(12).backgroundColor(COLORS.card)
}, (item: Reservation) => 'r' + item.id.toString())
}
.width('100%')
}
@Builder
vehicleTab() {
Column({ space: 10 }) {
Row({ space: 8 }) {
Text('🚗 我的车辆 · ' + this.vehicleList.length.toString() + '辆').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Column({ space: 1 }) {
Text('+').fontSize(14)
Text('添加').fontSize(7).fontColor(COLORS.text3)
}
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(10).backgroundColor(COLORS.chip)
.onClick(() => { this.addPlate = ''; this.addModel = ''; this.addColor = ''; this.addModal = true; })
}
.width('100%')
ForEach(this.vehicleList, (item: Vehicle, i: number) => {
Column({ space: 10 }) {
Row({ space: 12 }) {
Text(item.icon).fontSize(36)
.width(60).height(60).borderRadius(16).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
Column({ space: 4 }) {
Text(item.model).fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('颜色:' + item.color).fontSize(9).fontColor(COLORS.sub)
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (item.isDefault) {
Text('默认').fontSize(8).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(COLORS.teal)
}
}
.width('100%')
Row({ space: 8 }) {
Text('🪪 ' + item.plate).fontSize(16).fontColor(COLORS.blue).fontWeight(FontWeight.Bold)
Column().layoutWeight(1)
Text('✏️').fontSize(12)
.onClick(() => { this.editIdx = i; this.editPlate = item.plate; this.editModel = item.model; this.editModal = true; })
Text('🗑️').fontSize(12).margin({ left: 8 })
.onClick(() => { this.delIdx = i; this.delModal = true; })
}
.width('100%')
}
.width('100%').padding(14).borderRadius(14).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Start)
}, (item: Vehicle) => 'v' + item.plate)
}
.width('100%')
}
@Builder
recordTab() {
Column({ space: 10 }) {
Text('📋 停车记录 · 近6月消费(元)').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Row({ space: 6 }) {
ForEach(FEE_DATA, (v: number, i: number) => {
Column({ space: 4 }) {
Text(v === 0 ? '—' : v.toString()).fontSize(8).fontColor(COLORS.teal)
Column().width('100%').height(v === 0 ? 3 : v).borderRadius(5)
.backgroundColor(i === 5 ? COLORS.teal : COLORS.blue)
Text(MONTHS[i]).fontSize(8).fontColor(COLORS.text3)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (v: number, i: number) => 'fee' + i.toString())
}
.width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
.alignItems(VerticalAlign.Bottom).height(125)
Text('最近的停车记录').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
ForEach(RECORD_LIST, (item: ParkRecord) => {
Column({ space: 6 }) {
Row({ space: 10 }) {
Text('🅿️').fontSize(18).width(38).height(38).borderRadius(10).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
Column({ space: 3 }) {
Text(item.lot).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold).maxLines(1)
Row({ space: 6 }) {
Text('🚗 ' + item.plate).fontSize(8).fontColor(COLORS.sub)
Text('⏱ ' + item.duration).fontSize(8).fontColor(COLORS.text3)
}
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column({ space: 2 }) {
Text(item.fee).fontSize(13).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
Text(item.date).fontSize(8).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
}
.width('100%').padding(12).borderRadius(14).backgroundColor(COLORS.card)
.alignItems(HorizontalAlign.Start)
}, (item: ParkRecord) => 'rec' + item.id.toString())
}
.width('100%')
}
@Builder
walletTab() {
Column({ space: 12 }) {
Text('💳 停车钱包').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column({ space: 12 }) {
Row({ space: 12 }) {
Text('💳').fontSize(32)
.width(56).height(56).borderRadius(28).backgroundColor('rgba(255,255,255,0.2)').textAlign(TextAlign.Center)
Column({ space: 4 }) {
Text('钱包余额').fontSize(10).fontColor('#D2F0F0')
Text('¥328.50').fontSize(26).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
}
.width('100%')
Row({ space: 8 }) {
Column() { Text('充值').fontSize(10).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor('rgba(255,255,255,0.2)')
.justifyContent(FlexAlign.Center)
Column() { Text('账单').fontSize(10).fontColor('#FFFFFF') }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor('rgba(255,255,255,0.15)')
.justifyContent(FlexAlign.Center)
}
.width('100%')
}
.width('100%').padding(16).borderRadius(16)
.linearGradient({ angle: 135, colors: [['#0B7D7D', 0.0], ['#0EA5A5', 1.0]] })
Text('流水明细').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
ForEach(TXN_LIST, (item: Transaction) => {
Row({ space: 10 }) {
Text(item.type === '充值' ? '💰' : '🅿️').fontSize(16)
.width(36).height(36).borderRadius(10).backgroundColor(COLORS.dark).textAlign(TextAlign.Center)
Column({ space: 3 }) {
Text(item.type).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(item.date).fontSize(8).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column({ space: 2 }) {
Text(item.amount).fontSize(12).fontColor(feeColor(item.type)).fontWeight(FontWeight.Bold)
Text('余 ' + item.balance).fontSize(8).fontColor(COLORS.text3)
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(10).borderRadius(12).backgroundColor(COLORS.card)
}, (item: Transaction) => 'tx' + item.id.toString())
}
.width('100%')
}
@Builder
settingsTab() {
Column({ space: 10 }) {
Text('⚙️ 设置 · 偏好与安全').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Column({ space: 0 }) {
Row({ space: 12 }) {
Text('🔔').fontSize(18)
Text('车位提醒').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('开启').fontSize(9).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(COLORS.line)
Row({ space: 12 }) {
Text('🅿️').fontSize(18)
Text('自动缴费').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('已绑定').fontSize(9).fontColor(COLORS.teal)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(COLORS.line)
Row({ space: 12 }) {
Text('🗺️').fontSize(18)
Text('导航偏好').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('高德地图').fontSize(9).fontColor(COLORS.sub)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(COLORS.line)
Row({ space: 12 }) {
Text('🌙').fontSize(18)
Text('夜间模式').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('跟随系统').fontSize(9).fontColor(COLORS.sub)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(COLORS.line)
Row({ space: 12 }) {
Text('📱').fontSize(18)
Text('无感停车').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('已开通').fontSize(9).fontColor(COLORS.green)
.opacity(this.breath ? 0.6 : 1).animation({ duration: 600, iterations: 1 })
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(COLORS.line)
Row({ space: 12 }) {
Text('🎫').fontSize(18)
Text('我的卡券').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('5张').fontSize(9).fontColor(COLORS.orange)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
Divider().color(COLORS.line)
Row({ space: 12 }) {
Text('❓').fontSize(18)
Text('帮助与反馈').fontSize(11).fontColor(COLORS.title).layoutWeight(1)
Text('>').fontColor(COLORS.text3)
}
.width('100%').padding({ top: 12, bottom: 12, left: 4 })
}
.width('100%').padding({ left: 16, right: 16 }).borderRadius(14).backgroundColor(COLORS.card)
Text('v3.2.1 · 停易达智慧停车').fontSize(9).fontColor(COLORS.text3)
.alignSelf(ItemAlign.Center).margin({ top: 16, bottom: 8 })
}
.width('100%')
}
@Builder
tabBar() {
Row({ space: 0 }) {
ForEach(TAB_LIST, (t: TabMeta, i: number) => {
Column({ space: 3 }) {
Text(t.icon).fontSize(19)
Text(t.label).fontSize(9)
.fontColor(this.tabIdx === i ? COLORS.tabOn : COLORS.text3)
.fontWeight(this.tabIdx === i ? FontWeight.Bold : FontWeight.Normal)
}
.layoutWeight(1).padding({ top: 7, bottom: 6 })
.onClick(() => { this.tabIdx = i; })
}, (t: TabMeta, i: number) => t.label + i.toString())
}
.width('100%').backgroundColor(COLORS.card).borderRadius(16)
}
@Builder
modalOverlay(onClose: () => void) {
Column().width('100%').height('100%')
.backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
build() {
Stack({ alignContent: Alignment.Center }) {
Column() {
this.header()
Scroll() {
Column({ space: 10 }) {
if (this.tabIdx === 0) { this.nearbyTab() }
else if (this.tabIdx === 1) { this.reserveTab() }
else if (this.tabIdx === 2) { this.vehicleTab() }
else if (this.tabIdx === 3) { this.recordTab() }
else if (this.tabIdx === 4) { this.walletTab() }
else { this.settingsTab() }
}
.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 16 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
this.tabBar()
}
.width('100%').height('100%')
if (this.addModal) {
Stack({ alignContent: Alignment.Center }) {
this.modalOverlay(() => { this.addModal = false; })
Column({ space: 12 }) {
Text('🚗 添加车辆').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
TextInput({ placeholder: '车牌号(如 川A·DM856)', text: this.addPlate })
.onChange((v: string) => { this.addPlate = v; })
.fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
TextInput({ placeholder: '车型(如 理想 L7)', text: this.addModel })
.onChange((v: string) => { this.addModel = v; })
.fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
TextInput({ placeholder: '颜色(如 白色)', text: this.addColor })
.onChange((v: string) => { this.addColor = v; })
.fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
Row({ space: 10 }) {
Column() { Text('取消').fontSize(12).fontColor(COLORS.sub) }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.darkL)
.onClick(() => { this.addModal = false; })
Column() { Text('确认添加').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.teal)
.onClick(() => {
this.vehicleList.unshift(new Vehicle(
this.addPlate.length > 0 ? this.addPlate : '川A·00000',
this.addModel.length > 0 ? this.addModel : '未知车型',
this.addColor.length > 0 ? this.addColor : '未知', false, '🚗'));
this.addModal = false;
})
}
.width('100%')
}
.width('82%').constraintSize({ maxHeight: '80%' }).padding(16).borderRadius(16).backgroundColor(COLORS.card)
}
.width('100%').height('100%').zIndex(999)
}
if (this.editModal) {
Stack({ alignContent: Alignment.Center }) {
this.modalOverlay(() => { this.editModal = false; })
Column({ space: 12 }) {
Text('✏️ 修改车辆').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text(this.vehicleList[this.editIdx].plate).fontSize(10).fontColor(COLORS.sub)
TextInput({ placeholder: '车牌号', text: this.editPlate })
.onChange((v: string) => { this.editPlate = v; })
.fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
TextInput({ placeholder: '车型', text: this.editModel })
.onChange((v: string) => { this.editModel = v; })
.fontSize(12).padding(10).borderRadius(10).backgroundColor(COLORS.dark)
Row({ space: 10 }) {
Column() { Text('取消').fontSize(12).fontColor(COLORS.sub) }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.darkL)
.onClick(() => { this.editModal = false; })
Column() { Text('保存').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.teal)
.onClick(() => {
if (this.editPlate.length > 0) { this.vehicleList[this.editIdx].plate = this.editPlate; }
if (this.editModel.length > 0) { this.vehicleList[this.editIdx].model = this.editModel; }
this.editModal = false;
})
}
.width('100%')
}
.width('82%').constraintSize({ maxHeight: '80%' }).padding(16).borderRadius(16).backgroundColor(COLORS.card)
}
.width('100%').height('100%').zIndex(999)
}
if (this.delModal) {
Stack({ alignContent: Alignment.Center }) {
this.modalOverlay(() => { this.delModal = false; })
Column({ space: 14 }) {
Text('🗑️ 删除车辆?').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('「' + this.vehicleList[this.delIdx].plate + '」将从列表移除。').fontSize(10).fontColor(COLORS.sub)
Row({ space: 10 }) {
Column() { Text('再想想').fontSize(12).fontColor(COLORS.sub) }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.darkL)
.onClick(() => { this.delModal = false; })
Column() { Text('确认删除').fontSize(12).fontColor('#FFFFFF') }
.layoutWeight(1).padding({ top: 10, bottom: 10 }).borderRadius(10).backgroundColor(COLORS.red)
.onClick(() => {
this.vehicleList.splice(this.delIdx, 1);
this.delModal = false;
})
}
.width('100%')
}
.width('78%').constraintSize({ maxHeight: '80%' }).padding(16).borderRadius(16).backgroundColor(COLORS.card)
}
.width('100%').height('100%').zIndex(999)
}
}
.width('100%').height('100%').backgroundColor(COLORS.bg)
}
}
十五、开发环境搭建
15.1 安装 DevEco Studio
DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

15.2 选择安装路径
选择目标安装目录,建议安装到非系统盘,确认所需空间后点击「下一步」。

15.3 配置安装选项
设置环境变量,勾选创建桌面快捷方式、将 bin 文件夹添加到 PATH、添加上下文菜单,但是需要重启一下电脑使环境变量生效。

15.4 选择项目模板
新建一个空白模板,在模板市场中选择 Empty Ability。

15.5 配置项目信息
设置 API 为 24 的模板项目,项目名称 kfschool,Compatible SDK 6.1.1(24),设备类型勾选 Phone,点击完成后初始化项目,自动下载相关依赖。

十六、总结
通过对这款停易达智慧停车应用的深度剖析,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、防御性编程等多维度能力的综合体现。
在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将六个功能页面拆分为独立的 @Builder 构建器,每个页面采用完全不同的布局风格,实现"一页一型"的差异化设计;状态层利用 ArkUI 的 @State、@Observed 机制,实现了数据变化到 UI 刷新的自动响应,特别是车辆数据的真正增删改(unshift/改元素/splice),形成了完整的 CRUD 闭环。
在数据层面,五个 @Observed 数据类(ParkingLot/Reservation/Vehicle/ParkRecord/Transaction)以 constructor 构造函数方式实例化,完整描述了停易达的全部业务信息。三个工具函数(lotTypeColor 按车场类型着色、resStatusColor 按预约状态着色、feeColor 按流水类型着色)将业务数据转化为视觉色彩,实现了"数据即配色"的高级渲染模式,这是该应用最具特色的技术亮点之一。
在视觉层面,本应用独创了"停车概览头部"设计——顶部固定显示当前定位(📍万象城)+ 四栏统计(8附近车场/258可用车位/¥5均价/¥328钱包余额,竖线分隔)+ 优惠券横条(⚡新用户送¥30停车券 + 🔒自动缴费免扫码),无装饰带动效,信息密度高,直接还原真实智慧停车 App 的头部体验。应用还独创了"呼吸灯"动效——通过 aboutToAppear 中 setInterval 700ms 切换 breath 状态,可用车位标签、未完成预约状态、无感停车状态都使用 opacity 呼吸灯动画,营造出"车位实时更新、服务在线"的鲜活感。应用还独创了"车位占用进度条"——每个停车场卡片底部有一条 4px 高的进度条,宽度按 available/total 百分比计算,有车位用青蓝色、已满用红色,直观呈现车位占用率。钱包渐变卡使用 linearGradient 135 度从深青蓝到青蓝,营造出银行卡般的精致质感。六个 Tab 每个布局完全不同——附近车位横向车位卡、预约时间轴、车辆车牌卡、记录柱状图+列表、钱包余额+流水、设置开关清单,最大程度还原了真实智慧停车 App 的交互体验。统一的色彩系统是整个应用的视觉基石,青蓝主色搭配蓝色辅助色,配合天蓝底和多色语义色,营造出既科技又清爽的智慧交通氛围。
在交互层面,应用实现了完整的车辆管理 CRUD 闭环——添加车辆(3 个 TextInput 输入车牌/车型/颜色,vehicleList.unshift 新车辆)、修改车辆(2 个 TextInput 输入车牌/车型,修改数组指定元素)、删除车辆(二次确认,vehicleList.splice 删除指定元素),配合 @State 响应式刷新和 Stack 弹窗叠加(zIndex 999,mask 遮罩点击关闭),交互流畅且一致。车位占用进度条、时间轴预约、车牌卡管理、消费柱状图、钱包渐变卡、设置开关清单等细节,都体现了对智慧停车用户体验的深入考量。

标签: HarmonyOS · ArkTS · 华为 · 智慧停车 · 移动端开发
更多推荐


所有评论(0)