HarmonyOS 6效果实现:采用for循环加条件比较的经典算法,时间复杂度为O(n)
引言

咖啡与茗茶作为现代人日常生活中不可或缺的饮品品类,其消费场景已从简单的商品购买演变为涵盖产地溯源、烘焙度选择、周期订阅、门店体验和器具冲泡等复合维度的深度消费。本应用立足于HarmonyOS 6.1.1声明式开发范式,采用ArkTS语言和ArkUI声明式组件框架,构建了一个集咖啡集市、庄园咖啡豆、茶叶专区、器具冲泡、门店体验、消息中心和用户中心于一体的综合性咖啡茗茶生活服务平台。
在技术架构层面,本应用以@Entry和@Component装饰器为入口,通过@State状态管理机制实现UI与数据的响应式绑定。应用采用单页面多Tab架构,通过currentTab状态变量驱动条件渲染切换七个功能模块。在业务深度上,本应用引入了咖啡豆周期订阅流程(支持豆种选择、研磨粗细和订阅周期配置)、门店手冲体验预约(支持门店选择、到店人数和体验内容展示)和闲置咖啡茶品发布等复合业务交互。五个模态弹窗通过Stack布局叠加在主内容之上,实现了复杂业务交互而无需页面跳转。
从数据可视化设计的角度来看,该应用将庄园咖啡豆的海拔高度、茶叶发酵度、器具容量和门店预约热度等关键参数通过纯ArkUI柱状图进行直观展示。每个功能Tab都内置了不同维度的数据可视化组件,让用户在选购咖啡豆和茶叶时能够快速把握产品间的参数差异。此外,应用还实现了杯测评分进度条、海拔进度条、好评率进度条、门店预约率进度条等多种自定义进度条组件,通过Stack嵌套Column的双层叠加模式实现轨道与填充效果。庄园咖啡豆列表还创新性地引入了带序号编号的排名展示设计,前三名使用橙色编号高亮,增强了竞技感和品质导向。
逐段代码分析

数据模型接口定义
应用首先定义了六个核心数据接口,分别对应咖啡商品、庄园咖啡豆、茶叶、冲泡器具、门店和系统消息等不同业务领域的数据结构。
interface CoffeeItem {
name: string
price: number
sold: number
origin: string
roast: string
score: number
}
interface BeanItem {
name: string
farm: string
altitude: number
price: number
process: string
}
interface TeaItem {
name: string
type: string
ferment: number
price: number
rating: number
}
interface GearItem {
name: string
material: string
capacity: number
price: number
stock: number
}
interface ShopItem {
name: string
dist: number
score: number
seats: number
booked: number
}
interface MessageItem {
title: string
content: string
time: string
unread: number
}
CoffeeItem接口定义了咖啡商品的核心属性,其中origin字段标识产区(如埃塞俄比亚、哥伦比亚、云南保山等),roast字段标识烘焙度,score字段表示杯测评分。BeanItem中的farm字段标识庄园名称,altitude字段表示种植海拔,process字段标识处理法(水洗、日晒、蜜处理等),这三个字段是精品咖啡溯源的核心参数。TeaItem的type字段标识茶类(绿茶、红茶、乌龙、普洱等),ferment字段表示发酵度百分比,是区分茶类的关键指标。GearItem的material和capacity字段分别标识器具材质和容量。ShopItem的dist字段表示门店距离,seats和booked字段分别表示总席位和已预约数,通过两者的比值可以计算预约率。
组件状态声明与配置数据

