基于HarmonyOS API 24的花颜速递在线鲜花配送平台深度解析——粉绿双主题渐变头部、瀑布流双列与配送时间轴的完整实现

鲜花速递,新鲜直达,爱不缺席。本文完整拆解一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式框架构建的「花颜速递」在线鲜花配送应用,覆盖粉绿双主题配色系统、渐变头部、五Tab全差异化布局、七夕特惠Banner、花语分类网格、热销花束横滑、花语百科、筛选标签与排序条、瀑布流双列、配送概览四栏统计、配送路线时间轴、月度订单统计柱状图、收货地址管理、四动画状态联动、Stack弹窗叠加、加入购物车/修改地址/删除订单三大弹窗等核心技术点,附完整源码与运行效果截图。


一、引言:鲜花电商的数字化浪潮

在消费升级与情感经济双重驱动的今天,在线鲜花配送已经从传统的"节日买花、花店自取",演变为一种日常化、即时化、个性化的消费新形态。鲜花,以其"非标准化、强情感属性、高时效要求、易损耗"的特点,正在成为本地生活服务与电商融合的关键赛道。从情人节的红玫瑰到母亲节的康乃馨,从日常家居的百合到商务馈赠的混搭花篮,每一束鲜花都承载着送花人的情感与心意。然而,鲜花品类繁多、价格不透明、配送时效要求高、花材品质难保障,正成为用户获取可靠鲜花配送服务的现实痛点。

正是在这样的背景下,花颜速递类在线鲜花配送应用应运而生,并迅速成为鲜花电商的数字基础设施。一个优秀的鲜花配送应用,不仅需要覆盖从"选花"到"配送"再到"售后"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到浪漫而高效,让用户在指尖滑动间便能完成选花、加购、填地址、跟配送、查订单的全流程。

本文将深入剖析一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式开发框架构建的花颜速递在线鲜花配送应用。该应用以"首页"为核心,横向扩展出花束、配送、订单、我的五大功能模块,每个模块采用完全不同的布局风格——首页Banner+分类网格+热销横滑+花语百科、花束筛选标签+排序条+瀑布流双列、配送概览四栏+配送路线时间轴、订单月度柱状图+订单列表、我的用户卡+统计+地址管理+功能菜单,形成了一个完整闭环的鲜花配送生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、Stack弹窗叠加、四动画状态联动、渐变头部、瀑布流双列、配送时间轴、月度柱状图等关键技术点。

花颜速递应用整体运行效果 - 粉绿渐变头部与七夕特惠Banner及花语分类网格


二、应用整体架构概览

整个应用采用了"单入口 + 五Tab全差异化布局 + Stack弹窗叠加"的鲜花电商应用架构。入口组件 FlowerDeliveryApp 作为整个应用的根容器,负责管理当前激活的 Tab 状态、购物车数量、四个动画状态以及三个弹窗的显隐状态。

应用一共划分为五个一级页面:首页(HOME)、花束(FLOWER)、配送(DELIVERY)、订单(ORDER)以及我的(PROFILE)。与之前项目"页面布局风格统一或半差异化"不同,本应用的五个 Tab 每个布局完全不同——首页采用Banner+分类网格+热销横滑+花语百科、花束采用筛选标签+排序条+瀑布流双列、配送采用概览四栏+配送路线时间轴、订单采用月度柱状图+订单列表、我的采用用户卡+统计+地址管理+功能菜单,这种"一页一型"的差异化设计最大程度还原了真实鲜花电商 App 的交互体验。底部导航采用单排五Tab布局,简洁统一。

在数据层面,应用定义了一套完整的类型系统,包括色彩主题、鲜花元数据、分类元数据、配送任务元数据、订单元数据、地址元数据、月度数据接口等,并通过常量数组将数据与 UI 表现层解耦。所有业务数据以 @Observed 装饰的可观察类为载体(FlowerItem/CategoryItem/DeliveryTask/OrderRecord/AddressInfo),配合 10 束鲜花、8 个分类、6 个配送任务、10 条订单、3 个地址的模拟数据,模拟了一个真实的鲜花配送数据环境。所有数据类都带有 constructor 构造函数,使用 new FlowerItem(...) 方式实例化。MonthData 采用普通 interface 接口而非 @Observed 类,因为月度数据是静态统计数据无需响应式更新。

在交互层面,应用通过 Stack 布局叠加实现了三类弹窗的显示:加入购物车、修改地址、删除订单。与之前项目"每个弹窗独立 Stack 叠加"不同,本应用独创了 modalOverlay 统一封装——一个 @Builder modalOverlay(onClose) 构建器,内部通过 if-else 判断当前显示哪个弹窗内容(addToCartModalContent/editAddressModalContent/deleteOrderModalContent),外层统一全屏 rgba(0,0,0,0.4) 遮罩 + 居中 90% 宽卡片 maxHeight 80%,点击遮罩同时关闭所有三个弹窗状态。这种统一封装大幅减少了重复代码,三个弹窗共享同一套遮罩与卡片样式。加入购物车弹窗确认后 cartCount += 1,修改地址弹窗展示地址信息只读,删除订单弹窗二次确认后关闭,形成了完整的交互闭环。

在视觉层面,应用独创了"粉绿双主题渐变头部"设计——顶部 header 使用 linearGradient 135 度从 primary #E91E63 粉红到 accent #4CAF50 绿色的斜向渐变,"花颜速递"白色标题 + "新鲜直达·爱不缺席"白色副标题 0.85 透明度,是本应用最具辨识度的视觉元素。首页 Banner 也使用 linearGradient 135 度从粉红到浅粉的渐变 + shadow 阴影(radius 12 color #FFC1CC offsetY 4),营造出七夕特惠的浪漫促销氛围。应用还独创了"四动画状态联动"设计——aboutToAppear 中 setInterval 1500ms 同步切换四个布尔动画状态(breathAnim/blinkAnim/pulseAnim/floatAnim),分别用于不同元素:blinkAnim 用于配送中圆点缩放(scale 1.3)、pulseAnim 用于热销 tag/已完成订单状态/VIP 会员标签缩放(scale 1.05)、floatAnim 用于月度柱状图缩放(scale 1.02),四个动画同步切换营造出整体协调的动态感。应用还独创了"瀑布流双列左右异色"设计——花束页用 filter 按花 id 字符码奇偶分列(左列奇数 id f1/f3/f5/f7/f9,右列偶数 id f2/f4/f6/f8/f10),左列 tag 背景 accent 绿色、按钮背景 primary 粉红,右列 tag 背景 primary 粉红、按钮背景 accent 绿色,左右列颜色交错形成视觉韵律,是本应用最具特色的布局设计。

花颜速递加入购物车弹窗运行效果 - 弹窗叠加花束瀑布流双列页

下面逐页展示应用的五大功能模块运行效果。


三、花束页:筛选标签、排序条与瀑布流双列

花束页以"鲜花选购"为核心,独创"筛选标签+排序条+瀑布流双列"的三层布局,是五个 Tab 中布局最具特色的页面。

花束模块 - 筛选标签横滑与瀑布流双列左右异色布局

页面顶部第一部分是筛选标签,使用 Scroll horizontal 横向滚动 8 个标签(全部/玫瑰/百合/康乃馨/向日葵/郁金香/干花/花篮),每个标签白底圆角 16,padding 左右 12 上下 6,margin 右 8,用户可横向滑动选择品类。

第二部分是排序条,横向排列四个排序选项:综合排序(primary 粉红加粗,当前选中)、价格(textSub 灰)、销量(textSub 灰)、新品(textSub 灰),padding 左右 16 上下 8,用户可点击切换排序方式。

第三部分是瀑布流双列,这是本应用最具特色的布局设计。使用两个 Column 左右并列(各 layoutWeight 1,左列 margin 右 5,右列 margin 左 5),左列通过 this.flowers.filter((f: FlowerItem) => f.id.charCodeAt(1) % 2 === 1) 筛选奇数 id 的花(f1 红玫瑰/f3 向日葵/f5 郁金香/f7 薰衣草/f9 满天星,共 5 束),右列通过 % 2 === 0 筛选偶数 id 的花(f2 康乃馨/f4 百合/f6 花篮/f8 绣球/f10 洋桔梗,共 5 束)。每个花束卡片包含:图标 44 号 + 花名(加粗 13 号 maxLines 1)+ tag 标签 + 评分(★4.X warning 橙色)+ 价格(primary 粉红 15 号加粗 layoutWeight 1)+ 圆形按钮(28×28 圆角 14)。

特别的是,左右两列的 tag 和按钮颜色不同——左列 tag 背景 accent 绿色白字、按钮背景 primary 粉红;右列 tag 背景 primary 粉红白字、按钮背景 accent 绿色。这种左右异色设计让瀑布流双列形成视觉韵律,避免了单调。点击按钮触发 addModal 加入购物车弹窗,selectedFlower 记录当前花束信息。

瀑布流双列用 filter 按 id 字符码奇偶分列的实现方式,既保证了左右列花束数量均衡(各 5 束),又通过左右异色形成了独特的视觉风格,是本应用最具工程巧思的布局设计。


四、配送页:概览四栏与配送路线时间轴

配送页以"鲜花配送"为核心,独创"概览四栏+配送路线时间轴"的复合布局。

配送模块 - 配送概览四栏统计与配送路线时间轴

页面顶部第一部分是配送概览四栏统计卡片,横向排列四个数据指标,每栏 layoutWeight 1 居中对齐:今日配送 6(primary 粉红 28 号加粗)、已送达 2(accent 绿色 28 号加粗)、配送中 2(warning 橙色 28 号加粗)、待取件 2(danger 红色 28 号加粗),每个指标下方配文字标签(今日配送/已送达/配送中/待取件)textSub 11 号。四栏统计一目了然地展示了今日配送概况,四种颜色对应四种配送状态,形成"以色定状态"的语义着色。

