基于HarmonyOS API 24的花颜速递在线鲜花配送平台深度解析——粉绿双主题渐变头部、瀑布流双列与配送时间轴的完整实现
基于HarmonyOS API 24的花颜速递在线鲜花配送平台深度解析——粉绿双主题渐变头部、瀑布流双列与配送时间轴的完整实现
鲜花速递,新鲜直达,爱不缺席。本文完整拆解一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式框架构建的「花颜速递」在线鲜花配送应用,覆盖粉绿双主题配色系统、渐变头部、五Tab全差异化布局、七夕特惠Banner、花语分类网格、热销花束横滑、花语百科、筛选标签与排序条、瀑布流双列、配送概览四栏统计、配送路线时间轴、月度订单统计柱状图、收货地址管理、四动画状态联动、Stack弹窗叠加、加入购物车/修改地址/删除订单三大弹窗等核心技术点,附完整源码与运行效果截图。
一、引言:鲜花电商的数字化浪潮
在消费升级与情感经济双重驱动的今天,在线鲜花配送已经从传统的"节日买花、花店自取",演变为一种日常化、即时化、个性化的消费新形态。鲜花,以其"非标准化、强情感属性、高时效要求、易损耗"的特点,正在成为本地生活服务与电商融合的关键赛道。从情人节的红玫瑰到母亲节的康乃馨,从日常家居的百合到商务馈赠的混搭花篮,每一束鲜花都承载着送花人的情感与心意。然而,鲜花品类繁多、价格不透明、配送时效要求高、花材品质难保障,正成为用户获取可靠鲜花配送服务的现实痛点。
正是在这样的背景下,花颜速递类在线鲜花配送应用应运而生,并迅速成为鲜花电商的数字基础设施。一个优秀的鲜花配送应用,不仅需要覆盖从"选花"到"配送"再到"售后"的完整业务闭环,还要在视觉设计、交互体验、数据呈现上做到浪漫而高效,让用户在指尖滑动间便能完成选花、加购、填地址、跟配送、查订单的全流程。
本文将深入剖析一个基于鸿蒙 HarmonyOS API 24 与 ArkUI 声明式开发框架构建的花颜速递在线鲜花配送应用。该应用以"首页"为核心,横向扩展出花束、配送、订单、我的五大功能模块,每个模块采用完全不同的布局风格——首页Banner+分类网格+热销横滑+花语百科、花束筛选标签+排序条+瀑布流双列、配送概览四栏+配送路线时间轴、订单月度柱状图+订单列表、我的用户卡+统计+地址管理+功能菜单,形成了一个完整闭环的鲜花配送生态。它不仅是一个功能完备的演示项目,更是一份典型的 ArkUI 工程实践范本,涵盖了状态管理、组件化设计、Stack弹窗叠加、四动画状态联动、渐变头部、瀑布流双列、配送时间轴、月度柱状图等关键技术点。

二、应用整体架构概览
整个应用采用了"单入口 + 五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 种图表色(粉红/绿/橙/紫/蓝/红),预留给数据可视化使用。

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。下载安装包后双击运行,进入安装欢迎界面,点击「下一步」继续。

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

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

13.4 选择项目模板
新建一个空白模板,在模板市场中选择 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 内联三元判断映射花语(玫瑰→爱情/百合→百年好合/向日葵→向阳而生/郁金香→高雅聪颖/其余→温暖祝福),让鲜花选购更具浪漫文化内涵。

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


所有评论(0)