组件CoffeeTeaPage通过丰富的状态变量和配置数组,构建了应用运行时的数据基础和用户交互状态。
@Entry
@Component
struct CoffeeTeaPage {
@State currentTab: number = 0
@State showPublish: boolean = false
@State showSubscribe: boolean = false
@State showVisit: boolean = false
@State showDelete: boolean = false
@State showDetail: boolean = false
@State selectedItem: CoffeeItem | null = null
@State selectedOrigin: number = 0
@State selectedRoast: number = 0
@State selectedBean: number = 0
@State selectedGrind: number = 0
@State selectedShop: number = 0
@State selectedGuests: number = 0
@State weeks: number = 4
@State inputName: string = ''
private tabs: string[] = ['咖啡集市', '咖啡豆', '茶叶专区', '器具冲泡', '门店体验', '消息', '我的']
private origins: string[] = ['埃塞俄比亚', '哥伦比亚', '云南保山', '危地马拉', '巴拿马']
private roasts: string[] = ['浅烘焙', '中烘焙', '中深烘焙', '深烘焙']
private beans: string[] = ['瑰夏', '耶加雪菲', '蓝山', '曼特宁', '云南小粒']
private grinds: string[] = ['手冲粗度', '法压中度', '摩卡细度', '意式极细']
private shops: string[] = ['国贸旗舰店', '三里屯店', '望京小街店', '陆家嘴店']
private guestOptions: string[] = ['1 人', '2 人', '3-4 人', '5 人以上']
currentTab驱动七个Tab页签的条件渲染。五个布尔状态变量分别控制发布闲置、订阅咖啡豆、门店预约、删除确认和商品详情五个模态弹窗。selectedItem采用CoffeeItem | null联合类型。应用还声明了selectedOrigin、selectedRoast用于咖啡集市的产区筛选,selectedBean、selectedGrind用于订阅弹窗的豆种和研磨度选择,selectedShop、selectedGuests用于门店预约的门店和人数选择。weeks变量控制订阅周期,范围限制在1到24周之间。
咖啡集市数据集
咖啡数据集包含了24款不同产区和烘焙度的咖啡产品,从挂耳到冻干、从云南小粒到巴拿马瑰夏全品类覆盖。
private coffees: CoffeeItem[] = [
{ name: '瑰夏手冲挂耳 10 袋礼盒', price: 128, sold: 3214, origin: '巴拿马', roast: '浅烘焙', score: 97 },
{ name: '耶加雪菲日晒挂耳 20 袋', price: 88, sold: 5623, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
{ name: '蓝山一号咖啡豆 227g', price: 168, sold: 2134, origin: '哥伦比亚', roast: '中烘焙', score: 94 },
{ name: '云南保山小粒日晒豆', price: 58, sold: 4876, origin: '云南保山', roast: '中烘焙', score: 91 },
{ name: '危地马拉花神咖啡豆', price: 98, sold: 1856, origin: '危地马拉', roast: '中深烘焙', score: 93 },
{ name: '曼特宁黄金曼咖啡豆', price: 76, sold: 2341, origin: '巴拿马', roast: '深烘焙', score: 92 },
{ name: '冷萃冻干咖啡粉 3 秒即溶', price: 69, sold: 7842, origin: '云南保山', roast: '中烘焙', score: 96 },
{ name: '拿铁冻干咖啡礼盒 24 杯', price: 139, sold: 3267, origin: '哥伦比亚', roast: '中深烘焙', score: 94 },
{ name: '西达摩水洗挂耳 10 袋', price: 79, sold: 4521, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
{ name: '翡翠庄园红标瑰夏 100g', price: 299, sold: 876, origin: '巴拿马', roast: '浅烘焙', score: 99 },
{ name: '云南日晒卡迪姆 200g', price: 45, sold: 6234, origin: '云南保山', roast: '中烘焙', score: 89 },
{ name: '安提瓜火山咖啡豆', price: 108, sold: 1457, origin: '危地马拉', roast: '中深烘焙', score: 93 },
{ name: '椰香拿铁冻干条 30 条', price: 99, sold: 4312, origin: '海南', roast: '中深烘焙', score: 92 },
{ name: '瑰夏村竞标批次 50g', price: 388, sold: 432, origin: '埃塞俄比亚', roast: '浅烘焙', score: 98 },
{ name: '云南铁皮卡蜜处理 200g', price: 72, sold: 1987, origin: '云南保山', roast: '浅烘焙', score: 90 },
{ name: '哥伦比亚蕙兰咖啡豆', price: 68, sold: 2876, origin: '哥伦比亚', roast: '中烘焙', score: 91 },
{ name: '意式拼配金杯豆 1kg', price: 128, sold: 5163, origin: '巴拿马', roast: '深烘焙', score: 95 },
{ name: '低因瑞士水处理豆 227g', price: 118, sold: 965, origin: '哥伦比亚', roast: '中烘焙', score: 92 },
{ name: '手冲单品入门三豆组', price: 108, sold: 2743, origin: '云南保山', roast: '中烘焙', score: 93 },
{ name: '冷萃浓缩液 1L 大瓶装', price: 89, sold: 3562, origin: '云南保山', roast: '中深烘焙', score: 94 },
{ name: '挂耳咖啡混合口味 30 袋', price: 119, sold: 6987, origin: '埃塞俄比亚', roast: '中烘焙', score: 96 },
{ name: '麝香猫风味咖啡豆 100g', price: 158, sold: 654, origin: '巴拿马', roast: '中深烘焙', score: 88 },
{ name: '云南厌氧发酵黑蜜 200g', price: 96, sold: 1234, origin: '云南保山', roast: '浅烘焙', score: 95 },
{ name: '经典意式浓缩豆 500g', price: 85, sold: 4231, origin: '危地马拉', roast: '深烘焙', score: 92 }
]
每件咖啡商品的score杯测评分字段在UI渲染时通过Stack嵌套Column实现进度条可视化,评分越高进度条填充越满。origin产区标签使用橙色浅色背景,roast烘焙度标签使用绿色背景,两种颜色的交叉使用在视觉上区分产地信息和工艺信息。score字段配合scale缩放和animation动画实现脉冲式强调效果,杯测评分达到99分的翡翠庄园红标瑰夏是数据集中评分最高的产品。
庄园咖啡豆数据集

庄园咖啡豆数据集包含12款来自世界知名庄园的精品咖啡豆原豆,每款都标注了庄园名称、种植海拔和处理法。
private beanList: BeanItem[] = [
{ name: '翡翠庄园绿标瑰夏', farm: '翡翠庄园', altitude: 1650, price: 198, process: '水洗' },
{ name: '90+ 烛芒日晒', farm: 'Ninety Plus', altitude: 1800, price: 268, process: '日晒' },
{ name: '花魁 7.0 日晒原豆', farm: 'Hambela', altitude: 2100, price: 128, process: '日晒' },
{ name: '云南保山铁皮卡', farm: '佐园咖啡', altitude: 1500, price: 62, process: '水洗' },
{ name: '蓝山一号认证豆', farm: 'Clifton Mount', altitude: 1300, price: 168, process: '水洗' },
{ name: '黄金曼特宁 G1', farm: 'Takengon', altitude: 1400, price: 76, process: '湿刨' },
{ name: '哥斯达黎加黑蜜', farm: 'Tarrazu', altitude: 1600, price: 92, process: '黑蜜' },
{ name: '巴拿马驴子庄园', farm: 'El Burro', altitude: 1750, price: 156, process: '水洗' },
{ name: '肯尼亚 SL28 水洗', farm: 'Nyeri', altitude: 1900, price: 112, process: '水洗' },
{ name: '云南日晒卡杜拉', farm: '孟连庄园', altitude: 1350, price: 48, process: '日晒' },
{ name: '危地马拉薇薇特南果', farm: 'Huehuetenango', altitude: 1850, price: 98, process: '水洗' },
{ name: '巴拿马哈特曼庄园', farm: 'Hartmann', altitude: 1700, price: 142, process: '蜜处理' }
]
庄园咖啡豆的altitude海拔字段在海拔柱状图中通过b.altitude / this.maxAltitude() * 84公式计算柱高比例。花魁7.0日晒原豆的海拔2100m是数据集中最高的,柱状图中对应的柱子也最高。farm庄园名称和处理法process在列表中分别使用不同颜色的标签展示。庄园咖啡豆列表还创新性地引入了序号排名设计,前三名使用橙色编号高亮。
茶叶与器具数据集
茶叶数据集包含10款六大茶类的代表产品,器具数据集包含10款不同材质和容量的冲泡器具。
private teas: TeaItem[] = [
{ name: '明前西湖龙井 50g 罐装', type: '绿茶', ferment: 5, price: 158, rating: 96 },
{ name: '金骏眉红茶 250g 礼盒', type: '红茶', ferment: 85, price: 198, rating: 95 },
{ name: '凤凰单丛蜜兰香 100g', type: '乌龙', ferment: 60, price: 128, rating: 94 },
{ name: '云南古树普洱生茶 357g', type: '普洱', ferment: 20, price: 268, rating: 97 },
{ name: '福鼎白毫银针 50g', type: '白茶', ferment: 10, price: 218, rating: 95 },
{ name: '安溪铁观音清香型 250g', type: '乌龙', ferment: 55, price: 118, rating: 92 },
{ name: '正山小种传统松烟香', type: '红茶', ferment: 90, price: 148, rating: 93 },
{ name: '君山银针黄茶 50g', type: '黄茶', ferment: 30, price: 188, rating: 91 },
{ name: '茉莉花茶特级 200g', type: '花茶', ferment: 70, price: 88, rating: 92 },
{ name: '武夷岩茶大红袍 160g', type: '乌龙', ferment: 65, price: 238, rating: 96 }
]
private gears: GearItem[] = [
{ name: 'Hario V60 树脂滤杯 02 号', material: '树脂', capacity: 400, price: 68, stock: 421 },
{ name: 'Kalita 蛋糕杯 155 不锈钢', material: '不锈钢', capacity: 500, price: 158, stock: 187 },
{ name: '聪明杯浸泡式滤杯 350ml', material: 'PP 树脂', capacity: 350, price: 88, stock: 312 },
{ name: '法压壶耐热玻璃 600ml', material: '玻璃', capacity: 600, price: 98, stock: 265 },
{ name: '摩卡壶经典八角 3 杯份', material: '铝合金', capacity: 150, price: 128, stock: 198 },
{ name: '爱乐压便携压滤壶', material: 'PP+硅胶', capacity: 250, price: 218, stock: 156 },
{ name: '手冲鹅颈壶 0.9L 细嘴', material: '不锈钢', capacity: 900, price: 189, stock: 234 },
{ name: '陶瓷手冲套装五件套', material: '陶瓷', capacity: 400, price: 268, stock: 87 },
{ name: '意式咖啡机家用半自动', material: '不锈钢机身', capacity: 1500, price: 1999, stock: 45 },
{ name: '便携电动磨豆机 USB 充电', material: 'ABS+合金', capacity: 30, price: 328, stock: 176 }
]
茶叶的ferment发酵度字段在发酵度柱状图中通过t.ferment / 100 * 80 + 4公式计算柱高,正山小种的发酵度90%对应最高柱。rating好评率在进度条中直接作为百分比使用。器具的capacity容量字段在容量柱状图中通过g.capacity / 1500 * 80 + 3公式计算柱高,意式咖啡机的1500ml容量对应最高柱。当器具库存低于100时,通过条件渲染显示"少量"标签并配合脉冲动画提醒用户。
门店数据集
门店数据集包含10家不同城市的线下体验门店,每家门店标注了距离、评分、总席位和已预约数。
private shopList: ShopItem[] = [
{ name: '国贸旗舰店', dist: 1.2, score: 4.9, seats: 40, booked: 34 },
{ name: '三里屯店', dist: 2.8, score: 4.8, seats: 56, booked: 30 },
{ name: '望京小街店', dist: 3.5, score: 4.7, seats: 32, booked: 21 },
{ name: '陆家嘴店', dist: 5.1, score: 4.9, seats: 48, booked: 42 },
{ name: '中关村店', dist: 4.2, score: 4.6, seats: 28, booked: 12 },
{ name: '环贸 iapm 店', dist: 2.1, score: 4.8, seats: 36, booked: 25 },
{ name: '万象城店', dist: 6.3, score: 4.7, seats: 44, booked: 28 },
{ name: '太古里店', dist: 3.9, score: 4.9, seats: 52, booked: 48 },
{ name: '滨江天街店', dist: 7.5, score: 4.5, seats: 24, booked: 10 },
{ name: '首钢园店', dist: 9.8, score: 4.6, seats: 30, booked: 16 }
]
门店的booked已预约数在预约热度柱状图中通过s.booked / this.maxScore() * 80公式计算柱高。太古里店的已预约数48是数据集中最高的,柱状图对应柱子也最高。门店预约率进度条通过s.booked / s.seats * 100计算填充百分比,当预约率超过80%时进度条使用橙色#E65100填充表示即将满员,否则使用绿色#7CB342表示充裕。这种基于阈值的动态颜色切换是本应用的一大交互亮点。
消息数据集

消息数据集包含12条不同类型的系统通知,覆盖发货、订阅、预约、烘焙出炉、会员日等场景。
private messages: MessageItem[] = [
{ title: '发货通知', content: '您的瑰夏挂耳礼盒已由顺丰揽收,全程冷链', time: '11:02', unread: 2 },
{ title: '订阅提醒', content: '下期云南小粒豆将在周四烘焙,周五发出', time: '10:15', unread: 1 },
{ title: '门店预约', content: '国贸旗舰店手冲体验课已预约成功,请准时到店', time: '09:30', unread: 3 },
{ title: '烘焙出炉', content: '本周三支新豆出炉:花魁 7.0 已上架', time: '昨天', unread: 0 },
{ title: '会员日', content: '每月 18 日会员日,全场咖啡豆 88 折', time: '昨天', unread: 0 },
{ title: '品鉴邀请', content: '您获得 1 张杯测品鉴会入场券,本周六开场', time: '前天', unread: 1 },
{ title: '优惠到账', content: '咖啡节专享券 3 张已放入卡包,满 99 可用', time: '前天', unread: 0 },
{ title: '器具保养', content: '您的磨豆机已到建议清洁周期,查看教程', time: '周三', unread: 0 },
{ title: '茶叶上新', content: '明前龙井头采开售,限量 200 罐', time: '周三', unread: 1 },
{ title: '订单完成', content: '法压壶已签收,晒图可返 10 元京豆', time: '周二', unread: 0 },
{ title: '降价通知', content: '购物车里的聪明杯降价 20 元', time: '周一', unread: 0 },
{ title: '系统公告', content: '咖啡订阅服务升级,可随时暂停配送', time: '上周', unread: 0 }
]
消息的unread字段大于0时在UI中显示未读数量徽标,使用橙色背景配合600ms脉冲动画。消息头像使用浅棕色#EFEBE9圆形占位区,与咖啡主题的棕色系配色保持一致。
数据聚合计算方法

应用定义了两个私有方法用于在数据集合中计算最大值,为柱状图可视化提供比例基准。
private maxAltitude(): number {
let m: number = 0
for (let i = 0; i < this.beanList.length; i++) {
if (this.beanList[i].altitude > m) {
m = this.beanList[i].altitude
}
}
return m
}
private maxScore(): number {
let m: number = 0
for (let i = 0; i < this.shopList.length; i++) {
if (this.shopList[i].booked > m) {
m = this.shopList[i].booked
}
}
return m
}
maxAltitude方法遍历庄园咖啡豆数组找到最高海拔值,作为海拔柱状图的比例基准。maxScore方法遍历门店数组找到最大已预约数,作为预约热度柱状图的比例基准。两个方法均采用for循环加条件比较的经典算法,时间复杂度为O(n)。
选中项安全访问方法

由于selectedItem可能为null,应用定义了五个安全访问方法防止空指针异常。
private selName(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.name
}
private selPrice(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.price
}
private selOrigin(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.origin
}
private selScore(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.score
}
private selSold(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.sold
}
这些方法统一采用相同的空值检查模式。selOrigin返回选中咖啡的产区,selScore返回杯测评分,这两个字段是咖啡详情弹窗中展示的核心信息。在ArkTS的严格类型系统中,联合类型CoffeeItem | null要求在访问属性前必须进行显式空值检查,这是语言层面的安全约束。
主页面build方法与布局架构
build方法是组件的渲染入口,采用Stack作为根容器,内部嵌套Column实现头部、滚动内容和底部导航栏的三段式布局。
build() {
Stack() {
Column() {
this.headerBar()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabMarket()
} else if (this.currentTab === 1) {
this.tabBean()
} else if (this.currentTab === 2) {
this.tabTea()
} else if (this.currentTab === 3) {
this.tabGear()
} else if (this.currentTab === 4) {
this.tabShop()
} else if (this.currentTab === 5) {
this.tabMessage()
} else {
this.tabMine()
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
this.bottomBar()
}
.width('100%')
.height('100%')
if (this.showPublish) {
this.modalPublish()
}
if (this.showSubscribe) {
this.modalSubscribe()
}
if (this.showVisit) {
this.modalVisit()
}
if (this.showDelete) {
this.modalDelete()
}
if (this.showDetail) {
this.modalDetail()
}
}
.width('100%')
.height('100%')
.backgroundColor('#FAF6F0')
}
Stack布局将五个模态弹窗层叠在主内容之上。背景色#FAF6F0为温暖的浅棕色调,呼应咖啡的主题色彩。Tab切换通过if-else条件链实现,当currentTab的值改变时ArkUI框架自动触发重新渲染。
头部与底部导航栏
头部栏包含标题、搜索框、消息入口和热门关键词标签,底部导航栏通过ForEach渲染七个Tab按钮。
@Builder
headerBar() {
Column() {
Row() {
Column() {
Text('咖啡 · 茗茶')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('京东生活馆')
.fontSize(10)
.fontColor('#D7CCC8')
.margin({ top: 2 })
}
Column() {
Row() {
Text('搜索咖啡豆 / 茶叶 / 手冲器具')
.fontSize(12)
.fontColor('#BCAAA4')
}
.width('100%')
.height(30)
.backgroundColor('#FFFFFF')
.borderRadius(15)
.justifyContent(FlexAlign.Start)
.padding({ left: 14 })
}
.layoutWeight(1)
.margin({ left: 12, right: 12 })
Column() {
Text('消息')
.fontSize(13)
.fontColor('#FFFFFF')
}
.onClick(() => {
this.currentTab = 5
})
}
.width('100%')
.height(54)
.padding({ left: 14, right: 14 })
.alignItems(VerticalAlign.Center)
Row() {
ForEach(['咖啡豆', '挂耳', '意式', '龙井', '普洱', '白茶', '手冲壶'], (k: string) => {
Text(k)
.fontSize(11)
.fontColor('#D7CCC8')
.backgroundColor('rgba(255,255,255,0.12)')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ right: 8 })
})
}
.width('100%')
.padding({ left: 14, bottom: 8 })
.justifyContent(FlexAlign.Start)
}
.width('100%')
.backgroundColor('#5D4037')
}
头部使用深棕色#5D4037背景配合白色标题和浅棕色副标题"京东生活馆"。第二行通过ForEach渲染七个热门关键词标签,使用半透明白色背景rgba(255,255,255,0.12)和圆角设计。这种热门关键词标签栏是前两个应用所没有的特色功能,为用户提供了快速搜索入口。底部导航栏选中状态使用琥珀色#FFB300指示条和文字,与头部棕色背景形成冷暖对比。
咖啡集市Tab与产区筛选
咖啡集市Tab包含活动横幅、数据统计卡片、产区筛选器和商品列表,是应用的核心展示页面。
@Builder
tabMarket() {
Column() {
Row() {
Column() {
Text('早春豆单上新')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('埃塞俄比亚 · 瑰夏村直发 · 满159减40')
.fontSize(11)
.fontColor('#D7CCC8')
.margin({ top: 6 })
Text('查看豆单')
.fontSize(12)
.fontColor('#4E342E')
.backgroundColor('#FFB300')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ top: 12 })
}
.layoutWeight(2)
.alignItems(HorizontalAlign.Start)
Column() {
Text('7842')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFB300')
Text('爆款已售')
.fontSize(10)
.fontColor('#D7CCC8')
.margin({ top: 2 })
Text('订阅豆单')
.fontSize(11)
.fontColor('#4E342E')
.backgroundColor('#FFB300')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ top: 8 })
.scale({ x: 1.06, y: 1.06 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.margin({ left: 10 })
.onClick(() => {
this.showSubscribe = true
})
}
.width('100%')
.padding(16)
.backgroundColor('#5D4037')
.borderRadius(12)
.alignItems(VerticalAlign.Center)
活动横幅采用左右分栏布局:左侧layoutWeight(2)展示活动信息和"查看豆单"按钮,右侧layoutWeight(1)展示爆款已售数7842和"订阅豆单"按钮。"订阅豆单"按钮使用700ms脉冲动画引导用户点击,点击打开订阅弹窗。横幅背景使用深棕色#5D4037与头部背景一致,按钮使用琥珀色#FFB300形成视觉焦点。
Row() {
Column() {
Text('24')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
Text('在售单品')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10 })
Column() {
Text('99')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('最高杯测分')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
Column() {
Text('5 大')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
Text('著名产区')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
Column() {
Text('发布')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('闲置转让')
.fontSize(10)
.fontColor('#FFCC80')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#E65100')
.borderRadius(10)
.margin({ top: 10, left: 8 })
.onClick(() => {
this.showPublish = true
})
}
.width('100%')
数据统计区域使用四等分卡片布局:在售单品数24(棕色数字)、最高杯测分99(橙色数字)、著名产区数5大(棕色文字)和发布闲置入口(橙色背景按钮)。前三个卡片使用白色背景展示数据,第四个卡片使用橙色#E65100背景作为行动召唤按钮,点击打开发布弹窗。
Scroll() {
Row() {
ForEach(this.origins, (o: string, idx: number) => {
Text(o)
.fontSize(12)
.fontColor(this.selectedOrigin === idx ? '#FFFFFF' : '#5D4037')
.backgroundColor(this.selectedOrigin === idx ? '#5D4037' : '#FFFFFF')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ right: 8 })
.onClick(() => {
this.selectedOrigin = idx
})
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
Column() {
ForEach(this.coffees, (c: CoffeeItem) => {
Row() {
Column()
.width(84)
.height(84)
.backgroundColor('#D7CCC8')
.borderRadius(42)
Column() {
Text(c.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(c.origin)
.fontSize(10)
.fontColor('#E65100')
.backgroundColor('#FFF3E0')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(c.roast)
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 6 })
Text('杯测 ' + c.score)
.fontSize(10)
.fontColor('#5D4037')
.fontWeight(FontWeight.Bold)
.margin({ left: 6 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
}
.margin({ top: 6 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(6)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(c.score + '%')
.height(6)
.backgroundColor('#E65100')
.borderRadius(3)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('¥' + c.price)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('已售' + c.sold)
.fontSize(11)
.fontColor('#999999')
.margin({ left: 8 })
Text('订阅价')
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor('#5D4037')
.borderRadius(10)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ left: 6 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ top: 8 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.selectedItem = c
this.showDetail = true
})
})
}
.width('100%')
}
}
产区筛选器选中时使用棕色背景白色文字。商品图片占位区使用了圆形设计(borderRadius(42)即84/2),与前两个应用的方形图片占位区形成差异。产区标签使用橙色浅色背景,烘焙度标签使用绿色背景,杯测评分文本使用棕色字体配合800ms脉冲动画。杯测评分进度条使用橙色#E65100填充。"订阅价"按钮使用棕色背景,区别于"立即购买"的橙色按钮,体现了订阅业务的差异化定位。
咖啡豆Tab与海拔柱状图及排名设计
咖啡豆Tab的核心特色是海拔柱状图可视化和带序号编号的排名列表设计,是精品咖啡溯源信息的深度展示页面。
@Builder
tabBean() {
Column() {
Column() {
Text('庄园豆海拔对比 (m)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row() {
ForEach(this.beanList, (b: BeanItem) => {
Column() {
Column()
.width(15)
.height(b.altitude / this.maxAltitude() * 84)
.backgroundColor('#FFB300')
.borderRadius(4)
Text(b.altitude + '')
.fontSize(8)
.fontColor('#D7CCC8')
.margin({ top: 3 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(112)
.alignItems(VerticalAlign.End)
.justifyContent(FlexAlign.Start)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#4E342E')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
海拔柱状图使用琥珀色#FFB300柱体,每根柱子宽度15px,高度通过b.altitude / this.maxAltitude() * 84公式计算。柱子下方显示海拔数值,使用8px小字号避免遮挡。外层Row设置height(112)和alignItems(VerticalAlign.End)确保柱子从底部对齐生长。
Row() {
Column() {
Text('订阅咖啡豆')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('周期烘焙 · 定时配送')
.fontSize(10)
.fontColor('#FFCC80')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(52)
.backgroundColor('#5D4037')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
.onClick(() => {
this.showSubscribe = true
})
Column() {
Text('发布转让')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('闲置咖啡豆')
.fontSize(10)
.fontColor('#FFCC80')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(52)
.backgroundColor('#E65100')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
.margin({ top: 10, left: 8 })
.onClick(() => {
this.showPublish = true
})
}
.width('100%')
"订阅咖啡豆"和"发布转让"两个按钮使用等分布局,分别使用棕色和橙色背景,每个按钮包含主标题和副标题两行文字。
Column() {
ForEach(this.beanList, (b: BeanItem, idx: number) => {
Row() {
Column() {
Text('No.' + (idx + 1))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#E65100' : '#BCAAA4')
}
.width(44)
.height(64)
.backgroundColor('#EFEBE9')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
Column() {
Text(b.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(b.farm)
.fontSize(10)
.fontColor('#5D4037')
.backgroundColor('#EFEBE9')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(b.process)
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 6 })
Text('海拔 ' + b.altitude + 'm')
.fontSize(10)
.fontColor('#999999')
.margin({ left: 6 })
}
.margin({ top: 6 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(5)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(b.altitude / this.maxAltitude() * 100 + '%')
.height(5)
.backgroundColor('#FFB300')
.borderRadius(3)
}
.width('100%')
.margin({ top: 8 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
Column() {
Text('¥' + b.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('200g/袋')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.selectedItem = this.coffees[idx % this.coffees.length]
this.showDetail = true
})
})
}
.width('100%')
}
}
庄园咖啡豆列表引入了带序号编号的排名设计:每个卡片左侧有一个44px宽的编号区,显示"No.1"、"No.2"等排名编号。前三名(idx < 3)使用橙色#E65100编号高亮,第四名及以后使用浅棕色#BCAAA4,通过颜色差异突出排名前三的精品咖啡豆。庄园名称标签使用浅棕色背景,处理法标签使用绿色背景,海拔文本使用灰色字体。海拔进度条使用琥珀色#FFB300填充,与海拔柱状图的柱体颜色一致。点击咖啡豆卡片通过this.coffees[idx % this.coffees.length]将对应的咖啡商品赋值给selectedItem,取模运算确保索引不越界。
茶叶专区Tab与发酵度柱状图
茶叶Tab展示六大茶类发酵度柱状图和茶叶列表,列表中使用双列网格布局展示茶叶卡片。
@Builder
tabTea() {
Column() {
Column() {
Text('茗茶专区')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#33691E')
Text('明前头采 · 茶园直供 · 冷链锁鲜')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 14, bottom: 14, left: 14 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
.margin({ top: 10 })
Column() {
Text('六大茶类发酵度对比 (%)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row() {
ForEach(this.teas, (t: TeaItem) => {
Column() {
Column()
.width(15)
.height(t.ferment / 100 * 80 + 4)
.backgroundColor('#7CB342')
.borderRadius(4)
Text(t.ferment + '')
.fontSize(9)
.fontColor('#999999')
.margin({ top: 4 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(110)
.alignItems(VerticalAlign.End)
.justifyContent(FlexAlign.Start)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
发酵度柱状图使用绿色#7CB342柱体,高度通过t.ferment / 100 * 80 + 4公式计算。发酵度为90%的正山小种对应最高柱(76px),发酵度仅5%的西湖龙井对应最矮柱(8px)。+ 4的基础高度确保即使发酵度为0的茶叶也有最低可见的柱高。
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.teas, (t: TeaItem) => {
Column() {
Row() {
Text(t.type)
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('发酵 ' + t.ferment + '%')
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Text(t.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
Row() {
Text('¥' + t.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('好评 ' + t.rating + '%')
.fontSize(10)
.fontColor('#5D4037')
.margin({ left: 6 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 750, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(5)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(t.rating + '%')
.height(5)
.backgroundColor('#7CB342')
.borderRadius(3)
}
.width('100%')
.margin({ top: 6 })
}
.width('48%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
.onClick(() => {
this.showSubscribe = true
})
})
}
.width('100%')
}
}
茶叶列表使用Flex双列网格布局,每个卡片宽度48%。茶类标签使用绿色背景,发酵度文本使用灰色字体。茶叶名称使用maxLines(2)允许两行显示。好评率文本使用750ms脉冲动画强调,好评率进度条使用绿色填充。点击茶叶卡片打开订阅弹窗(此处复用了订阅弹窗作为通用下单交互),体现了茶叶与咖啡在订阅业务上的共享流程。
器具冲泡Tab与容量柱状图
器具Tab展示器具容量柱状图、水平滚动器具卡片和垂直器具列表,列表中通过库存进度条和少量标签提示库存状态。
@Builder
tabGear() {
Column() {
Column() {
Row() {
Text('器具冲泡实验室')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('容量对比 (ml)')
.fontSize(11)
.fontColor('#D7CCC8')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
ForEach(this.gears, (g: GearItem) => {
Column() {
Column()
.width(15)
.height(g.capacity / 1500 * 80 + 3)
.backgroundColor('#FF7043')
.borderRadius(4)
Text(g.capacity + '')
.fontSize(8)
.fontColor('#D7CCC8')
.margin({ top: 3 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(110)
.alignItems(VerticalAlign.End)
.justifyContent(FlexAlign.Start)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#6D4C41')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
容量柱状图使用橙红色#FF7043柱体,高度通过g.capacity / 1500 * 80 + 3公式计算,1500ml为基准值。意式咖啡机的1500ml容量对应最高柱(83px),便携电动磨豆机的30ml容量对应最矮柱(4.6px)。+ 3的基础高度确保小容量器具也有最低可见柱高。
Scroll() {
Row() {
ForEach(this.gears, (g: GearItem) => {
Column() {
Column()
.width(96)
.height(60)
.backgroundColor('#D7CCC8')
.borderRadius(10)
Text(g.material)
.fontSize(10)
.fontColor('#6D4C41')
.backgroundColor('#EFEBE9')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ top: 6 })
Text('¥' + g.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
.margin({ top: 4 })
}
.width(96)
.padding(8)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ right: 8 })
.alignItems(HorizontalAlign.Center)
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
Column() {
ForEach(this.gears, (g: GearItem) => {
Row() {
Column()
.width(58)
.height(58)
.backgroundColor('#EFEBE9')
.borderRadius(10)
Column() {
Text(g.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(g.capacity + 'ml')
.fontSize(10)
.fontColor('#6D4C41')
.backgroundColor('#EFEBE9')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('库存 ' + g.stock)
.fontSize(10)
.fontColor('#999999')
.margin({ left: 8 })
if (g.stock < 100) {
Text('少量')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#E65100')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 6 })
.scale({ x: 1.08, y: 1.08 })
.animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
}
}
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
Column() {
Text('¥' + g.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('加入购物车')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#6D4C41')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.showDelete = true
})
})
}
.width('100%')
}
}
器具列表中每个卡片左侧有58px的方形图片占位区。容量标签使用浅棕色背景深棕色文字。当库存低于100时通过if (g.stock < 100)条件渲染"少量"标签,使用橙色背景配合600ms脉冲动画。"加入购物车"按钮使用棕色#6D4C41背景,与咖啡集市Tab的"订阅价"按钮使用相同棕色保持一致。
门店体验Tab与预约热度柱状图
门店Tab展示门店体验预约横幅、预约热度柱状图和门店列表,列表中通过预约率进度条和动态颜色切换展示门店热度。
@Builder
tabShop() {
Column() {
Column() {
Row() {
Column() {
Text('门店手冲体验')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('大师现场冲煮 · 免费品鉴三杯')
.fontSize(11)
.fontColor('#FFCC80')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('立即预约')
.fontSize(12)
.fontColor('#4E342E')
.backgroundColor('#FFB300')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
.onClick(() => {
this.showVisit = true
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor('#5D4037')
.borderRadius(12)
Column() {
Text('到店预约热度 (本周)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row() {
ForEach(this.shopList, (s: ShopItem) => {
Column() {
Column()
.width(15)
.height(s.booked / this.maxScore() * 80)
.backgroundColor('#8D6E63')
.borderRadius(4)
Text(s.booked + '')
.fontSize(9)
.fontColor('#999999')
.margin({ top: 4 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(110)
.alignItems(VerticalAlign.End)
.justifyContent(FlexAlign.Start)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
预约热度柱状图使用棕色#8D6E63柱体,高度通过s.booked / this.maxScore() * 80公式计算。太古里店的已预约数48对应最高柱(80px)。"立即预约"按钮使用琥珀色#FFB300背景配合700ms脉冲动画,与头部导航栏的选中色一致。
Column() {
ForEach(this.shopList, (s: ShopItem) => {
Row() {
Column()
.width(50)
.height(50)
.backgroundColor('#D7CCC8')
.borderRadius(10)
Column() {
Row() {
Text(s.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(s.score + ' 分')
.fontSize(11)
.fontColor('#E65100')
.fontWeight(FontWeight.Bold)
.margin({ left: 8 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
}
Text('距离 ' + s.dist + 'km · ' + s.seats + ' 席位')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(6)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(s.booked / s.seats * 100 + '%')
.height(6)
.backgroundColor(s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342')
.borderRadius(3)
}
.width('100%')
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
Column() {
Text(s.booked + '/' + s.seats)
.fontSize(11)
.fontColor('#999999')
Text('预约')
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor('#5D4037')
.borderRadius(10)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.showVisit = true
})
})
}
.width('100%')
}
}
门店列表中的预约率进度条是本应用的技术亮点之一。填充宽度通过s.booked / s.seats * 100计算,填充颜色通过三元表达式s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342'动态切换:当预约率超过80%时使用橙色#E65100表示即将满员,否则使用绿色#7CB342表示充裕。这种基于阈值的动态颜色设计让用户能够直观感知门店的热度状态,无需阅读具体数字即可判断是否需要尽快预约。评分文本使用800ms脉冲动画强调展示,s.booked + '/' + s.seats格式直观展示已预约/总席位的比例关系。
消息中心与我的页面
消息中心通过ForEach渲染12条消息列表,我的页面展示用户信息、统计数据和功能菜单。
@Builder
tabMessage() {
Column() {
Column() {
Row() {
Text('消息中心')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('8 条新动态')
.fontSize(11)
.fontColor('#E65100')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding({ top: 14, bottom: 14, left: 14, right: 14 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10 })
Column() {
ForEach(this.messages, (m: MessageItem) => {
Row() {
Column()
.width(46)
.height(46)
.backgroundColor('#EFEBE9')
.borderRadius(23)
Column() {
Row() {
Text(m.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
if (m.unread > 0) {
Text(m.unread + '')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#E65100')
.borderRadius(9)
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.margin({ left: 6 })
.scale({ x: 1.1, y: 1.1 })
.animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
}
Text(m.time)
.fontSize(10)
.fontColor('#BBBBBB')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Text(m.content)
.fontSize(12)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(12)
.margin({ top: 10 })
.onClick(() => {
this.showDelete = true
})
})
}
.width('100%')
}
}
消息头像使用浅棕色#EFEBE9圆形占位区,未读徽标使用橙色#E65100背景配合600ms脉冲动画。消息布局采用justifyContent(FlexAlign.SpaceBetween)将标题、未读徽标和时间分置两端。
@Builder
tabMine() {
Column() {
Column() {
Row() {
Column()
.width(60)
.height(60)
.backgroundColor('#D7CCC8')
.borderRadius(30)
Column() {
Text('手冲玩家_Leo')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('咖啡品味等级:银杯杯测师')
.fontSize(11)
.fontColor('#FFCC80')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 12 })
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor('#4E342E')
.borderRadius(12)
Row() {
Column() {
Text('52')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
Text('咖啡订单')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(64)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10 })
Column() {
Text('3')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('门店体验')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(64)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
Column() {
Text('680')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
Text('品味积分')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(64)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
Column() {
Text('2')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('品鉴券')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(64)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
}
.width('100%')
Column() {
ForEach(['我的豆单订阅', '常购茶单', '器具收藏夹', '门店体验记录', '杯测笔记', '帮助与反馈'], (item: string) => {
Row() {
Text(item)
.fontSize(14)
.fontColor('#333333')
Text('>')
.fontSize(14)
.fontColor('#CCCCCC')
}
.width('100%')
.padding({ top: 14, bottom: 14 })
.justifyContent(FlexAlign.SpaceBetween)
.onClick(() => {
this.showDelete = true
})
})
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ left: 14, right: 14 })
.margin({ top: 12 })
}
}
用户信息区域使用深棕色#4E342E背景配合白色用户名和浅琥珀色#FFCC80等级文字。统计区域通过四等分卡片展示咖啡订单、门店体验、品味积分和品鉴券数据,交替使用棕色和橙色数字。功能菜单列表使用ForEach渲染六个菜单项,包括"我的豆单订阅"、“常购茶单”、"杯测笔记"等咖啡特色菜单项。
模态弹窗——咖啡豆周期订阅
订阅弹窗是咖啡业务的核心交互界面,支持豆种选择、研磨粗细和订阅周期配置。
@Builder
modalSubscribe() {
Column() {
Column().layoutWeight(1).width('100%')
Column() {
Row() {
Text('咖啡豆周期订阅')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('¥' + ((this.selectedBean + 1) * 19 + 12) * this.weeks)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
Column() {
Text('首选豆种')
.fontSize(13)
.fontColor('#666666')
Row() {
ForEach(this.beans, (b: string, idx: number) => {
Text(b)
.fontSize(12)
.fontColor(this.selectedBean === idx ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedBean === idx ? '#5D4037' : '#FAF6F0')
.borderRadius(8)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.margin({ right: 8, top: 8 })
.onClick(() => {
this.selectedBean = idx
})
})
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
订阅费用计算公式为((this.selectedBean + 1) * 19 + 12) * this.weeks,将豆种索引加1后乘以19元基础价、加上12元固定费、再乘以订阅周数。豆种选择器选中时使用棕色#5D4037背景,五个选项为瑰夏、耶加雪菲、蓝山、曼特宁和云南小粒。订阅费用使用700ms脉冲动画强调展示。
Column() {
Text('研磨粗细')
.fontSize(13)
.fontColor('#666666')
Row() {
ForEach(this.grinds, (g: string, idx: number) => {
Text(g)
.fontSize(12)
.fontColor(this.selectedGrind === idx ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedGrind === idx ? '#7CB342' : '#FAF6F0')
.borderRadius(8)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.margin({ right: 8, top: 8 })
.onClick(() => {
this.selectedGrind = idx
})
})
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('订阅周期:' + this.weeks + ' 周')
.fontSize(13)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
Row() {
Text('-')
.fontSize(16)
.fontColor('#5D4037')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.backgroundColor('#EFEBE9')
.borderRadius(8)
.onClick(() => {
if (this.weeks > 1) {
this.weeks -= 1
}
})
Text(this.weeks + '')
.fontSize(15)
.fontColor('#333333')
.width(50)
.textAlign(TextAlign.Center)
Text('+')
.fontSize(16)
.fontColor('#E65100')
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.backgroundColor('#FFF3E0')
.borderRadius(8)
.onClick(() => {
if (this.weeks < 24) {
this.weeks += 1
}
})
}
.margin({ top: 8 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Text('每周新鲜烘焙直达,可随时暂停')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 10 })
Text('确认订阅')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(44)
.textAlign(TextAlign.Center)
.backgroundColor('#E65100')
.borderRadius(22)
.margin({ top: 16 })
.onClick(() => {
this.showSubscribe = false
})
}
.width('100%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.padding(16)
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showSubscribe = false
})
}
研磨粗细选择器选中时使用绿色#7CB342背景,四个选项为手冲粗度、法压中度、摩卡细度和意式极细,不同研磨度对应不同的冲泡方式。订阅周期调节器范围限制在1到24周之间,减号使用浅棕色背景加号使用浅橙色背景。底部提示"每周新鲜烘焙直达,可随时暂停"增强了用户对订阅服务的信任感。确认订阅按钮使用橙色全宽胶囊设计。
模态弹窗——门店体验预约
门店预约弹窗支持门店选择、到店人数和体验内容展示,是线下体验服务的核心交互界面。
@Builder
modalVisit() {
Column() {
Column().layoutWeight(1).width('100%')
Column() {
Row() {
Text('门店体验预约')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('免费')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.scale({ x: 1.06, y: 1.06 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
Column() {
Text('选择门店')
.fontSize(13)
.fontColor('#666666')
Row() {
ForEach(this.shops, (s: string, idx: number) => {
Text(s)
.fontSize(12)
.fontColor(this.selectedShop === idx ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedShop === idx ? '#5D4037' : '#FAF6F0')
.borderRadius(8)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.margin({ right: 8, top: 8 })
.onClick(() => {
this.selectedShop = idx
})
})
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('到店人数')
.fontSize(13)
.fontColor('#666666')
Row() {
ForEach(this.guestOptions, (g: string, idx: number) => {
Text(g)
.fontSize(12)
.fontColor(this.selectedGuests === idx ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedGuests === idx ? '#E65100' : '#FAF6F0')
.borderRadius(8)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.margin({ right: 8, top: 8 })
.onClick(() => {
this.selectedGuests = idx
})
})
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('体验内容')
.fontSize(13)
.fontColor('#666666')
Text('大师手冲演示 · 三款庄园豆品鉴 · 拉花体验')
.fontSize(12)
.fontColor('#5D4037')
.margin({ top: 6 })
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Text('提交预约')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(44)
.textAlign(TextAlign.Center)
.backgroundColor('#5D4037')
.borderRadius(22)
.margin({ top: 20 })
.onClick(() => {
this.showVisit = false
})
}
.width('100%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.padding(16)
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showVisit = false
})
}
门店预约弹窗的"免费"标签使用绿色背景配合700ms脉冲动画,强调体验服务的免费属性吸引用户预约。门店选择器选中时使用棕色背景,到店人数选择器选中时使用橙色背景,两个选择器使用不同颜色区分维度。体验内容区域以纯文本形式展示三项体验内容:“大师手冲演示”、“三款庄园豆品鉴"和"拉花体验”。提交预约按钮使用棕色全宽胶囊设计。
模态弹窗——发布闲置与商品详情
发布闲置弹窗支持商品名称输入、豆种选择和烘焙度选择,商品详情弹窗采用右侧滑出面板设计。
@Builder
modalPublish() {
Column() {
Column().layoutWeight(1).width('100%')
Column() {
Row() {
Text('发布闲置咖啡茶品')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('关闭')
.fontSize(13)
.fontColor('#999999')
.padding(6)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Column() {
Text('商品名称')
.fontSize(13)
.fontColor('#666666')
TextInput({ placeholder: '如:翡翠庄园瑰夏 200g' })
.height(40)
.fontSize(13)
.backgroundColor('#FAF6F0')
.borderRadius(8)
.margin({ top: 6 })
.onChange((value: string) => {
this.inputName = value
})
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('咖啡豆种')
.fontSize(13)
.fontColor('#666666')
Row() {
ForEach(this.beans, (b: string, idx: number) => {
Text(b)
.fontSize(12)
.fontColor(this.selectedBean === idx ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedBean === idx ? '#5D4037' : '#FAF6F0')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ right: 8, top: 8 })
.onClick(() => {
this.selectedBean = idx
})
})
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('烘焙度')
.fontSize(13)
.fontColor('#666666')
Row() {
ForEach(this.roasts, (r: string, idx: number) => {
Text(r)
.fontSize(12)
.fontColor(this.selectedRoast === idx ? '#FFFFFF' : '#666666')
.backgroundColor(this.selectedRoast === idx ? '#E65100' : '#FAF6F0')
.borderRadius(8)
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.margin({ right: 8, top: 8 })
.onClick(() => {
this.selectedRoast = idx
})
})
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
}
.width('100%')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
Column() {
Text('预估价:¥' + ((this.selectedBean + 1) * 28 + this.selectedRoast * 10))
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
.scale({ x: 1.04, y: 1.04 })
.animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.margin({ top: 20 })
.alignItems(HorizontalAlign.Start)
Text('确认发布')
.fontSize(15)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(44)
.textAlign(TextAlign.Center)
.backgroundColor('#5D4037')
.borderRadius(22)
.margin({ top: 20 })
.onClick(() => {
this.showPublish = false
})
}
.width('100%')
.constraintSize({ maxHeight: '80%' })
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.padding(16)
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showPublish = false
})
}
发布闲置弹窗的预估价计算公式为(this.selectedBean + 1) * 28 + this.selectedRoast * 10,将豆种索引和烘焙度索引映射为价格系数。豆种选择器选中时使用棕色背景圆角14,烘焙度选择器选中时使用橙色背景圆角8,两种选择器使用不同的圆角大小进一步区分维度。确认发布按钮使用棕色全宽胶囊设计。
@Builder
modalDetail() {
Row() {
Column().layoutWeight(1).height('100%')
Column() {
Text('单品详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(this.selName())
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
.margin({ top: 14 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(this.selOrigin())
.fontSize(11)
.fontColor('#E65100')
.backgroundColor('#FFF3E0')
.borderRadius(4)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text('已售 ' + this.selSold())
.fontSize(11)
.fontColor('#999999')
.margin({ left: 10 })
}
.margin({ top: 10 })
Column() {
Text('杯测评分 ' + this.selScore())
.fontSize(12)
.fontColor('#666666')
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(8)
.backgroundColor('#F0F0F0')
.borderRadius(4)
Column()
.width(this.selScore() + '%')
.height(8)
.backgroundColor('#FFB300')
.borderRadius(4)
}
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.margin({ top: 16 })
Row() {
Text('¥' + this.selPrice())
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
.scale({ x: 1.06, y: 1.06 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('庄园直发')
.fontSize(11)
.fontColor('#5D4037')
.margin({ left: 10 })
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
.margin({ top: 16 })
Text('立即下单')
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.width('100%')
.height(42)
.textAlign(TextAlign.Center)
.backgroundColor('#E65100')
.borderRadius(21)
.margin({ top: 20 })
.onClick(() => {
this.showDetail = false
this.showSubscribe = true
})
Text('加入豆单')
.fontSize(14)
.fontColor('#5D4037')
.width('100%')
.height(42)
.textAlign(TextAlign.Center)
.backgroundColor('#EFEBE9')
.borderRadius(21)
.margin({ top: 10 })
.onClick(() => {
this.showDetail = false
})
}
.width('78%')
.height('100%')
.backgroundColor('#FFFFFF')
.padding(16)
.alignItems(HorizontalAlign.Start)
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showDetail = false
})
}
详情弹窗采用Row布局将屏幕分为22%透明关闭区和78%白色内容面板。杯测评分进度条使用琥珀色#FFB300填充,与前两个应用使用绿色或粉色填充的设计形成差异。价格使用700ms脉冲动画强调。详情弹窗提供"立即下单"和"加入豆单"两个操作按钮:点击"立即下单"先关闭详情弹窗再打开订阅弹窗(this.showDetail = false; this.showSubscribe = true),实现弹窗级联切换;点击"加入豆单"仅关闭详情弹窗。"加入豆单"按钮使用浅棕色#EFEBE9背景深棕色文字,与"立即下单"的橙色实心按钮形成主次区分。
流程图
以下是咖啡茗茶生活馆应用的核心业务流程,展示了用户从浏览咖啡到完成订阅或门店预约的完整交互路径。
技术点对比表格
| 技术维度 | 实现方式 | 关键API/特性 | 优势 | 适用场景 |
|---|---|---|---|---|
| 状态管理 | @State装饰器 | currentTab、showSubscribe等状态变量 | 单一数据源驱动UI,自动触发重渲染 | Tab切换、弹窗显隐、多维选择 |
| 布局架构 | Stack + Column | Stack叠加模态弹窗 | 弹窗层叠在主内容之上,无需页面跳转 | 多弹窗管理系统 |
| 数据可视化 | 纯ArkUI柱状图 | Column动态height + ForEach | 无需图表库,纯声明式实现数据对比 | 海拔、发酵度、容量、预约热度对比 |
| 进度条组件 | Stack嵌套Column | alignContent: Alignment.Start | 双层叠加实现轨道与填充效果 | 杯测评分、海拔、好评率、预约率展示 |
| 动态颜色 | 三元表达式阈值判断 | booked/seats>0.8?橙:绿 | 基于数据阈值自动切换颜色 | 门店预约率热度可视化 |
| 排名设计 | 序号编号+条件颜色 | idx<3?橙:浅棕 | 前三名高亮,竞技感强 | 庄园咖啡豆品质排名展示 |
| 动画效果 | scale + animation | iterations: -1, Curve.EaseInOut | 无限循环脉冲动画,吸引用户注意 | 营销标签、价格强调、未读徽标 |
| 弹窗级联 | 顺序状态修改 | showDetail=false再showSubscribe=true | 弹窗间无缝切换,无闪烁过渡 | 详情到订阅的场景跳转 |
| 空值安全 | 联合类型+条件检查 | CoffeeItem | null, if null判断 | 编译期类型安全,防止运行时空指针 | 详情弹窗数据访问 |
| 取模索引 | 模运算防越界 | coffees[idx % coffees.length] | 确保索引不越界,安全映射数据 | 咖啡豆到咖啡商品的关联访问 |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
interface CoffeeItem {
name: string
price: number
sold: number
origin: string
roast: string
score: number
}
interface BeanItem {
name: string
farm: string
altitude: number
price: number
process: string
}
interface TeaItem {
name: string
type: string
ferment: number
price: number
rating: number
}
interface GearItem {
name: string
material: string
capacity: number
price: number
stock: number
}
interface ShopItem {
name: string
dist: number
score: number
seats: number
booked: number
}
interface MessageItem {
title: string
content: string
time: string
unread: number
}
@Entry
@Component
struct CoffeeTeaPage {
@State currentTab: number = 0
@State showPublish: boolean = false
@State showSubscribe: boolean = false
@State showVisit: boolean = false
@State showDelete: boolean = false
@State showDetail: boolean = false
@State selectedItem: CoffeeItem | null = null
@State selectedOrigin: number = 0
@State selectedRoast: number = 0
@State selectedBean: number = 0
@State selectedGrind: number = 0
@State selectedShop: number = 0
@State selectedGuests: number = 0
@State weeks: number = 4
@State inputName: string = ''
private tabs: string[] = ['咖啡集市', '咖啡豆', '茶叶专区', '器具冲泡', '门店体验', '消息', '我的']
private origins: string[] = ['埃塞俄比亚', '哥伦比亚', '云南保山', '危地马拉', '巴拿马']
private roasts: string[] = ['浅烘焙', '中烘焙', '中深烘焙', '深烘焙']
private beans: string[] = ['瑰夏', '耶加雪菲', '蓝山', '曼特宁', '云南小粒']
private grinds: string[] = ['手冲粗度', '法压中度', '摩卡细度', '意式极细']
private shops: string[] = ['国贸旗舰店', '三里屯店', '望京小街店', '陆家嘴店']
private guestOptions: string[] = ['1 人', '2 人', '3-4 人', '5 人以上']
private coffees: CoffeeItem[] = [
{ name: '瑰夏手冲挂耳 10 袋礼盒', price: 128, sold: 3214, origin: '巴拿马', roast: '浅烘焙', score: 97 },
{ name: '耶加雪菲日晒挂耳 20 袋', price: 88, sold: 5623, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
{ name: '蓝山一号咖啡豆 227g', price: 168, sold: 2134, origin: '哥伦比亚', roast: '中烘焙', score: 94 },
{ name: '云南保山小粒日晒豆', price: 58, sold: 4876, origin: '云南保山', roast: '中烘焙', score: 91 },
{ name: '危地马拉花神咖啡豆', price: 98, sold: 1856, origin: '危地马拉', roast: '中深烘焙', score: 93 },
{ name: '曼特宁黄金曼咖啡豆', price: 76, sold: 2341, origin: '巴拿马', roast: '深烘焙', score: 92 },
{ name: '冷萃冻干咖啡粉 3 秒即溶', price: 69, sold: 7842, origin: '云南保山', roast: '中烘焙', score: 96 },
{ name: '拿铁冻干咖啡礼盒 24 杯', price: 139, sold: 3267, origin: '哥伦比亚', roast: '中深烘焙', score: 94 },
{ name: '西达摩水洗挂耳 10 袋', price: 79, sold: 4521, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
{ name: '翡翠庄园红标瑰夏 100g', price: 299, sold: 876, origin: '巴拿马', roast: '浅烘焙', score: 99 },
{ name: '云南日晒卡迪姆 200g', price: 45, sold: 6234, origin: '云南保山', roast: '中烘焙', score: 89 },
{ name: '安提瓜火山咖啡豆', price: 108, sold: 1457, origin: '危地马拉', roast: '中深烘焙', score: 93 },
{ name: '椰香拿铁冻干条 30 条', price: 99, sold: 4312, origin: '海南', roast: '中深烘焙', score: 92 },
{ name: '瑰夏村竞标批次 50g', price: 388, sold: 432, origin: '埃塞俄比亚', roast: '浅烘焙', score: 98 },
{ name: '云南铁皮卡蜜处理 200g', price: 72, sold: 1987, origin: '云南保山', roast: '浅烘焙', score: 90 },
{ name: '哥伦比亚蕙兰咖啡豆', price: 68, sold: 2876, origin: '哥伦比亚', roast: '中烘焙', score: 91 },
{ name: '意式拼配金杯豆 1kg', price: 128, sold: 5163, origin: '巴拿马', roast: '深烘焙', score: 95 },
{ name: '低因瑞士水处理豆 227g', price: 118, sold: 965, origin: '哥伦比亚', roast: '中烘焙', score: 92 },
{ name: '手冲单品入门三豆组', price: 108, sold: 2743, origin: '云南保山', roast: '中烘焙', score: 93 },
{ name: '冷萃浓缩液 1L 大瓶装', price: 89, sold: 3562, origin: '云南保山', roast: '中深烘焙', score: 94 },
{ name: '挂耳咖啡混合口味 30 袋', price: 119, sold: 6987, origin: '埃塞俄比亚', roast: '中烘焙', score: 96 },
{ name: '麝香猫风味咖啡豆 100g', price: 158, sold: 654, origin: '巴拿马', roast: '中深烘焙', score: 88 },
{ name: '云南厌氧发酵黑蜜 200g', price: 96, sold: 1234, origin: '云南保山', roast: '浅烘焙', score: 95 },
{ name: '经典意式浓缩豆 500g', price: 85, sold: 4231, origin: '危地马拉', roast: '深烘焙', score: 92 }
]
private beanList: BeanItem[] = [
{ name: '翡翠庄园绿标瑰夏', farm: '翡翠庄园', altitude: 1650, price: 198, process: '水洗' },
{ name: '90+ 烛芒日晒', farm: 'Ninety Plus', altitude: 1800, price: 268, process: '日晒' },
{ name: '花魁 7.0 日晒原豆', farm: 'Hambela', altitude: 2100, price: 128, process: '日晒' },
{ name: '云南保山铁皮卡', farm: '佐园咖啡', altitude: 1500, price: 62, process: '水洗' },
{ name: '蓝山一号认证豆', farm: 'Clifton Mount', altitude: 1300, price: 168, process: '水洗' },
{ name: '黄金曼特宁 G1', farm: 'Takengon', altitude: 1400, price: 76, process: '湿刨' },
{ name: '哥斯达黎加黑蜜', farm: 'Tarrazu', altitude: 1600, price: 92, process: '黑蜜' },
{ name: '巴拿马驴子庄园', farm: 'El Burro', altitude: 1750, price: 156, process: '水洗' },
{ name: '肯尼亚 SL28 水洗', farm: 'Nyeri', altitude: 1900, price: 112, process: '水洗' },
{ name: '云南日晒卡杜拉', farm: '孟连庄园', altitude: 1350, price: 48, process: '日晒' },
{ name: '危地马拉薇薇特南果', farm: 'Huehuetenango', altitude: 1850, price: 98, process: '水洗' },
{ name: '巴拿马哈特曼庄园', farm: 'Hartmann', altitude: 1700, price: 142, process: '蜜处理' }
]
private teas: TeaItem[] = [
{ name: '明前西湖龙井 50g 罐装', type: '绿茶', ferment: 5, price: 158, rating: 96 },
{ name: '金骏眉红茶 250g 礼盒', type: '红茶', ferment: 85, price: 198, rating: 95 },
{ name: '凤凰单丛蜜兰香 100g', type: '乌龙', ferment: 60, price: 128, rating: 94 },
{ name: '云南古树普洱生茶 357g', type: '普洱', ferment: 20, price: 268, rating: 97 },
{ name: '福鼎白毫银针 50g', type: '白茶', ferment: 10, price: 218, rating: 95 },
{ name: '安溪铁观音清香型 250g', type: '乌龙', ferment: 55, price: 118, rating: 92 },
{ name: '正山小种传统松烟香', type: '红茶', ferment: 90, price: 148, rating: 93 },
{ name: '君山银针黄茶 50g', type: '黄茶', ferment: 30, price: 188, rating: 91 },
{ name: '茉莉花茶特级 200g', type: '花茶', ferment: 70, price: 88, rating: 92 },
{ name: '武夷岩茶大红袍 160g', type: '乌龙', ferment: 65, price: 238, rating: 96 }
]
private gears: GearItem[] = [
{ name: 'Hario V60 树脂滤杯 02 号', material: '树脂', capacity: 400, price: 68, stock: 421 },
{ name: ' Kalita 蛋糕杯 155 不锈钢', material: '不锈钢', capacity: 500, price: 158, stock: 187 },
{ name: '聪明杯浸泡式滤杯 350ml', material: 'PP 树脂', capacity: 350, price: 88, stock: 312 },
{ name: '法压壶耐热玻璃 600ml', material: '玻璃', capacity: 600, price: 98, stock: 265 },
{ name: '摩卡壶经典八角 3 杯份', material: '铝合金', capacity: 150, price: 128, stock: 198 },
{ name: '爱乐压便携压滤壶', material: 'PP+硅胶', capacity: 250, price: 218, stock: 156 },
{ name: '手冲鹅颈壶 0.9L 细嘴', material: '不锈钢', capacity: 900, price: 189, stock: 234 },
{ name: '陶瓷手冲套装五件套', material: '陶瓷', capacity: 400, price: 268, stock: 87 },
{ name: '意式咖啡机家用半自动', material: '不锈钢机身', capacity: 1500, price: 1999, stock: 45 },
{ name: '便携电动磨豆机 USB 充电', material: 'ABS+合金', capacity: 30, price: 328, stock: 176 }
]
private shopList: ShopItem[] = [
{ name: '国贸旗舰店', dist: 1.2, score: 4.9, seats: 40, booked: 34 },
{ name: '三里屯店', dist: 2.8, score: 4.8, seats: 56, booked: 30 },
{ name: '望京小街店', dist: 3.5, score: 4.7, seats: 32, booked: 21 },
{ name: '陆家嘴店', dist: 5.1, score: 4.9, seats: 48, booked: 42 },
{ name: '中关村店', dist: 4.2, score: 4.6, seats: 28, booked: 12 },
{ name: '环贸 iapm 店', dist: 2.1, score: 4.8, seats: 36, booked: 25 },
{ name: '万象城店', dist: 6.3, score: 4.7, seats: 44, booked: 28 },
{ name: '太古里店', dist: 3.9, score: 4.9, seats: 52, booked: 48 },
{ name: '滨江天街店', dist: 7.5, score: 4.5, seats: 24, booked: 10 },
{ name: '首钢园店', dist: 9.8, score: 4.6, seats: 30, booked: 16 }
]
private messages: MessageItem[] = [
{ title: '发货通知', content: '您的瑰夏挂耳礼盒已由顺丰揽收,全程冷链', time: '11:02', unread: 2 },
{ title: '订阅提醒', content: '下期云南小粒豆将在周四烘焙,周五发出', time: '10:15', unread: 1 },
{ title: '门店预约', content: '国贸旗舰店手冲体验课已预约成功,请准时到店', time: '09:30', unread: 3 },
{ title: '烘焙出炉', content: '本周三支新豆出炉:花魁 7.0 已上架', time: '昨天', unread: 0 },
{ title: '会员日', content: '每月 18 日会员日,全场咖啡豆 88 折', time: '昨天', unread: 0 },
{ title: '品鉴邀请', content: '您获得 1 张杯测品鉴会入场券,本周六开场', time: '前天', unread: 1 },
{ title: '优惠到账', content: '咖啡节专享券 3 张已放入卡包,满 99 可用', time: '前天', unread: 0 },
{ title: '器具保养', content: '您的磨豆机已到建议清洁周期,查看教程', time: '周三', unread: 0 },
{ title: '茶叶上新', content: '明前龙井头采开售,限量 200 罐', time: '周三', unread: 1 },
{ title: '订单完成', content: '法压壶已签收,晒图可返 10 元京豆', time: '周二', unread: 0 },
{ title: '降价通知', content: '购物车里的聪明杯降价 20 元', time: '周一', unread: 0 },
{ title: '系统公告', content: '咖啡订阅服务升级,可随时暂停配送', time: '上周', unread: 0 }
]
private maxAltitude(): number {
let m: number = 0
for (let i = 0; i < this.beanList.length; i++) {
if (this.beanList[i].altitude > m) {
m = this.beanList[i].altitude
}
}
return m
}
private maxScore(): number {
let m: number = 0
for (let i = 0; i < this.shopList.length; i++) {
if (this.shopList[i].booked > m) {
m = this.shopList[i].booked
}
}
return m
}
private selName(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.name
}
private selPrice(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.price
}
private selOrigin(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.origin
}
private selScore(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.score
}
private selSold(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.sold
}
build() {
Stack() {
Column() {
this.headerBar()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabMarket()
} else if (this.currentTab === 1) {
this.tabBean()
} else if (this.currentTab === 2) {
this.tabTea()
} else if (this.currentTab === 3) {
this.tabGear()
} else if (this.currentTab === 4) {
this.tabShop()
} else if (this.currentTab === 5) {
this.tabMessage()
} else {
this.tabMine()
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Off)
this.bottomBar()
}
.width('100%')
.height('100%')
if (this.showPublish) {
this.modalPublish()
}
if (this.showSubscribe) {
this.modalSubscribe()
}
if (this.showVisit) {
this.modalVisit()
}
if (this.showDelete) {
this.modalDelete()
}
if (this.showDetail) {
this.modalDetail()
}
}
.width('100%')
.height('100%')
.backgroundColor('#FAF6F0')
}
@Builder
headerBar() {
Column() {
Row() {
Column() {
Text('咖啡 · 茗茶')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('京东生活馆')
.fontSize(10)
.fontColor('#D7CCC8')
.margin({ top: 2 })
}
Column() {
Row() {
Text('搜索咖啡豆 / 茶叶 / 手冲器具')
.fontSize(12)
.fontColor('#BCAAA4')
}
.width('100%')
.height(30)
.backgroundColor('#FFFFFF')
.borderRadius(15)
.justifyContent(FlexAlign.Start)
.padding({ left: 14 })
}
.layoutWeight(1)
.margin({ left: 12, right: 12 })
Column() {
Text('消息')
.fontSize(13)
.fontColor('#FFFFFF')
}
.onClick(() => {
this.currentTab = 5
})
}
.width('100%')
.height(54)
.padding({ left: 14, right: 14 })
.alignItems(VerticalAlign.Center)
Row() {
ForEach(['咖啡豆', '挂耳', '意式', '龙井', '普洱', '白茶', '手冲壶'], (k: string) => {
Text(k)
.fontSize(11)
.fontColor('#D7CCC8')
.backgroundColor('rgba(255,255,255,0.12)')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ right: 8 })
})
}
.width('100%')
.padding({ left: 14, bottom: 8 })
.justifyContent(FlexAlign.Start)
}
.width('100%')
.backgroundColor('#5D4037')
}
@Builder
bottomBar() {
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column() {
Column()
.width(index === this.currentTab ? 16 : 6)
.height(3)
.borderRadius(2)
.backgroundColor(index === this.currentTab ? '#FFB300' : 'rgba(255,255,255,0.35)')
Text(tab)
.fontSize(11)
.fontColor(index === this.currentTab ? '#FFB300' : 'rgba(255,255,255,0.6)')
.margin({ top: 5 })
}
.layoutWeight(1)
.height(54)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTab = index
})
})
}
.width('100%')
.backgroundColor('#4E342E')
}
@Builder
tabMarket() {
Column() {
Row() {
Column() {
Text('早春豆单上新')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('埃塞俄比亚 · 瑰夏村直发 · 满159减40')
.fontSize(11)
.fontColor('#D7CCC8')
.margin({ top: 6 })
Text('查看豆单')
.fontSize(12)
.fontColor('#4E342E')
.backgroundColor('#FFB300')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ top: 12 })
}
.layoutWeight(2)
.alignItems(HorizontalAlign.Start)
Column() {
Text('7842')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFB300')
Text('爆款已售')
.fontSize(10)
.fontColor('#D7CCC8')
.margin({ top: 2 })
Text('订阅豆单')
.fontSize(11)
.fontColor('#4E342E')
.backgroundColor('#FFB300')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ top: 8 })
.scale({ x: 1.06, y: 1.06 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.margin({ left: 10 })
.onClick(() => {
this.showSubscribe = true
})
}
.width('100%')
.padding(16)
.backgroundColor('#5D4037')
.borderRadius(12)
.alignItems(VerticalAlign.Center)
Row() {
Column() {
Text('24')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
Text('在售单品')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10 })
Column() {
Text('99')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('最高杯测分')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
Column() {
Text('5 大')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#5D4037')
Text('著名产区')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ top: 10, left: 8 })
Column() {
Text('发布')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('闲置转让')
.fontSize(10)
.fontColor('#FFCC80')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(58)
.backgroundColor('#E65100')
.borderRadius(10)
.margin({ top: 10, left: 8 })
.onClick(() => {
this.showPublish = true
})
}
.width('100%')
Scroll() {
Row() {
ForEach(this.origins, (o: string, idx: number) => {
Text(o)
.fontSize(12)
.fontColor(this.selectedOrigin === idx ? '#FFFFFF' : '#5D4037')
.backgroundColor(this.selectedOrigin === idx ? '#5D4037' : '#FFFFFF')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.margin({ right: 8 })
.onClick(() => {
this.selectedOrigin = idx
})
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
Column() {
ForEach(this.coffees, (c: CoffeeItem) => {
Row() {
Column()
.width(84)
.height(84)
.backgroundColor('#D7CCC8')
.borderRadius(42)
Column() {
Text(c.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(c.origin)
.fontSize(10)
.fontColor('#E65100')
.backgroundColor('#FFF3E0')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(c.roast)
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 6 })
Text('杯测 ' + c.score)
.fontSize(10)
.fontColor('#5D4037')
.fontWeight(FontWeight.Bold)
.margin({ left: 6 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
}
.margin({ top: 6 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(6)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(c.score + '%')
.height(6)
.backgroundColor('#E65100')
.borderRadius(3)
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('¥' + c.price)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('已售' + c.sold)
.fontSize(11)
.fontColor('#999999')
.margin({ left: 8 })
Text('订阅价')
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor('#5D4037')
.borderRadius(10)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ left: 6 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.margin({ top: 8 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.selectedItem = c
this.showDetail = true
})
})
}
.width('100%')
}
.width('100%')
}
@Builder
tabBean() {
Column() {
Column() {
Text('庄园豆海拔对比 (m)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Row() {
ForEach(this.beanList, (b: BeanItem) => {
Column() {
Column()
.width(15)
.height(b.altitude / this.maxAltitude() * 84)
.backgroundColor('#FFB300')
.borderRadius(4)
Text(b.altitude + '')
.fontSize(8)
.fontColor('#D7CCC8')
.margin({ top: 3 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(112)
.justifyContent(FlexAlign.Start)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#4E342E')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
Row() {
Column() {
Text('订阅咖啡豆')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('周期烘焙 · 定时配送')
.fontSize(10)
.fontColor('#FFCC80')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(52)
.backgroundColor('#5D4037')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
.margin({ top: 10 })
.onClick(() => {
this.showSubscribe = true
})
Column() {
Text('发布转让')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('闲置咖啡豆')
.fontSize(10)
.fontColor('#FFCC80')
.margin({ top: 2 })
}
.layoutWeight(1)
.height(52)
.backgroundColor('#E65100')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
.margin({ top: 10, left: 8 })
.onClick(() => {
this.showPublish = true
})
}
.width('100%')
Column() {
ForEach(this.beanList, (b: BeanItem, idx: number) => {
Row() {
Column() {
Text('No.' + (idx + 1))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#E65100' : '#BCAAA4')
}
.width(44)
.height(64)
.backgroundColor('#EFEBE9')
.borderRadius(10)
.justifyContent(FlexAlign.Center)
Column() {
Text(b.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(b.farm)
.fontSize(10)
.fontColor('#5D4037')
.backgroundColor('#EFEBE9')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(b.process)
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 6 })
Text('海拔 ' + b.altitude + 'm')
.fontSize(10)
.fontColor('#999999')
.margin({ left: 6 })
}
.margin({ top: 6 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(5)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(b.altitude / this.maxAltitude() * 100 + '%')
.height(5)
.backgroundColor('#FFB300')
.borderRadius(3)
}
.width('100%')
.margin({ top: 8 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
Column() {
Text('¥' + b.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('200g/袋')
.fontSize(10)
.fontColor('#999999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.selectedItem = this.coffees[idx % this.coffees.length]
this.showDetail = true
})
})
}
.width('100%')
}
.width('100%')
}
@Builder
tabTea() {
Column() {
Column() {
Text('茗茶专区')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#33691E')
Text('明前头采 · 茶园直供 · 冷链锁鲜')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 14, bottom: 14, left: 14 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
.margin({ top: 10 })
Column() {
Text('六大茶类发酵度对比 (%)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row() {
ForEach(this.teas, (t: TeaItem) => {
Column() {
Column()
.width(15)
.height(t.ferment / 100 * 80 + 4)
.backgroundColor('#7CB342')
.borderRadius(4)
Text(t.ferment + '')
.fontSize(9)
.fontColor('#999999')
.margin({ top: 4 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(110)
.justifyContent(FlexAlign.Start)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.teas, (t: TeaItem) => {
Column() {
Row() {
Text(t.type)
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#7CB342')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('发酵 ' + t.ferment + '%')
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Text(t.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 8 })
Row() {
Text('¥' + t.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('好评 ' + t.rating + '%')
.fontSize(10)
.fontColor('#5D4037')
.margin({ left: 6 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 750, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(5)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(t.rating + '%')
.height(5)
.backgroundColor('#7CB342')
.borderRadius(3)
}
.width('100%')
.margin({ top: 6 })
}
.width('48%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
.onClick(() => {
this.showSubscribe = true
})
})
}
.width('100%')
}
.width('100%')
}
@Builder
tabGear() {
Column() {
Column() {
Row() {
Text('器具冲泡实验室')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('容量对比 (ml)')
.fontSize(11)
.fontColor('#D7CCC8')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Row() {
ForEach(this.gears, (g: GearItem) => {
Column() {
Column()
.width(15)
.height(g.capacity / 1500 * 80 + 3)
.backgroundColor('#FF7043')
.borderRadius(4)
Text(g.capacity + '')
.fontSize(8)
.fontColor('#D7CCC8')
.margin({ top: 3 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(110)
.justifyContent(FlexAlign.Start)
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.backgroundColor('#6D4C41')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
Scroll() {
Row() {
ForEach(this.gears, (g: GearItem) => {
Column() {
Column()
.width(96)
.height(60)
.backgroundColor('#D7CCC8')
.borderRadius(10)
Text(g.material)
.fontSize(10)
.fontColor('#6D4C41')
.backgroundColor('#EFEBE9')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ top: 6 })
Text('¥' + g.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
.margin({ top: 4 })
}
.width(96)
.padding(8)
.backgroundColor('#FFFFFF')
.borderRadius(10)
.margin({ right: 8 })
.alignItems(HorizontalAlign.Center)
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
Column() {
ForEach(this.gears, (g: GearItem) => {
Row() {
Column()
.width(58)
.height(58)
.backgroundColor('#EFEBE9')
.borderRadius(10)
Column() {
Text(g.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(g.capacity + 'ml')
.fontSize(10)
.fontColor('#6D4C41')
.backgroundColor('#EFEBE9')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text('库存 ' + g.stock)
.fontSize(10)
.fontColor('#999999')
.margin({ left: 8 })
if (g.stock < 100) {
Text('少量')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#E65100')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 6 })
.scale({ x: 1.08, y: 1.08 })
.animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
}
}
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
Column() {
Text('¥' + g.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E65100')
Text('加入购物车')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#6D4C41')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.showDelete = true
})
})
}
.width('100%')
}
.width('100%')
}
@Builder
tabShop() {
Column() {
Column() {
Row() {
Column() {
Text('门店手冲体验')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('大师现场冲煮 · 免费品鉴三杯')
.fontSize(11)
.fontColor('#FFCC80')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('立即预约')
.fontSize(12)
.fontColor('#4E342E')
.backgroundColor('#FFB300')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
.onClick(() => {
this.showVisit = true
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
.width('100%')
.padding(16)
.backgroundColor('#5D4037')
.borderRadius(12)
Column() {
Text('到店预约热度 (本周)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Row() {
ForEach(this.shopList, (s: ShopItem) => {
Column() {
Column()
.width(15)
.height(s.booked / this.maxScore() * 80)
.backgroundColor('#8D6E63')
.borderRadius(4)
Text(s.booked + '')
.fontSize(9)
.fontColor('#999999')
.margin({ top: 4 })
}
.margin({ right: 9 })
.alignItems(HorizontalAlign.Center)
})
}
.width('100%')
.height(110)
.justifyContent(FlexAlign.Start)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
Column() {
ForEach(this.shopList, (s: ShopItem) => {
Row() {
Column()
.width(50)
.height(50)
.backgroundColor('#D7CCC8')
.borderRadius(10)
Column() {
Row() {
Text(s.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text(s.score + ' 分')
.fontSize(11)
.fontColor('#E65100')
.fontWeight(FontWeight.Bold)
.margin({ left: 8 })
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
}
Text('距离 ' + s.dist + 'km · ' + s.seats + ' 席位')
.fontSize(11)
.fontColor('#999999')
.margin({ top: 4 })
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(6)
.backgroundColor('#F0F0F0')
.borderRadius(3)
Column()
.width(s.booked / s.seats * 100 + '%')
.height(6)
.backgroundColor(s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342')
.borderRadius(3)
}
.width('100%')
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
Column() {
Text(s.booked + '/' + s.seats)
.fontSize(11)
.fontColor('#999999')
Text('预约')
.fontSize(11)
.fontColor('#FFFFFF')
.backgroundColor('#5D4037')
.borderRadius(10)
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(10)
.margin({ top: 10 })
.onClick(() => {
this.showVisit = true
})
})
}
.width('100%')
}
.width('100%')
}
@Builder
tabMessage() {
Column() {
Column() {
Row() {
Text('消息中心')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Text('8 条新动态')
.fontSize(11)
.fontColor('#E65100')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('100%')
.padding({ top: 14, bottom: 14, left: 14, right: 14 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10 })
Column() {
ForEach(this.messages, (m: MessageItem) => {
Row() {
Column()
.width(46)
.height(46)
.backgroundColor('#EFEBE9')
.borderRadius(23)
Column() {
Row() {
Text(m.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
if (m.unread > 0) {
Text(m.unread + '')
.fontSize(10)
.fontColor('#FFFFFF')
.backgroundColor('#E65100')
.borderRadius(9)
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.margin({ left: 6 })
.scale({ x: 1.1, y: 1.1 })
.animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
}
Text(m.time)
.fontSize(10)
.fontColor('#BBBBBB')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
Text(m.content)
.fontSize(12)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 10 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(12)
.margin({ top: 10 })
.onClick(() => {
this.showDelete = true
})
})
}
.width('100%')
}
.width('100%')
}
@Builder
tabMine() {
Column() {
Column() {
Row() {
Column()
.width(60)
.height(60)
.backgroundColor('#D7CCC8')
.borderRadius(30)
Column() {
Text('手冲玩家_Leo')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('咖啡品味等级:银杯杯测师')
.fontSize(11)
.fontColor('#FFCC80')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 12 })
}
---
## 总结

本文深入剖析了一个基于HarmonyOS 6.1.1声明式UI框架的咖啡茗茶生活馆应用的完整源码实现。从六个数据接口定义到状态管理机制,从七个功能Tab的详细构建到五个模态弹窗的交互逻辑,全面覆盖了ArkTS声明式开发在咖啡茗茶垂直电商领域的核心技术实践。应用通过24款咖啡商品、12款庄园咖啡豆、10款茶叶、10款冲泡器具和10家门店的数据集,构建了一个覆盖咖啡购买、豆单订阅、茶叶选购、器具冲泡和门店体验全链路的服务平台。
在技术实现层面,该应用展示了ArkUI声明式框架的丰富表达能力。海拔柱状图、发酵度柱状图、容量柱状图和预约热度柱状图均通过Column组件的动态height计算实现,无需引入任何第三方图表库。门店预约率进度条通过三元表达式`s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342'`实现了基于阈值的动态颜色切换,当预约率超过80%时自动从绿色切换为橙色,这种数据驱动的视觉反馈是本应用的一大交互创新。庄园咖啡豆列表的序号排名设计通过`idx < 3`条件判断为前三名分配橙色编号、其余使用浅棕色编号,在视觉上突出了品质排名。取模索引`coffees[idx % coffees.length]`的运用确保了咖啡豆到咖啡商品数据的安全映射,防止数组越界。
门店体验预约通过"免费"标签的脉冲动画强调服务的零成本属性,配合体验内容文本展示有效提升了预约转化率。弹窗级联切换通过顺序修改两个状态变量实现场景无缝过渡。颜色编码设计贯穿整个应用:棕色系用于咖啡品类、绿色用于茶叶和烘焙度标签、琥珀色用于评分和排名、橙色用于价格和行动按钮,形成了一套完整的视觉语言系统。整体而言,该应用是学习HarmonyOS ArkTS声明式开发在垂直电商场景下应用的一个综合性范例,涵盖了数据建模、状态管理、布局设计、数据可视化、动画交互、动态颜色和多维度业务流程编排等核心技术领域。
更多推荐



所有评论(0)