下方第二部分是配送路线时间轴,标题"配送路线"加粗 16 号。遍历 deliveries 6 个配送任务,每个任务采用"时间轴+配送卡"的横向布局:

左侧时间轴列(width 24,height 100%,居中对齐):Circle 圆点 12×12,fill 颜色按状态动态着色(已送达 accent 绿/配送中 warning 橙/待取件 danger 红),配送中圆点带 blinkAnim 缩放动画(this.blinkAnim && task.status === '配送中' ? { x: 1.3, y: 1.3 } : { x: 1.0, y: 1.0 },duration 500ms),营造出"正在配送"的动态脉冲感;圆点下方 Line 竖线 2 宽 layoutWeight 1 border 色,连接上下任务形成时间轴线。

右侧配送卡(layoutWeight 1,padding 12,白底圆角 12,margin 左 8 下 10):订单号(textSub 10 号 layoutWeight 1)+ 状态(动态着色加粗 10 号,右对齐);花名(加粗 14 号,margin 上 6);地址行(📍图标 + 地址 textSub 11 号 layoutWeight 1 maxLines 1 省略,margin 上 6);收件人行(收件人: XXX textSub 11 号 + 时间 textSub 11 号 margin 左 12,margin 上 4);骑人行(🚴骑手 textSub 10 号 + 距离 X.Xkm primary 粉红加粗 10 号 margin 左 12,margin 上 4)。

6 个配送任务从 08-20 当天排列,状态涵盖配送中(2 条:红玫瑰99朵李师傅2.3km/百合花束周师傅3.6km)、待取件(2 条:康乃馨赵师傅5.8km/郁金香吴师傅7.2km)、已送达(2 条:向日葵孙师傅1.2km/花篮郑师傅9.5km),配送时间从 08:00 到 16:30,骑手从李师傅到郑师傅共 6 位,距离从 1.2km 到 9.5km,完整还原了一天的鲜花配送路线。配送中圆点的 blinkAnim 缩放动画让静态的时间轴有了"正在配送"的动态感,是本应用动效设计的亮点之一。


五、订单页:月度统计柱状图与最近订单列表

订单页以"鲜花订单"为核心,独创"月度订单统计柱状图+最近订单列表"的复合布局。

订单模块 - 月度订单统计柱状图与最近订单列表

页面顶部第一部分是月度订单统计柱状图卡片,标题"月度订单统计"加粗 15 号 margin 下 16。下方 Row 横向排列 6 个月柱子,每栏 layoutWeight 1 居中对齐,alignItems Bottom 让柱子从底部生长,卡片高度 180。每个柱子包含:

  • 柱子本体:width 24,height = data.orders * 1.8(订单数乘以 1.8 作为像素高度),backgroundColor 按订单数分三档动态着色(orders > 80 用 danger 红色:5月88/8月95;orders > 60 用 primary 粉红:4月62/7月72;其余用 accent 绿色:3月45/6月56),borderRadius 4,justifyContent End 让文字从底部对齐,padding 上 2,柱子内白字显示订单数(textWhite 9 号)。柱子带 floatAnim 缩放动画(this.floatAnim ? { x: 1.02, y: 1.02 } : { x: 1.0, y: 1.0 },duration 600ms),营造出轻微的浮动感。
  • 月份标签:data.month(3月/4月/5月/6月/7月/8月)textSub 9 号,margin 上 4。

6 个月数据从 3 月到 8 月,订单数从 45 到 95,营收从 12800 到 28900,5 月(88 单,母亲节康乃馨高峰)和 8 月(95 单,七夕玫瑰高峰)用红色突出显示,符合鲜花电商"节日驱动"的销售规律。三档着色(红/粉/绿)让订单量高低一目了然,柱子内白字显示数值避免了额外标注,floatAnim 缩放动画让静态柱状图有了轻微的呼吸感。

下方第二部分是最近订单列表,标题行"最近订单"加粗 16 号 layoutWeight 1 + "共10单"textSub 12 号(右对齐)。遍历 orders 10 条订单,每条订单横向布局:💐图标 28 号 margin 右 12 + 订单信息列(layoutWeight 1,padding 12,白底圆角 12)。

订单信息列包含:订单号行(订单号 textSub 10 号 layoutWeight 1 + 状态:已完成 accent 绿色加粗带 pulseAnim 缩放动画 this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 } duration 600ms / 已取消 danger 红色加粗,右对齐);花名行(花名 x 数量 text 13 号,margin 上 4);日期金额行(日期 textSub 11 号 layoutWeight 1 + 金额 primary 粉红 15 号加粗,右对齐,margin 上 4);按钮行(仅已完成订单显示:白底边框按钮 60×24 + 删除按钮 danger 红色 60×24 margin 左 8,点击删除按钮触发 delModal 删除订单弹窗,selectedOrderId 记录当前订单号;已取消订单无按钮行)。

10 条订单从 08-19 到 08-10 按时间倒序排列,9 条已完成(红玫瑰/康乃馨/向日葵/百合/花篮/薰衣草/绣球/满天星/洋桔梗),1 条已取消(郁金香 08-15),金额从 ¥98 到 ¥368,数量从 1 到 3,完整还原了用户的鲜花订单历史。已完成订单状态的 pulseAnim 缩放动画让订单列表有了"活跃"的动态感,删除按钮触发二次确认弹窗,交互完整。


六、我的页:用户卡、统计、地址管理与功能菜单

我的页以"个人中心"为核心,采用"用户卡+四栏统计+收货地址+功能菜单"的四层布局。

我的模块 - 用户卡、四栏统计、收货地址管理与功能菜单

页面顶部第一部分是用户卡,横向布局:Circle 头像 56×56 fill primaryLight 浅粉 + 用户信息列(layoutWeight 1,margin 左 12,左对齐):姓名"张小美"加粗 18 号 + 标签行(VIP 会员标签 primary 粉底白字圆角 8 padding 左右 6 上下 2,带 pulseAnim 缩放动画 this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 } duration 700ms + "积分 2680"textSub 10 号 margin 左 8,margin 上 4)。用户卡白底圆角 16 padding 16,margin 下 16。

第二部分是四栏统计卡片,横向排列四个数据指标,每栏 layoutWeight 1 居中对齐:总订单 10(primary 粉红 22 号加粗)、积分 2680(accent 绿色 22 号加粗)、优惠券 8(warning 橙色 22 号加粗)、收藏 12(danger 红色 22 号加粗),每个指标下方配文字标签(总订单/积分/优惠券/收藏)textSub 10 号 margin 上 2。四栏统计白底圆角 16 padding 上下 16,margin 下 16。

第三部分是收货地址管理,标题行"📍 收货地址"加粗 15 号 layoutWeight 1 + "管理"primary 粉红 12 号(右对齐),margin 下 12。遍历 addresses 3 个地址,每个地址横向布局:地址信息列(layoutWeight 1,左对齐)+ 编辑按钮(28×28 白底边框圆角 14,点击触发 editModal 修改地址弹窗,selectedAddress 记录当前地址)。

地址信息列包含:姓名行(姓名加粗 13 号 + 电话 textSub 12 号 margin 左 8 + 默认标签(仅 isDefault 为 true 的地址显示,accent 绿底白字圆角 4 padding 左右 4 上下 1,margin 左 6)+ tag 标签(tagBg 粉底 tagText 粉字圆角 4 padding 左右 4 上下 1,margin 左 6,如"公司"/“家”/“收件人”));地址行(地址 textSub 12 号,margin 上 4)。

3 个地址:张小美 1388888 朝阳区望京SOHO T1-1802(默认,公司)、张小美 1388888 海淀区中关村大街1号院(家)、王女士 139****6666 西城区西单大悦城5层(收件人),完整还原了用户的收货地址管理。默认标签用 accent 绿色突出,tag 标签用粉底粉字区分地址类型,编辑按钮触发修改地址弹窗。

第四部分是功能菜单,遍历 5 项(我的收藏/优惠券/积分商城/帮助中心/关于我们),每项横向布局:功能名 text 14 号 layoutWeight 1 + ">"textSub 14 号(右箭头),白底圆角 12 padding 上下 14 左右 16,margin 下 8。功能菜单简洁明了,用户可点击进入对应功能页面。

我的页四层布局(用户卡+统计+地址+菜单)完整覆盖了个人中心的核心功能,VIP 会员标签的 pulseAnim 缩放动画让用户卡有了"会员尊贵"的动态感,地址管理的默认标签与 tag 标签双标签设计让地址类型一目了然。


七、色彩主题系统:粉绿双主题与渐变头部的浪漫视觉语言

7.1 色彩接口定义

任何一款设计精良的应用,背后都有一套严谨的色彩规范。该应用首先通过一个 ColorPalette 接口,将整个应用所需的色彩抽象为一个结构化的契约,共 24 个字段:

interface ColorPalette {
  bg: string; cardBg: string;
  primary: string; primaryLight: string;
  accent: string; accentLight: string;
  text: string; textSub: string; textWhite: string;
  border: string; tagBg: string; tagText: string; shadow: string;
  success: string; warning: string; danger: string;
  chart1: string; chart2: string; chart3: string;
  chart4: string; chart5: string; chart6: string;
}
  • bg:页面整体背景色 #FFF0F5,粉白底,营造鲜花电商的浪漫柔美氛围。
  • primary:主色调 #E91E63,粉红,用于按钮、强调文字、激活 Tab、价格、渐变头部起始色等高优先级视觉元素,是鲜花电商的标志性色彩(玫瑰粉)。
  • accent:辅助色 #4CAF50,绿色,用于渐变头部结束色、左列 tag、右列按钮、已送达状态、积分统计等,与粉红形成"花+叶"的自然配色,象征鲜花与绿叶。
  • textWhite:白色 #FFFFFF,用于渐变头部文字、Banner 文字、tag 文字等深色背景上的文字。
  • shadow:阴影色 #FFC1CC,浅粉,用于 Banner 阴影,营造浪漫柔和的阴影效果。
  • tagBg/tagText:标签底色 #FCE4EC 浅粉 / 标签文字色 #E91E63 粉红,用于地址 tag 标签等。
  • chart1-chart6:6 种图表色(粉红/绿/橙/紫/蓝/红),预留给数据可视化使用。

花颜速递首页模拟器运行效果 - 粉绿渐变头部与七夕特惠Banner及花语分类网格真实渲染

7.2 色彩实例化

const COLORS: ColorPalette = {
  bg: '#FFF0F5', cardBg: '#FFFFFF',
  primary: '#E91E63', primaryLight: '#F8BBD0',
  accent: '#4CAF50', accentLight: '#A5D6A7',
  text: '#2D2D2D', textSub: '#888888', textWhite: '#FFFFFF',
  border: '#F0F0F0', tagBg: '#FCE4EC', tagText: '#E91E63', shadow: '#FFC1CC',
  success: '#4CAF50', warning: '#FF9800', danger: '#F44336',
  chart1: '#E91E63', chart2: '#4CAF50', chart3: '#FF9800',
  chart4: '#9C27B0', chart5: '#2196F3', chart6: '#F44336',
};

主色 #E91E63 是一种浪漫的粉红,既契合"鲜花速递"主题(玫瑰粉、浪漫、柔美的标志性色彩),又具有活力、甜美的气质。辅助色 #4CAF50 绿色与粉红形成"花+叶"的自然配色,象征鲜花与绿叶,是本应用最具辨识度的色彩组合。bg #FFF0F5 粉白底与主色同暖色系,营造出鲜花电商的浪漫柔美氛围。

值得注意的是,本应用的渐变设计是色彩系统的核心亮点——header 使用 linearGradient 135 度从 primary #E91E63 粉红到 accent #4CAF50 绿色的斜向渐变,"花颜速递"白色标题 + "新鲜直达·爱不缺席"白色副标题,粉绿渐变从左到右(135 度)自然过渡,象征"从花朵到绿叶"的完整鲜花生态,是本应用最具辨识度的视觉元素。首页 Banner 也使用 linearGradient 135 度从粉红到浅粉 primaryLight 的渐变 + shadow 阴影(radius 12 color #FFC1CC offsetY 4),营造出七夕特惠的浪漫促销氛围。

本应用的动态颜色逻辑也十分丰富——配送状态按状态分四档着色(已送达 accent 绿/配送中 warning 橙/待取件 danger 红),月度柱状图按订单数分三档着色(>80 danger 红/>60 primary 粉/其余 accent 绿),订单状态按状态分两档着色(已完成 accent 绿/已取消 danger 红),瀑布流双列左右异色(左列 tag 绿按钮粉/右列 tag 粉按钮绿),形成了"以色定状态、以色定量级、以色定列"的多层语义着色模式。formatPrice 工具函数统一价格格式为 ¥XX.XX,价格统一使用 primary 粉红加粗,形成了一致的价格视觉语言。这种将所有色彩集中管理、配合渐变与动态着色的做法保证了全局一致性,降低了维护成本。

花颜速递订单页模拟器运行效果 - 月度订单统计柱状图三档着色与最近订单列表真实渲染


八、类型定义:构建严谨的数据契约

应用通过一组带 constructor 的 @Observed 类,为每一类业务对象建立了明确的数据契约。

8.1 鲜花、分类、配送元数据

@Observed
class FlowerItem {
  id: string; name: string; price: number; originalPrice: number;
  image: string; category: string; tag: string; sales: number; rating: number; stock: number;
  constructor(...) { ... }
}
@Observed
class CategoryItem {
  id: string; name: string; icon: string; count: number;
  constructor(...) { ... }
}
@Observed
class DeliveryTask {
  id: string; orderNo: string; flowerName: string; recipient: string;
  phone: string; address: string; status: string; time: string; courier: string; distance: number;
  constructor(...) { ... }
}

FlowerItem 描述鲜花的 ID、名称、价格、原价、图标、品类、标签、销量、评分、库存,用 price/originalPrice 双价格字段体现折扣,sales/rating/stock 三维度体现商品热度与品质。CategoryItem 描述分类的 ID、名称、图标、款数,用 count 字段体现该分类下的鲜花数量。DeliveryTask 描述配送任务的 ID、订单号、花名、收件人、电话、地址、状态、时间、骑手、距离,用 status 字段体现配送流转状态,distance 字段体现配送距离,courier 字段体现配送骑手。

8.2 订单、地址元数据与月度数据接口

@Observed
class OrderRecord {
  id: string; orderNo: string; flowerName: string; amount: number;
  status: string; date: string; count: number;
  constructor(...) { ... }
}
@Observed
class AddressInfo {
  id: string; name: string; phone: string; address: string; isDefault: boolean; tag: string;
  constructor(...) { ... }
}
interface MonthData {
  month: string; orders: number; revenue: number;
}
function formatPrice(price: number): string {
  return '¥' + price.toFixed(2);
}

OrderRecord 描述订单的 ID、订单号、花名、金额、状态、日期、数量,用 status 字段体现订单状态(已完成/已取消),count 字段体现购买数量。AddressInfo 描述地址的 ID、姓名、电话、地址、是否默认、标签,用 isDefault 布尔字段体现默认地址,tag 字段体现地址类型(公司/家/收件人)。MonthData 采用普通 interface 接口而非 @Observed 类,包含月份、订单数、营收三个字段,因为月度数据是静态统计数据无需响应式更新,这种"按需选择 @Observed 或 interface"的设计体现了对响应式机制的精准理解。formatPrice 工具函数统一价格格式为 ¥XX.XX,toFixed(2) 保证两位小数,是本应用唯一的工具函数。所有数据类都带有 constructor 构造函数,使用 new ClassName(...) 方式实例化,配合 @Observed 装饰器实现响应式更新。


九、配置驱动:常量数据与工具函数

9.1 Tab 导航配置

private tabItems: string[] = ['首页', '花束', '配送', '订单', '我的'];

与之前项目"TabMeta 对象数组配置"不同,本应用的 Tab 导航采用最简的字符串数组配置,仅包含 Tab 名称,不包含图标和颜色。底部 tabBar 通过 ForEach 遍历 tabItems,每个 Tab 用 Column 包裹文字(11 号,激活态 primary 粉红加粗,非激活 textSub 灰),height 48,layoutWeight 1 均分,点击切换 currentTab。这种极简配置让底部导航简洁统一,五 Tab 单排布局不拥挤,符合鲜花电商 App 的底部导航习惯。

9.2 模拟数据与工具函数

private flowers: FlowerItem[] = [ new FlowerItem('f1', '红玫瑰99朵', 299, 399, '🌹', '玫瑰', '热销', 2380, 4.9, 56), ... ];
private categories: CategoryItem[] = [ new CategoryItem('c1', '玫瑰', '🌹', 86), ... ];
private deliveries: DeliveryTask[] = [ new DeliveryTask('d1', 'DD20260820001', '红玫瑰99朵', '张小美', '138****8888', '朝阳区望京SOHO T1-1802', '配送中', '10:30-11:30', '李师傅', 2.3), ... ];
private orders: OrderRecord[] = [ new OrderRecord('o1', 'ORD20260819001', '红玫瑰99朵', 299, '已完成', '08-19', 1), ... ];
private addresses: AddressInfo[] = [ new AddressInfo('a1', '张小美', '138****8888', '朝阳区望京SOHO T1-1802', true, '公司'), ... ];
private monthData: MonthData[] = [ { month: '3月', orders: 45, revenue: 12800 }, ... ];

六组常量数据承载了花颜速递的全部业务信息:flowers 10 束鲜花(覆盖玫瑰/康乃馨/向日葵/百合/郁金香/花篮/干花/绣球/满天星/洋桔梗十大品类,价格从 ¥98 到 ¥368,评分从 4.6 到 4.9,库存从 18 到 88,销量从 430 到 2380)、categories 8 个分类(玫瑰86款/百合52款/康乃馨68款/向日葵45款/郁金香38款/绣球22款/干花54款/花篮18款)、deliveries 6 个配送任务(状态涵盖配送中/待取件/已送达,骑手从李师傅到郑师傅,距离从 1.2km 到 9.5km)、orders 10 条订单(9 已完成 1 已取消,金额从 ¥98 到 ¥368)、addresses 3 个地址(1 默认 2 非默认,标签公司/家/收件人)、monthData 6 个月数据(订单数从 45 到 95,营收从 12800 到 28900)。所有 @Observed 类数据使用 new ClassName(...) 构造函数实例化,MonthData 使用对象字面量。formatPrice 工具函数统一价格格式,是本应用唯一的工具函数。


十、动态颜色与四动画状态:数据驱动的视觉映射与联动动效

应用通过动态颜色逻辑和四动画状态联动,将业务数据转化为视觉色彩与动态效果,实现"数据即配色、状态即动效"。

10.1 动态颜色逻辑

本应用的动态颜色逻辑分布在多个维度:

  • 配送状态着色task.status === '已送达' ? COLORS.accent : task.status === '配送中' ? COLORS.warning : COLORS.danger,按配送状态分三档(已送达绿/配送中橙/待取件红),时间轴圆点和状态文字统一着色。
  • 月度柱状图着色data.orders > 80 ? COLORS.danger : data.orders > 60 ? COLORS.primary : COLORS.accent,按订单数分三档(>80 红/>60 粉/其余绿),5 月 88 单和 8 月 95 单用红色突出,符合节日高峰规律。
  • 订单状态着色order.status === '已完成' ? COLORS.accent : COLORS.danger,按订单状态分两档(已完成绿/已取消红)。
  • 瀑布流左右异色:左列 tag 背景 accent 绿、按钮背景 primary 粉;右列 tag 背景 primary 粉、按钮背景 accent 绿,通过 filter 分列后分别设置颜色,形成左右交错的视觉韵律。
  • 地址标签双色系:默认标签 accent 绿底白字,tag 标签 tagBg 粉底 tagText 粉字,两种标签颜色区分地址属性(默认/类型)。

10.2 四动画状态联动

aboutToAppear(): void {
  this.timerId = setInterval(() => {
    this.breathAnim = !this.breathAnim;
    this.blinkAnim = !this.blinkAnim;
    this.pulseAnim = !this.pulseAnim;
    this.floatAnim = !this.floatAnim;
  }, 1500);
}

aboutToAppear 中启动 setInterval 1500ms 同步切换四个布尔动画状态(breathAnim/blinkAnim/pulseAnim/floatAnim),aboutToDisappear 中 clearInterval 清理定时器避免内存泄漏。四个动画状态分别用于不同元素:

  • blinkAnim:用于配送中圆点缩放(this.blinkAnim && task.status === '配送中' ? { x: 1.3, y: 1.3 } : { x: 1.0, y: 1.0 },duration 500ms),仅配送中的圆点放大 1.3 倍,营造出"正在配送"的脉冲闪烁感,是本应用最具动态感的动效。
  • pulseAnim:用于热销 tag 缩放(首页热销花束卡片的 tag,scale 1.05,duration 800ms)、已完成订单状态缩放(订单列表的已完成状态标签,scale 1.05,duration 600ms)、VIP 会员标签缩放(我的页用户卡的 VIP 标签,scale 1.05,duration 700ms),三个元素共用同一 pulseAnim 状态,缩放比例相同但 duration 不同,营造出整体协调的脉冲感。
  • floatAnim:用于月度柱状图缩放(this.floatAnim ? { x: 1.02, y: 1.02 } : { x: 1.0, y: 1.0 },duration 600ms),所有柱子同步轻微放大 1.02 倍,营造出"数据浮动"的轻微呼吸感。
  • breathAnim:定义但未在 UI 中使用(预留扩展),体现了"预留动画状态"的工程习惯。

四个动画状态同步切换(1500ms 周期),分别用于不同元素,形成了整体协调的联动动效体系。blinkAnim 的配送中圆点脉冲、pulseAnim 的标签缩放、floatAnim 的柱状图浮动,三种动效各司其职,让静态的页面有了"鲜活"的动态感,是本应用动效设计的核心亮点。


十一、入口组件:应用的骨架与五Tab全差异化布局

11.1 组件声明与状态

@Entry
@Component
struct FlowerDeliveryApp {
  @State currentTab: number = 0;
  @State showAddToCartModal: boolean = false;
  @State showEditAddressModal: boolean = false;
  @State showDeleteOrderModal: boolean = false;
  @State selectedFlower: FlowerItem = new FlowerItem('f0', '', 0, 0, '', '', '', 0, 0, 0);
  @State selectedAddress: AddressInfo = new AddressInfo('a0', '', '', '', false, '');
  @State selectedOrderId: string = '';
  @State cartCount: number = 3;
  @State breathAnim: boolean = false;
  @State blinkAnim: boolean = false;
  @State pulseAnim: boolean = false;
  @State floatAnim: boolean = false;
  private timerId: number = -1;
}

14 个 @State 状态分六类:导航状态(currentTab)、弹窗控制状态(showAddToCartModal/showEditAddressModal/showDeleteOrderModal)、弹窗数据状态(selectedFlower/selectedAddress/selectedOrderId,selectedFlower 和 selectedAddress 用 new ClassName(...) 初始化空对象避免空引用)、业务数据状态(cartCount)、动画状态(breathAnim/blinkAnim/pulseAnim/floatAnim)、定时器引用(private timerId)。private 字段(flowers/categories/deliveries/orders/addresses/monthData/tabItems)存储静态模拟数据,不触发响应式更新。

11.2 生命周期与四动画联动

aboutToAppear(): void {
  this.timerId = setInterval(() => {
    this.breathAnim = !this.breathAnim;
    this.blinkAnim = !this.blinkAnim;
    this.pulseAnim = !this.pulseAnim;
    this.floatAnim = !this.floatAnim;
  }, 1500);
}
aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

aboutToAppear 中启动 setInterval 1500ms 同步切换四个动画状态,实现四动画联动。aboutToDisappear 中判断 timerId 不为 -1 时 clearInterval 并重置为 -1,避免内存泄漏和重复启动。这种"生命周期管理定时器"的模式是 ArkUI 动效的标准实践。

11.3 粉绿渐变头部(@Builder header)

顶部头部通过 @Builder header() 抽取,Row 横向布局,height 56,padding 左右 20,linearGradient 135 度从 primary #E91E63 粉红到 accent #4CAF50 绿色,alignItems Center。内部 Column 左对齐:"花颜速递"白色 22 号加粗 + "新鲜直达·爱不缺席"白色 11 号 0.85 透明度 margin 上 2。粉绿渐变头部是本应用最具辨识度的视觉元素,从粉红到绿色的 135 度斜向渐变象征"从花朵到绿叶"的完整鲜花生态。

11.4 底部 Tab 导航与 Stack 弹窗

底部导航通过 @Builder tabBar() 构建,Column 包裹 Row,Row 内 ForEach 遍历 tabItems 5 个 Tab,每个 Tab Column 包裹文字(11 号,激活 primary 粉红加粗,非激活 textSub 灰),height 48 layoutWeight 1 均分,点击切换 currentTab。底部 0.5px border 顶部分隔线。

build() 使用 Stack 布局,主 Column 包含 header + if-else 切换 5 个 @Builder 页面(currentTab 0-4 对应 homeTab/flowerTab/deliveryTab/orderTab/profileTab)+ tabBar,width/height 100%,bg 粉白底。弹窗条件渲染:if (this.showAddToCartModal || this.showEditAddressModal || this.showDeleteOrderModal) 任一为 true 时渲染 this.modalOverlay(...),modalOverlay 内部 if-else 判断当前显示哪个弹窗内容,点击遮罩同时关闭所有三个弹窗状态。这种"统一 modalOverlay 封装 + 条件渲染"的模式大幅减少了重复代码,三个弹窗共享同一套遮罩与卡片样式。


十二、关键技术特性对比

特性维度 实现方式 技术要点 设计价值
色彩管理 ColorPalette 接口 + COLORS 常量 集中定义 24 种语义色,含 tagBg/tagText/shadow/chart1-6 保证视觉一致性,支持主题切换
渐变头部 linearGradient 135度粉→绿 header 粉红到绿色斜向渐变,白字标题副标题 最具辨识度的视觉元素,象征花+叶
数据建模 @Observed 类 + constructor + interface 5 个 @Observed 类(FlowerItem/CategoryItem/DeliveryTask/OrderRecord/AddressInfo)+ MonthData interface 按需选择响应式或静态,精准理解响应式机制
五Tab差异化 每页独立 @Builder 布局 首页Banner+分类+横滑+百科/花束筛选+瀑布流/配送概览+时间轴/订单柱状图+列表/我的用户卡+统计+地址+菜单 一页一型,还原真实鲜花电商体验
瀑布流双列 filter按id奇偶分列 + 左右异色 左列奇数id tag绿按钮粉/右列偶数id tag粉按钮绿 最具特色的布局,左右交错视觉韵律
配送时间轴 Circle + Line 组合 圆点动态着色,配送中圆点blinkAnim缩放1.3,竖线连接 动态脉冲感,"正在配送"直观呈现
月度柱状图 三档着色 + 柱子内白字 + floatAnim orders>80红/>60粉/其余绿,height=orders*1.8,scale1.02 以色定量级,节日高峰红色突出
四动画联动 setInterval 1500ms同步切换 breathAnim/blinkAnim/pulseAnim/floatAnim四状态,分别用于不同元素 整体协调的联动动效体系
统一弹窗封装 modalOverlay @Builder + if-else 一个构建器封装三个弹窗内容,共享遮罩卡片样式,点击遮罩全关 大幅减少重复代码,样式统一
三大弹窗 加入购物车/修改地址/删除订单 加入购物车cartCount+1/修改地址只读展示/删除订单二次确认 完整交互闭环
工具函数 formatPrice ¥ + price.toFixed(2),统一价格格式 价格视觉语言一致
花语映射 category内联三元判断 玫瑰→爱情/百合→百年好合/向日葵→向阳而生/郁金香→高雅聪颖/其余→温暖祝福 花语百科浪漫呈现
状态管理 @State + @Observed + private 14个@State分六类,private存静态数据不触发更新 细粒度状态控制,性能优化
组件化 @Component + @Builder 1个入口组件 + 5个页面@Builder + 3个弹窗内容@Builder + header/tabBar/modalOverlay 高内聚低耦合,代码复用率高
列表渲染 ForEach + 网格/横滑/双列/时间轴/列表 分类两排四列/热销横滑/瀑布流双列/配送时间轴/订单列表/地址列表 数据驱动渲染,布局灵活
数据可视化 柱状图 + 四栏统计 + 三栏统计 月度柱状图三档着色/配送概览四栏/我的统计四栏 多维度呈现,直观易读
文本省略 maxLines + textOverflow 花名maxLines1/地址maxLines1省略 长文本优雅截断,布局整齐
生命周期 aboutToAppear/aboutToDisappear setInterval启动/clearInterval清理,timerId判-1 健壮性强,无资源泄漏
布局系统 Column/Row + Stack + Scroll + Circle/Line 弹性布局、Stack叠加、Scroll横向/纵向滚动、Circle/Line绘制时间轴 自适应布局,层次分明

完整代码

// 814.ets - 花颜速递·在线鲜花配送平台
// 粉色绿色主题 | 8tab两排(4+4) | 购物头部

interface ColorPalette {
  bg: string;
  cardBg: string;
  primary: string;
  primaryLight: string;
  accent: string;
  accentLight: string;
  text: string;
  textSub: string;
  textWhite: string;
  border: string;
  tagBg: string;
  tagText: string;
  shadow: string;
  success: string;
  warning: string;
  danger: string;
  chart1: string;
  chart2: string;
  chart3: string;
  chart4: string;
  chart5: string;
  chart6: string;
}

const COLORS: ColorPalette = {
  bg: '#FFF0F5',
  cardBg: '#FFFFFF',
  primary: '#E91E63',
  primaryLight: '#F8BBD0',
  accent: '#4CAF50',
  accentLight: '#A5D6A7',
  text: '#2D2D2D',
  textSub: '#888888',
  textWhite: '#FFFFFF',
  border: '#F0F0F0',
  tagBg: '#FCE4EC',
  tagText: '#E91E63',
  shadow: '#FFC1CC',
  success: '#4CAF50',
  warning: '#FF9800',
  danger: '#F44336',
  chart1: '#E91E63',
  chart2: '#4CAF50',
  chart3: '#FF9800',
  chart4: '#9C27B0',
  chart5: '#2196F3',
  chart6: '#F44336',
};

@Observed
class FlowerItem {
  id: string;
  name: string;
  price: number;
  originalPrice: number;
  image: string;
  category: string;
  tag: string;
  sales: number;
  rating: number;
  stock: number;

  constructor(id: string, name: string, price: number, originalPrice: number,
    image: string, category: string, tag: string, sales: number, rating: number, stock: number) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.originalPrice = originalPrice;
    this.image = image;
    this.category = category;
    this.tag = tag;
    this.sales = sales;
    this.rating = rating;
    this.stock = stock;
  }
}

@Observed
class CategoryItem {
  id: string;
  name: string;
  icon: string;
  count: number;

  constructor(id: string, name: string, icon: string, count: number) {
    this.id = id;
    this.name = name;
    this.icon = icon;
    this.count = count;
  }
}

@Observed
class DeliveryTask {
  id: string;
  orderNo: string;
  flowerName: string;
  recipient: string;
  phone: string;
  address: string;
  status: string;
  time: string;
  courier: string;
  distance: number;

  constructor(id: string, orderNo: string, flowerName: string, recipient: string,
    phone: string, address: string, status: string, time: string, courier: string, distance: number) {
    this.id = id;
    this.orderNo = orderNo;
    this.flowerName = flowerName;
    this.recipient = recipient;
    this.phone = phone;
    this.address = address;
    this.status = status;
    this.time = time;
    this.courier = courier;
    this.distance = distance;
  }
}

@Observed
class OrderRecord {
  id: string;
  orderNo: string;
  flowerName: string;
  amount: number;
  status: string;
  date: string;
  count: number;

  constructor(id: string, orderNo: string, flowerName: string, amount: number,
    status: string, date: string, count: number) {
    this.id = id;
    this.orderNo = orderNo;
    this.flowerName = flowerName;
    this.amount = amount;
    this.status = status;
    this.date = date;
    this.count = count;
  }
}

@Observed
class AddressInfo {
  id: string;
  name: string;
  phone: string;
  address: string;
  isDefault: boolean;
  tag: string;

  constructor(id: string, name: string, phone: string, address: string, isDefault: boolean, tag: string) {
    this.id = id;
    this.name = name;
    this.phone = phone;
    this.address = address;
    this.isDefault = isDefault;
    this.tag = tag;
  }
}

interface MonthData {
  month: string;
  orders: number;
  revenue: number;
}

function formatPrice(price: number): string {
  return '¥' + price.toFixed(2);
}

@Entry
@Component
struct FlowerDeliveryApp {
  @State currentTab: number = 0;
  @State showAddToCartModal: boolean = false;
  @State showEditAddressModal: boolean = false;
  @State showDeleteOrderModal: boolean = false;
  @State selectedFlower: FlowerItem = new FlowerItem('f0', '', 0, 0, '', '', '', 0, 0, 0);
  @State selectedAddress: AddressInfo = new AddressInfo('a0', '', '', '', false, '');
  @State selectedOrderId: string = '';
  @State cartCount: number = 3;
  @State breathAnim: boolean = false;
  @State blinkAnim: boolean = false;
  @State pulseAnim: boolean = false;
  @State floatAnim: boolean = false;
  private timerId: number = -1;

  private flowers: FlowerItem[] = [
    new FlowerItem('f1', '红玫瑰99朵', 299, 399, '🌹', '玫瑰', '热销', 2380, 4.9, 56),
    new FlowerItem('f2', '粉色康乃馨花束', 158, 218, '🌸', '康乃馨', '母亲节', 1820, 4.8, 88),
    new FlowerItem('f3', '向日葵6朵', 128, 168, '🌻', '向日葵', '新品', 960, 4.7, 42),
    new FlowerItem('f4', '百合花束', 188, 248, '💐', '百合', '畅销', 1530, 4.8, 35),
    new FlowerItem('f5', '郁金香10朵', 218, 288, '🌷', '郁金香', '进口', 760, 4.9, 28),
    new FlowerItem('f6', '混搭鲜花花篮', 368, 468, '🪴', '花篮', '高端', 520, 4.9, 18),
    new FlowerItem('f7', '薰衣草干花束', 98, 138, '🌾', '干花', '永生', 1340, 4.6, 65),
    new FlowerItem('f8', '绣球花束', 258, 338, '🌼', '绣球', '限定', 430, 4.7, 22),
    new FlowerItem('f9', '满天星花束', 168, 218, '✿', '满天星', '热销', 1670, 4.8, 78),
    new FlowerItem('f10', '洋桔梗花束', 198, 268, '🌺', '桔梗', '新品', 680, 4.7, 32),
  ];

  private categories: CategoryItem[] = [
    new CategoryItem('c1', '玫瑰', '🌹', 86),
    new CategoryItem('c2', '百合', '💐', 52),
    new CategoryItem('c3', '康乃馨', '🌸', 68),
    new CategoryItem('c4', '向日葵', '🌻', 45),
    new CategoryItem('c5', '郁金香', '🌷', 38),
    new CategoryItem('c6', '绣球', '🌼', 22),
    new CategoryItem('c7', '干花', '🌾', 54),
    new CategoryItem('c8', '花篮', '🪴', 18),
  ];

  private deliveries: DeliveryTask[] = [
    new DeliveryTask('d1', 'DD20260820001', '红玫瑰99朵', '张小美', '138****8888', '朝阳区望京SOHO T1-1802', '配送中', '10:30-11:30', '李师傅', 2.3),
    new DeliveryTask('d2', 'DD20260820002', '粉色康乃馨花束', '王女士', '139****6666', '海淀区中关村大街1号', '待取件', '11:00-12:00', '赵师傅', 5.8),
    new DeliveryTask('d3', 'DD20260820003', '向日葵6朵', '刘先生', '137****7777', '西城区西单大悦城', '已送达', '09:00-10:00', '孙师傅', 1.2),
    new DeliveryTask('d4', 'DD20260820004', '百合花束', '陈老师', '136****5555', '东城区王府井大街', '配送中', '14:00-15:00', '周师傅', 3.6),
    new DeliveryTask('d5', 'DD20260820005', '郁金香10朵', '林女士', '135****3333', '丰台区方庄芳古园', '待取件', '15:30-16:30', '吴师傅', 7.2),
    new DeliveryTask('d6', 'DD20260820006', '混搭鲜花花篮', '赵总', '134****1111', '昌平区回龙观东大街', '已送达', '08:00-09:00', '郑师傅', 9.5),
  ];

  private orders: OrderRecord[] = [
    new OrderRecord('o1', 'ORD20260819001', '红玫瑰99朵', 299, '已完成', '08-19', 1),
    new OrderRecord('o2', 'ORD20260818002', '粉色康乃馨花束', 158, '已完成', '08-18', 1),
    new OrderRecord('o3', 'ORD20260817003', '向日葵6朵', 128, '已完成', '08-17', 2),
    new OrderRecord('o4', 'ORD20260816004', '百合花束', 188, '已完成', '08-16', 1),
    new OrderRecord('o5', 'ORD20260815005', '郁金香10朵', 218, '已取消', '08-15', 1),
    new OrderRecord('o6', 'ORD20260814006', '混搭鲜花花篮', 368, '已完成', '08-14', 1),
    new OrderRecord('o7', 'ORD20260813007', '薰衣草干花束', 98, '已完成', '08-13', 3),
    new OrderRecord('o8', 'ORD20260812008', '绣球花束', 258, '已完成', '08-12', 1),
    new OrderRecord('o9', 'ORD20260811009', '满天星花束', 168, '已完成', '08-11', 2),
    new OrderRecord('o10', 'ORD20260810010', '洋桔梗花束', 198, '已完成', '08-10', 1),
  ];

  private addresses: AddressInfo[] = [
    new AddressInfo('a1', '张小美', '138****8888', '朝阳区望京SOHO T1-1802', true, '公司'),
    new AddressInfo('a2', '张小美', '138****8888', '海淀区中关村大街1号院', false, '家'),
    new AddressInfo('a3', '王女士', '139****6666', '西城区西单大悦城5层', false, '收件人'),
  ];

  private monthData: MonthData[] = [
    { month: '3月', orders: 45, revenue: 12800 },
    { month: '4月', orders: 62, revenue: 18500 },
    { month: '5月', orders: 88, revenue: 26200 },
    { month: '6月', orders: 56, revenue: 16800 },
    { month: '7月', orders: 72, revenue: 21500 },
    { month: '8月', orders: 95, revenue: 28900 },
  ];

  private tabItems: string[] = ['首页', '花束', '配送', '订单', '我的'];

  aboutToAppear(): void {
    this.timerId = setInterval(() => {
      this.breathAnim = !this.breathAnim;
      this.blinkAnim = !this.blinkAnim;
      this.pulseAnim = !this.pulseAnim;
      this.floatAnim = !this.floatAnim;
    }, 1500);
  }

  aboutToDisappear(): void {
    if (this.timerId !== -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
  }

  @Builder
  header() {
    Row() {
      Column() {
        Text('花颜速递')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textWhite)
        Text('新鲜直达·爱不缺席')
          .fontSize(11)
          .fontColor(COLORS.textWhite)
          .opacity(0.85)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)

      Row().width(1)
    }
    .width('100%')
    .height(56)
    .padding({ left: 20, right: 20 })
    .linearGradient({
      angle: 135,
      colors: [[COLORS.primary, 0.0], [COLORS.accent, 1.0]]
    })
    .alignItems(VerticalAlign.Center)
  }

  @Builder
  tabBar() {
    Column() {
      Row() {
        ForEach(this.tabItems, (item: string, index: number) => {
          Column() {
            Text(item)
              .fontSize(11)
              .fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.currentTab === index ? COLORS.primary : COLORS.textSub)
          }
          .layoutWeight(1)
          .height(48)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.currentTab = index;
          })
        }, (item: string) => item)
      }
      .width('100%')
      .height(48)
      .backgroundColor(COLORS.cardBg)
    }
    .width('100%')
    .border({ width: 0.5, color: COLORS.border })
  }

  @Builder
  homeTab() {
    Scroll() {
      Column() {
        // Banner
        Row() {
          Column() {
            Text('七夕特惠')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textWhite)
            Text('满299减50')
              .fontSize(13)
              .fontColor(COLORS.textWhite)
              .opacity(0.9)
              .margin({ top: 4 })
            Row() {
              Button()
                .width(80)
                .height(28)
                .backgroundColor(COLORS.textWhite)
                .borderRadius(14)
                .opacity(0.95)
            }
            .margin({ top: 8 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .padding({ left: 20, top: 16, bottom: 16 })

          Text('🌹')
            .fontSize(48)
            .margin({ right: 20 })
        }
        .width('100%')
        .height(120)
        .borderRadius(16)
        .linearGradient({ angle: 135, colors: [[COLORS.primary, 0.0], [COLORS.primaryLight, 1.0]] })
        .margin({ bottom: 16 })
        .shadow({ radius: 12, color: COLORS.shadow, offsetX: 0, offsetY: 4 })

        // 分类网格
        Text('花语分类')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        Row() {
          ForEach(this.categories.slice(0, 4), (cat: CategoryItem) => {
            Column() {
              Text(cat.icon)
                .fontSize(28)
                .margin({ bottom: 6 })
              Text(cat.name)
                .fontSize(12)
                .fontColor(COLORS.text)
              Text(cat.count + '款')
                .fontSize(10)
                .fontColor(COLORS.textSub)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ right: 8 })
          }, (cat: CategoryItem) => cat.id)
        }
        .width('100%')
        .margin({ bottom: 8 })

        Row() {
          ForEach(this.categories.slice(4, 8), (cat: CategoryItem) => {
            Column() {
              Text(cat.icon)
                .fontSize(28)
                .margin({ bottom: 6 })
              Text(cat.name)
                .fontSize(12)
                .fontColor(COLORS.text)
              Text(cat.count + '款')
                .fontSize(10)
                .fontColor(COLORS.textSub)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ right: 8 })
          }, (cat: CategoryItem) => cat.id)
        }
        .width('100%')
        .margin({ bottom: 16 })

        // 热销花束横滑
        Row() {
          Text('🔥 热销花束')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
          Text('查看全部')
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .margin({ bottom: 12 })

        Scroll() {
          Row() {
            ForEach(this.flowers.slice(0, 5), (flower: FlowerItem) => {
              Column() {
                Text(flower.image)
                  .fontSize(40)
                  .margin({ bottom: 8 })
                Text(flower.name)
                  .fontSize(12)
                  .fontColor(COLORS.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row() {
                  Text(formatPrice(flower.price))
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.primary)
                  Text(formatPrice(flower.originalPrice))
                    .fontSize(10)
                    .fontColor(COLORS.textSub)
                    .decoration({ type: TextDecorationType.LineThrough })
                    .margin({ left: 4 })
                }
                .margin({ top: 4 })
                Row() {
                  Text('销量 ' + flower.sales)
                    .fontSize(9)
                    .fontColor(COLORS.textSub)
                  Text(flower.tag)
                    .fontSize(9)
                    .fontColor(COLORS.tagText)
                    .backgroundColor(COLORS.tagBg)
                    .borderRadius(4)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                    .margin({ left: 4 })
                    .scale(this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
                    .animation({ duration: 800, iterations: 1 })
                }
                .margin({ top: 4 })
              }
              .width(110)
              .padding(10)
              .backgroundColor(COLORS.cardBg)
              .borderRadius(12)
              .margin({ right: 10 })
            }, (flower: FlowerItem) => flower.id)
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')

        // 花语知识
        Row() {
          Text('📖 花语百科')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
        }
        .width('100%')
        .margin({ top: 16, bottom: 12 })

        Column() {
          ForEach(this.flowers.slice(5, 9), (flower: FlowerItem) => {
            Row() {
              Text(flower.image)
                .fontSize(36)
                .margin({ right: 12 })
              Column() {
                Text(flower.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.text)
                Text('花语:' + (flower.category === '玫瑰' ? '爱情' : flower.category === '百合' ? '百年好合' : flower.category === '向日葵' ? '向阳而生' : flower.category === '郁金香' ? '高雅聪颖' : '温暖祝福'))
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                  .margin({ top: 2 })
                Row() {
                  Text('★ ' + flower.rating.toFixed(1))
                    .fontSize(11)
                    .fontColor(COLORS.warning)
                  Text(' 库存' + flower.stock)
                    .fontSize(10)
                    .fontColor(COLORS.textSub)
                    .margin({ left: 8 })
                }
                .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              Column() {
                Text(formatPrice(flower.price))
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.primary)
                Button()
                  .width(56)
                  .height(26)
                  .backgroundColor(COLORS.primary)
                  .borderRadius(13)
                  .margin({ top: 6 })
                  .onClick(() => {
                    this.selectedFlower = flower;
                    this.showAddToCartModal = true;
                  })
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
            .padding(12)
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ bottom: 8 })
          }, (flower: FlowerItem) => flower.id)
        }
        .width('100%')
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  flowerTab() {
    Scroll() {
      Column() {
        // 筛选标签
        Scroll() {
          Row() {
            ForEach(['全部', '玫瑰', '百合', '康乃馨', '向日葵', '郁金香', '干花', '花篮'], (tag: string) => {
              Text(tag)
                .fontSize(12)
                .fontColor(COLORS.text)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .backgroundColor(COLORS.cardBg)
                .borderRadius(16)
                .margin({ right: 8 })
            }, (tag: string) => tag)
          }
          .padding({ left: 16, right: 16 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .margin({ bottom: 12 })

        // 排序条
        Row() {
          Text('综合排序')
            .fontSize(12)
            .fontColor(COLORS.primary)
            .fontWeight(FontWeight.Bold)
          Text('价格')
            .fontSize(12)
            .fontColor(COLORS.textSub)
            .margin({ left: 16 })
          Text('销量')
            .fontSize(12)
            .fontColor(COLORS.textSub)
            .margin({ left: 16 })
          Text('新品')
            .fontSize(12)
            .fontColor(COLORS.textSub)
            .margin({ left: 16 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })

        // 瀑布流双列
        Row() {
          Column() {
            ForEach(this.flowers.filter((f: FlowerItem) => f.id.charCodeAt(1) % 2 === 1), (flower: FlowerItem) => {
              Column() {
                Text(flower.image)
                  .fontSize(44)
                  .margin({ bottom: 8 })
                Text(flower.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row() {
                  Text(flower.tag)
                    .fontSize(9)
                    .fontColor(COLORS.textWhite)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(4)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  Text('★' + flower.rating.toFixed(1))
                    .fontSize(10)
                    .fontColor(COLORS.warning)
                    .margin({ left: 6 })
                }
                .margin({ top: 6 })
                Row() {
                  Text(formatPrice(flower.price))
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.primary)
                    .layoutWeight(1)
                  Button()
                    .width(28)
                    .height(28)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(14)
                    .onClick(() => {
                      this.selectedFlower = flower;
                      this.showAddToCartModal = true;
                    })
                }
                .margin({ top: 8 })
                .width('100%')
              }
              .width('100%')
              .padding(12)
              .backgroundColor(COLORS.cardBg)
              .borderRadius(14)
              .margin({ bottom: 10 })
            }, (flower: FlowerItem) => flower.id)
          }
          .layoutWeight(1)
          .margin({ right: 5 })

          Column() {
            ForEach(this.flowers.filter((f: FlowerItem) => f.id.charCodeAt(1) % 2 === 0), (flower: FlowerItem) => {
              Column() {
                Text(flower.image)
                  .fontSize(44)
                  .margin({ bottom: 8 })
                Text(flower.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row() {
                  Text(flower.tag)
                    .fontSize(9)
                    .fontColor(COLORS.textWhite)
                    .backgroundColor(COLORS.primary)
                    .borderRadius(4)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  Text('★' + flower.rating.toFixed(1))
                    .fontSize(10)
                    .fontColor(COLORS.warning)
                    .margin({ left: 6 })
                }
                .margin({ top: 6 })
                Row() {
                  Text(formatPrice(flower.price))
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.primary)
                    .layoutWeight(1)
                  Button()
                    .width(28)
                    .height(28)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(14)
                    .onClick(() => {
                      this.selectedFlower = flower;
                      this.showAddToCartModal = true;
                    })
                }
                .margin({ top: 8 })
                .width('100%')
              }
              .width('100%')
              .padding(12)
              .backgroundColor(COLORS.cardBg)
              .borderRadius(14)
              .margin({ bottom: 10 })
            }, (flower: FlowerItem) => flower.id)
          }
          .layoutWeight(1)
          .margin({ left: 5 })
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 16 })
      }
      .padding({ top: 12 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  deliveryTab() {
    Scroll() {
      Column() {
        // 配送概览
        Row() {
          Column() {
            Text('今日配送')
              .fontSize(11)
              .fontColor(COLORS.textSub)
            Text('6')
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primary)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('已送达')
              .fontSize(11)
              .fontColor(COLORS.textSub)
            Text('2')
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.accent)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('配送中')
              .fontSize(11)
              .fontColor(COLORS.textSub)
            Text('2')
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.warning)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('待取件')
              .fontSize(11)
              .fontColor(COLORS.textSub)
            Text('2')
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.danger)
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ top: 16, bottom: 16 })
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 配送时间轴
        Text('配送路线')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.text)
          .margin({ bottom: 12 })

        ForEach(this.deliveries, (task: DeliveryTask) => {
          Row() {
            // 时间轴圆点
            Column() {
              Circle({ width: 12, height: 12 })
                .fill(task.status === '已送达' ? COLORS.accent : task.status === '配送中' ? COLORS.warning : COLORS.danger)
                .scale(this.blinkAnim && task.status === '配送中' ? { x: 1.3, y: 1.3 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 500, iterations: 1 })
              Line()
                .width(2)
                .layoutWeight(1)
                .backgroundColor(COLORS.border)
            }
            .width(24)
            .height('100%')
            .alignItems(HorizontalAlign.Center)

            // 配送卡
            Column() {
              Row() {
                Text(task.orderNo)
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                  .layoutWeight(1)
                Text(task.status)
                  .fontSize(10)
                  .fontColor(task.status === '已送达' ? COLORS.accent : task.status === '配送中' ? COLORS.warning : COLORS.danger)
                  .fontWeight(FontWeight.Bold)
              }
              .width('100%')

              Text(task.flowerName)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.text)
                .margin({ top: 6 })

              Row() {
                Text('📍')
                  .fontSize(12)
                Text(task.address)
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                  .layoutWeight(1)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .margin({ left: 4 })
              }
              .margin({ top: 6 })

              Row() {
                Text('收件人: ' + task.recipient)
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                Text(task.time)
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                  .margin({ left: 12 })
              }
              .margin({ top: 4 })

              Row() {
                Text('🚴 ' + task.courier)
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                Text('距离 ' + task.distance.toFixed(1) + 'km')
                  .fontSize(10)
                  .fontColor(COLORS.primary)
                  .margin({ left: 12 })
                  .fontWeight(FontWeight.Bold)
              }
              .margin({ top: 4 })
            }
            .layoutWeight(1)
            .padding(12)
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ left: 8, bottom: 10 })
          }
          .width('100%')
        }, (task: DeliveryTask) => task.id)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  orderTab() {
    Scroll() {
      Column() {
        // 月度统计柱状图
        Column() {
          Text('月度订单统计')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .margin({ bottom: 16 })

          Row() {
            ForEach(this.monthData, (data: MonthData) => {
              Column() {
                Column() {
                  Text(data.orders.toString())
                    .fontSize(9)
                    .fontColor(COLORS.textWhite)
                }
                .width(24)
                .height(data.orders * 1.8)
                .backgroundColor(data.orders > 80 ? COLORS.danger : data.orders > 60 ? COLORS.primary : COLORS.accent)
                .borderRadius(4)
                .justifyContent(FlexAlign.End)
                .padding({ top: 2 })
                .scale(this.floatAnim ? { x: 1.02, y: 1.02 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 600, iterations: 1 })

                Text(data.month)
                  .fontSize(9)
                  .fontColor(COLORS.textSub)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (data: MonthData) => data.month)
          }
          .width('100%')
          .height(180)
          .alignItems(VerticalAlign.Bottom)
        }
        .width('100%')
        .padding(16)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 订单列表
        Row() {
          Text('最近订单')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
          Text('共' + this.orders.length + '单')
            .fontSize(12)
            .fontColor(COLORS.textSub)
        }
        .width('100%')
        .margin({ bottom: 12 })

        ForEach(this.orders, (order: OrderRecord) => {
          Row() {
            Text('💐')
              .fontSize(28)
              .margin({ right: 12 })
            Column() {
              Row() {
                Text(order.orderNo)
                  .fontSize(10)
                  .fontColor(COLORS.textSub)
                  .layoutWeight(1)
                Text(order.status)
                  .fontSize(10)
                  .fontColor(order.status === '已完成' ? COLORS.accent : COLORS.danger)
                  .fontWeight(FontWeight.Bold)
                  .scale(order.status === '已完成' && this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
                  .animation({ duration: 600, iterations: 1 })
              }
              .width('100%')

              Text(order.flowerName + ' x' + order.count)
                .fontSize(13)
                .fontColor(COLORS.text)
                .margin({ top: 4 })

              Row() {
                Text(order.date)
                  .fontSize(11)
                  .fontColor(COLORS.textSub)
                  .layoutWeight(1)
                Text(formatPrice(order.amount))
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.primary)
              }
              .width('100%')
              .margin({ top: 4 })

              if (order.status !== '已取消') {
                Row() {
                  Button()
                    .width(60)
                    .height(24)
                    .backgroundColor(COLORS.cardBg)
                    .border({ width: 0.5, color: COLORS.border })
                    .borderRadius(12)
                  if (order.status === '已完成') {
                    Button()
                      .width(60)
                      .height(24)
                      .backgroundColor(COLORS.danger)
                      .borderRadius(12)
                      .margin({ left: 8 })
                      .onClick(() => {
                        this.selectedOrderId = order.orderNo;
                        this.showDeleteOrderModal = true;
                      })
                  }
                }
                .margin({ top: 8 })
              }
            }
            .layoutWeight(1)
            .padding(12)
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
          }
          .width('100%')
          .margin({ bottom: 8 })
        }, (order: OrderRecord) => order.id)
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  profileTab() {
    Scroll() {
      Column() {
        // 用户卡
        Row() {
          Circle({ width: 56, height: 56 })
            .fill(COLORS.primaryLight)

          Column() {
            Text('张小美')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.text)
            Row() {
              Text('VIP会员')
                .fontSize(10)
                .fontColor(COLORS.textWhite)
                .backgroundColor(COLORS.primary)
                .borderRadius(8)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .scale(this.pulseAnim ? { x: 1.05, y: 1.05 } : { x: 1.0, y: 1.0 })
                .animation({ duration: 700, iterations: 1 })
              Text('积分 2680')
                .fontSize(10)
                .fontColor(COLORS.textSub)
                .margin({ left: 8 })
            }
            .margin({ top: 4 })
          }
          .layoutWeight(1)
          .margin({ left: 12 })
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(16)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 统计
        Row() {
          Column() {
            Text(this.orders.length.toString())
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primary)
            Text('总订单')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('2680')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.accent)
            Text('积分')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('8')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.warning)
            Text('优惠券')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('12')
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.danger)
            Text('收藏')
              .fontSize(10)
              .fontColor(COLORS.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ top: 16, bottom: 16 })
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)
        .margin({ bottom: 16 })

        // 收货地址
        Row() {
          Text('📍 收货地址')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
            .layoutWeight(1)
          Text('管理')
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .margin({ bottom: 12 })

        ForEach(this.addresses, (addr: AddressInfo) => {
          Row() {
            Column() {
              Row() {
                Text(addr.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.text)
                Text(addr.phone)
                  .fontSize(12)
                  .fontColor(COLORS.textSub)
                  .margin({ left: 8 })
                if (addr.isDefault) {
                  Text('默认')
                    .fontSize(9)
                    .fontColor(COLORS.textWhite)
                    .backgroundColor(COLORS.accent)
                    .borderRadius(4)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                    .margin({ left: 6 })
                }
                Text(addr.tag)
                  .fontSize(9)
                  .fontColor(COLORS.tagText)
                  .backgroundColor(COLORS.tagBg)
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  .margin({ left: 6 })
              }
              Text(addr.address)
                .fontSize(12)
                .fontColor(COLORS.textSub)
                .margin({ top: 4 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Button()
              .width(28)
              .height(28)
              .backgroundColor(COLORS.cardBg)
              .border({ width: 0.5, color: COLORS.border })
              .borderRadius(14)
              .onClick(() => {
                this.selectedAddress = addr;
                this.showEditAddressModal = true;
              })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
          .margin({ bottom: 8 })
        }, (addr: AddressInfo) => addr.id)

        // 功能菜单
        Column() {
          ForEach(['我的收藏', '优惠券', '积分商城', '帮助中心', '关于我们'], (item: string) => {
            Row() {
              Text(item)
                .fontSize(14)
                .fontColor(COLORS.text)
                .layoutWeight(1)
              Text('>')
                .fontSize(14)
                .fontColor(COLORS.textSub)
            }
            .width('100%')
            .padding({ top: 14, bottom: 14, left: 16, right: 16 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .margin({ bottom: 8 })
          }, (item: string) => item)
        }
        .width('100%')
      }
      .padding({ left: 16, right: 16, top: 12, bottom: 16 })
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .layoutWeight(1)
  }

  @Builder
  modalOverlay(onClose: () => void) {
    Column() {
      Column() {
        if (this.showAddToCartModal) {
          this.addToCartModalContent()
        } else if (this.showEditAddressModal) {
          this.editAddressModalContent()
        } else if (this.showDeleteOrderModal) {
          this.deleteOrderModalContent()
        }
      }
      .width('90%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor(COLORS.cardBg)
      .borderRadius(20)
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.4)')
    .onClick(() => {
      onClose();
    })
  }

  @Builder
  addToCartModalContent() {
    Column() {
      Text('加入购物车')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.text)
        .margin({ bottom: 16 })

      Row() {
        Text(this.selectedFlower.image)
          .fontSize(48)
          .margin({ right: 12 })
        Column() {
          Text(this.selectedFlower.name)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.text)
          Text(formatPrice(this.selectedFlower.price))
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .margin({ bottom: 16 })

      Row() {
        Text('数量')
          .fontSize(13)
          .fontColor(COLORS.text)
          .layoutWeight(1)
        Text('- 1 +')
          .fontSize(14)
          .fontColor(COLORS.primary)
          .fontWeight(FontWeight.Bold)
      }
      .margin({ bottom: 16 })

      Row() {
        Text('配送日期')
          .fontSize(13)
          .fontColor(COLORS.text)
          .layoutWeight(1)
        Text('2026-08-21')
          .fontSize(13)
          .fontColor(COLORS.textSub)
      }
      .margin({ bottom: 16 })

      Row() {
        Text('配送地址')
          .fontSize(13)
          .fontColor(COLORS.text)
          .layoutWeight(1)
        Text('朝阳区望京SOHO')
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .margin({ bottom: 20 })

      Row() {
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.cardBg)
          .border({ width: 0.5, color: COLORS.border })
          .borderRadius(20)
          .onClick(() => {
            this.showAddToCartModal = false;
          })
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.primary)
          .borderRadius(20)
          .margin({ left: 10 })
          .onClick(() => {
            this.cartCount += 1;
            this.showAddToCartModal = false;
          })
      }
      .width('100%')
    }
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  editAddressModalContent() {
    Column() {
      Text('修改地址')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.text)
        .margin({ bottom: 16 })

      Row() {
        Text('姓名')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedAddress.name)
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .layoutWeight(1)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('电话')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedAddress.phone)
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .layoutWeight(1)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('地址')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedAddress.address)
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .layoutWeight(1)
      }
      .margin({ bottom: 12 })

      Row() {
        Text('标签')
          .fontSize(13)
          .fontColor(COLORS.text)
          .width(60)
        Text(this.selectedAddress.tag)
          .fontSize(13)
          .fontColor(COLORS.textSub)
          .layoutWeight(1)
      }
      .margin({ bottom: 20 })

      Row() {
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.cardBg)
          .border({ width: 0.5, color: COLORS.border })
          .borderRadius(20)
          .onClick(() => {
            this.showEditAddressModal = false;
          })
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.accent)
          .borderRadius(20)
          .margin({ left: 10 })
          .onClick(() => {
            this.showEditAddressModal = false;
          })
      }
      .width('100%')
    }
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  deleteOrderModalContent() {
    Column() {
      Text('删除订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.text)
        .margin({ bottom: 16 })

      Text('确定删除订单 ' + this.selectedOrderId + ' 吗?')
        .fontSize(14)
        .fontColor(COLORS.textSub)
        .margin({ bottom: 8 })

      Text('删除后无法恢复,请谨慎操作')
        .fontSize(12)
        .fontColor(COLORS.danger)
        .margin({ bottom: 20 })

      Row() {
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.cardBg)
          .border({ width: 0.5, color: COLORS.border })
          .borderRadius(20)
          .onClick(() => {
            this.showDeleteOrderModal = false;
          })
        Button()
          .width('45%')
          .height(40)
          .backgroundColor(COLORS.danger)
          .borderRadius(20)
          .margin({ left: 10 })
          .onClick(() => {
            this.showDeleteOrderModal = false;
          })
      }
      .width('100%')
    }
    .alignItems(HorizontalAlign.Start)
  }

  build() {
    Stack() {
      Column() {
        this.header()

        if (this.currentTab === 0) {
          this.homeTab()
        } else if (this.currentTab === 1) {
          this.flowerTab()
        } else if (this.currentTab === 2) {
          this.deliveryTab()
        } else if (this.currentTab === 3) {
          this.orderTab()
        } else {
          this.profileTab()
        }

        this.tabBar()
      }
      .width('100%')
      .height('100%')
      .backgroundColor(COLORS.bg)

      if (this.showAddToCartModal || this.showEditAddressModal || this.showDeleteOrderModal) {
        this.modalOverlay(() => {
          this.showAddToCartModal = false;
          this.showEditAddressModal = false;
          this.showDeleteOrderModal = false;
        })
      }
    }
    .width('100%')
    .height('100%')
  }
}

十三、开发环境搭建

13.1 安装 DevEco Studio

DevEco Studio 是 HarmonyOS 应用开发的官方 IDE。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

DevEco Studio 安装欢迎界面

13.2 选择安装路径

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

选择安装位置

13.3 配置安装选项

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

安装选项配置

13.4 选择项目模板

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

选择 Empty Ability 模板

13.5 配置项目信息

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

配置项目信息


十四、总结

通过对这款花颜速递在线鲜花配送应用的深度拆解,我们可以看到,一个优秀的移动端应用远不止是功能的堆砌,它更是架构设计、数据建模、视觉系统、交互逻辑、动效设计、防御性编程等多维度能力的综合体现。

在架构层面,该应用采用了"配置驱动 + 组件化 + 状态管理"的三层设计模式。配置层将所有可变的数据与视觉属性抽离为常量数组和元数据接口,使得业务逻辑与表现层彻底解耦;组件层将五个功能页面拆分为独立的 @Builder 构建器,每个页面采用完全不同的布局风格,实现"一页一型"的差异化设计;状态层利用 ArkUI 的 @State、@Observed 机制,实现了数据变化到 UI 刷新的自动响应,特别是四个动画状态的联动管理(setInterval 1500ms 同步切换 breathAnim/blinkAnim/pulseAnim/floatAnim)和弹窗数据状态的空对象初始化(new FlowerItem('f0',...) 避免空引用),体现了对状态管理的精准理解。

在数据层面,五个 @Observed 数据类(FlowerItem/CategoryItem/DeliveryTask/OrderRecord/AddressInfo)以 constructor 构造函数方式实例化,完整描述了花颜速递的全部业务信息。特别的是,MonthData 采用普通 interface 接口而非 @Observed 类,因为月度数据是静态统计数据无需响应式更新,这种"按需选择 @Observed 或 interface"的设计体现了对响应式机制的精准理解,避免了不必要的响应式开销。formatPrice 工具函数统一价格格式为 ¥XX.XX,是本应用唯一的工具函数,体现了"最小工具集"的设计原则。

在视觉层面,本应用独创了"粉绿双主题渐变头部"设计——顶部 header 使用 linearGradient 135 度从 primary #E91E63 粉红到 accent #4CAF50 绿色的斜向渐变,是本应用最具辨识度的视觉元素,粉绿渐变象征"从花朵到绿叶"的完整鲜花生态。首页 Banner 也使用粉→浅粉渐变 + shadow 阴影,营造出七夕特惠的浪漫促销氛围。应用还独创了"瀑布流双列左右异色"设计——花束页用 filter 按花 id 字符码奇偶分列,左列 tag 背景绿色按钮粉红,右列 tag 背景粉红按钮绿色,左右列颜色交错形成视觉韵律,是本应用最具工程巧思的布局设计。应用的动态颜色逻辑也十分丰富——配送状态分三档着色(已送达绿/配送中橙/待取件红)、月度柱状图分三档着色(>80 红/>60 粉/其余绿)、订单状态分两档着色(已完成绿/已取消红)、瀑布流左右异色,形成了"以色定状态、以色定量级、以色定列"的多层语义着色模式。统一的色彩系统是整个应用的视觉基石,粉红主色搭配绿色辅助色,配合粉白底和渐变头部,营造出既浪漫又清新的鲜花电商氛围。

在交互层面,应用独创了"统一 modalOverlay 封装"设计——一个 @Builder modalOverlay(onClose) 构建器,内部通过 if-else 判断当前显示哪个弹窗内容,外层统一全屏遮罩 + 居中卡片,点击遮罩同时关闭所有三个弹窗状态。这种统一封装大幅减少了重复代码,三个弹窗(加入购物车/修改地址/删除订单)共享同一套遮罩与卡片样式。加入购物车弹窗确认后 cartCount += 1,修改地址弹窗展示地址信息只读,删除订单弹窗二次确认后关闭,形成了完整的交互闭环。应用还独创了"四动画状态联动"设计——blinkAnim 用于配送中圆点脉冲缩放、pulseAnim 用于热销 tag/已完成订单状态/VIP 会员标签缩放、floatAnim 用于月度柱状图浮动,四个动画同步切换分别用于不同元素,形成了整体协调的联动动效体系,让静态的页面有了"鲜活"的动态感。配送时间轴用 Circle+Line 组合,配送中圆点的 blinkAnim 缩放动画营造出"正在配送"的动态脉冲感;月度柱状图按订单数分三档着色,柱子内白字显示数值,floatAnim 缩放动画让数据有了轻微的呼吸感;花语百科用 category 内联三元判断映射花语(玫瑰→爱情/百合→百年好合/向日葵→向阳而生/郁金香→高雅聪颖/其余→温暖祝福),让鲜花选购更具浪漫文化内涵。

花颜速递应用整体运行效果 - 粉绿渐变头部与七夕特惠Banner及花语分类网格


标签: HarmonyOS · API 24 · ArkTS · 华为 · 鲜花电商 · 移动端开发

Logo

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

更多推荐