基于HarmonyOS ArkTS API 24鸿蒙高效列表渲染教程:运用ForEach语法遍历数据源,实现长列表高效渲染与按需加载优化
本文基于 HarmonyOS NEXT(API 24)ArkTS 声明式 UI 框架,完整拆解一个奶茶主题社交应用「QQ奶茶·茶友圈」的源码实现。应用以奶茶棕
#8A5A3B为主色、珍珠黑#3B2B26为深色、蜜桃粉#F5A3B7为强调色,构建了底部 4 Tab(奶茶/打卡/配方/我的)+ 顶部 6 Tab(新品/经典/果茶/咖啡/联名/隐藏)的双层导航体系,配套 14 个奶茶杯粒子的水平向右飘动动画(所有应用中唯一水平飘动粒子)、经典 Tab 周销量柱状图(周六蜜桃粉高亮)、果茶 Tab 配方进化时间轴、联名 Tab IP 对决卡 + 侧栏统计、隐藏 Tab 标签云 + 暗号玩法、4 个模态弹窗(发布打卡新增/创建配方新增/删除打卡删除/奶茶详情查询),覆盖从奶茶发现、打卡分享、配方共创到个人茶友成就的完整奶茶社交闭环。

一、引言:今天也要快乐吨吨吨
奶茶已经成为年轻人的日常续命水——上班路上一杯生椰拿铁唤醒大脑,下午三点一杯多肉葡萄治愈疲惫,周末和搭子排队四十分钟就为一杯桂花酒酿奶茶。「QQ奶茶·茶友圈」正是为这群奶茶爱好者打造的移动端社区应用:用户可以按奶茶类型(新品/经典/果茶/咖啡/联名/隐藏)发现好喝的奶茶、在打卡页分享今日奶茶动态、在配方页共创自研点单口诀、在个人中心追踪喝过杯数与年度奶茶账单。
应用的视觉语言高度统一:奶茶棕代表奶茶的醇厚与温暖,珍珠黑代表珍珠的深邃与 Q 弹,蜜桃粉代表奶茶的甜美与少女心。整个应用采用甜美奶茶风——页面底色 #FFF6EC 奶油浅橙、卡片纯白、头部三色渐变 #8A5A3B→#B07B55→#D9A876(从深奶茶棕到浅焦糖金,模拟奶茶从杯底到杯面的渐变层次)。粒子系统选用奶茶杯 🧋、饮料杯 🥤、团子 🍡、闪光 ✨、纸杯蛋糕 🧁 五种图标循环,以所有应用中最快的速度(speed 0.6~2.0,setInterval 80ms)水平向右飘动——这一设计巧妙模拟了奶茶杯在传送带上向右移动、奶茶店出杯口一杯接一杯送出的场景,与垂直飘动的常规粒子形成鲜明反差,是所有应用中唯一采用水平飘动粒子的应用。
本文将从架构概览、色彩系统、数据模型、工具函数与筛选函数、粒子动画系统、双层导航、6 大奶茶差异化布局、打卡/配方/我的三大 Tab、4 个模态弹窗、特性对比、完整代码、开发环境搭建等维度,对源码 1096.ets 进行逐模块深度解析。
二、架构概览:单文件声明式组件 + 状态驱动
整个应用采用 ArkTS 声明式 UI 范式,全部逻辑收敛在一个 @Entry @Component struct Index 主组件中,通过 @State 装饰器驱动视图刷新。架构上可分为以下几层:
- 常量层:
PALETTE配色对象、BOTTOM_TABS/TOP_TABS导航配置、TEAS(18 款奶茶)/CHECKS(8 条打卡)/RECIPES(5 个配方)/SHOPS(6 家门店)/REVIEWS(3 条点评)五大业务数据数组、SALES_LABELS/SALES_BARS周销量柱状图数据、MOODS/SUGAR_LEVELS/TEA_BASES/ADDON_OPTIONS/LEVEL_OPTIONS弹框选项数组。 - 接口层:
ColorPalette/TabItem/ParticleItem/TeaItem/CheckItem/RecipeItem/ShopItem/TeaReview共 8 个 interface,强类型约束所有数据结构。 - 工具函数层:
catColor(奶茶类型 6 色映射)、catLabel(奶茶类型 6 档文案)、heatLabel(热量 3 档文案)、sugarColor(糖度 3 色映射)、salesBarHeight(周销量柱状图高度换算)、badgeCount(打卡点赞 3 档徽章),共 6 个工具函数(所有应用中工具函数最多的)。 - 筛选函数层:
newLeftTeas/newRightTeas(新品左右列)/classicTeas/fruitTeas/coffeeTeas/collabTeas/hiddenTeas/hotTeas,共 9 个筛选函数(所有应用中筛选函数最多的),为 6 大顶部 Tab 和热销榜提供数据来源。 - 组件层:主组件
Index内部通过@Builder装饰器定义了头部、顶部 Tab、底部 Tab、6 个奶茶内容构建器、打卡/配方/我的内容构建器、4 个弹窗构建器,共 19 个@Builder方法。 - 状态层:19 个
@State变量 + 1 个private timerId,覆盖当前 Tab、弹窗显隐、表单数据(含pickedAdds布尔数组实现多选)、粒子数组、定时器 ID。 - 生命周期层:
aboutToAppear中构建粒子并启动setInterval80ms 定时器(所有应用中最快);aboutToDisappear中clearInterval释放资源。

主组件的 build() 方法以 Stack 为根容器,底层是 Column 布局的主界面(头部 + 滚动内容区 + 底部 Tab),上层叠加 ForEach 渲染的粒子层(水平向右飘动),最上层是 4 个条件渲染的模态弹窗遮罩。这种 Stack 分层结构保证了粒子始终浮在内容之上、弹窗始终浮在最顶层。
三、色彩系统:奶茶棕主色 + 珍珠黑深色 + 蜜桃粉强调 + 甜美奶茶风
应用的配色通过 ColorPalette 接口约束,由 PALETTE 常量统一提供,共 10 个色值:
| 色值名 | HEX | 用途 |
|---|---|---|
bg |
#FFF6EC |
奶油浅橙页面底色,甜美奶茶风核心 |
card |
#FFFFFF |
纯白卡片背景 |
brown |
#8A5A3B |
奶茶棕主色,按钮、标签、激活文字、柱状图 |
deep |
#3B2B26 |
珍珠黑深色,主文字、隐藏标签 |
pink |
#F5A3B7 |
蜜桃粉强调色,购物车角标、顶部下划线、柱状图高亮 |
cream |
#FFE8D6 |
奶油色,标签背景、弹框头、按钮 |
gold |
#E8B04B |
焦糖金,新品类型色 |
sub |
#9C8578 |
奶茶灰次级文字 |
line |
#F0E2D6 |
分割线与边框,浅奶茶 |
除了这 10 个基础色,应用还在多个场景中引入了辅助色以丰富视觉层次:
- 奶茶类型映射:
catColor函数为 6 类奶茶分配专属色——新品#E8B04B焦糖金、经典#8A5A3B奶茶棕、果茶#F0788A草莓粉、咖啡#6B4F3A深咖啡棕、联名#B76E79玫瑰棕、隐藏#3B2B26珍珠黑。这一函数在各 Tab 中广泛使用,用颜色直观区分奶茶类型。 - 糖度映射:
sugarColor函数为糖度分配 3 色——无糖/微糖/三分甜#5FA77A草绿(健康)、五分甜#E8B04B焦糖金(适中)、七分甜/全糖#E86A5B珊瑚红(高糖警告)。这一函数在果茶 Tab 糖度标签和奶茶详情建议糖度中使用,用颜色直观表达糖度健康度。 - 热量映射:
heatLabel函数为热量分配 3 档文案——<250「轻负担」、<350「小放纵」、其他「快乐核弹」。 - 打卡徽章映射:
badgeCount函数为点赞数分配 3 档徽章——>=150「🔥爆赞」、>=80「👍热评」、其他「🌱新人帖」。 - 危险/删除:
#E86A5B珊瑚红,用于删除按钮、确认删除按钮、价格、高糖警告、到手价。 - 成功/健康:
#5FA77A草绿,用于低糖标签。

渐变的运用是本应用色彩表达的重点:头部使用 linearGradient angle:135 colors:[[#8A5A3B,0],[#B07B55,0.6],[#D9A876,1]] 三色渐变(从深奶茶棕到中奶茶棕到浅焦糖金,模拟奶茶从杯底到杯面的渐变层次,是所有应用中唯一三色渐变头部);新品 Tab banner 使用奶茶棕渐变 #8A5A3B→#C98A5B;发布打卡弹窗头使用粉棕渐变 #B76E79→#8A5A3B(从玫瑰棕到奶茶棕);奶茶详情弹窗头使用棕渐变 #8A5A3B→#C98A5B;我的 Tab 个人卡使用棕到玫红渐变 #8A5A3B→#B76E79。创建配方弹窗头使用纯奶油 #FFE8D6 底(深字,是所有弹窗中唯一用浅色头的),与其他渐变头形成鲜明对比。不同弹窗通过头色快速区分交互类型。
四、数据模型设计:8 个 interface 强类型约束
应用定义了 8 个 interface,覆盖配色、导航、粒子、奶茶、打卡、配方、门店、点评八大领域:
4.1 ColorPalette、TabItem 与 ParticleItem
interface ColorPalette {
bg: string; card: string; brown: string; deep: string;
pink: string; cream: string; gold: string; sub: string; line: string;
}
interface TabItem {
label: string;
icon: string;
}
interface ParticleItem {
x: number;
emoji: string;
size: number;
speed: number;
opacity: number;
}
ParticleItem 与其他应用不同——没有 id 和 y 字段,只有 x(水平位置,单位 px)、emoji、size、speed、opacity。y 坐标在渲染时通过 80 + (i % 5) * 118 计算(5 行分布),x 用 px 而非百分比,这是因为粒子采用水平向右飘动,x 坐标需要精确到像素才能控制飘动速度和边界。
4.2 TeaItem 奶茶模型(核心)
interface TeaItem {
id: number; name: string; eng: string; cat: string;
price: number; heat: number; sugar: string; rating: number;
starStr: string; shop: string; tag: string; desc: string;
sales: number; emoji: string;
}
奶茶模型是应用最复杂的数据结构,15 个字段覆盖了奶茶的完整信息:id 唯一标识、name 奶茶名、eng 英文名、cat 类型(new/classic/fruit/coffee/collab/hidden)、price 价格、heat 热量(kcal)、sugar 建议糖度、rating 评分、starStr 星号字符串、shop 门店、tag 标签(秋日限定/镇店招牌/断货王等)、desc 描述、sales 周销量、emoji 图标。
eng、heat、sugar、sales 是本应用数据模型的特色字段:eng 记录英文名增加专业感,heat 记录热量(kcal)配合 heatLabel 函数表达健康度,sugar 记录建议糖度配合 sugarColor 函数表达糖度健康度,sales 记录周销量配合周销量柱状图和热销榜展示。
TEAS 常量预置了 18 款奶茶,从桂花酒酿奶茶/冻冻青提恋/芝士莓莓果园(3 款新品),到黑糖珍珠鲜奶/茉莉奶绿/烤奶三分糖(3 款经典),到杨枝甘露/多肉葡萄/满杯百香果(3 款果茶),到生椰拿铁厚乳/燕麦Dirty/焦糖玛奇朵冰(3 款咖啡),到联名草莓熊奶昔/联名库洛米紫苏饮/联名玉桂狗云朵顶(3 款联名),到隐藏咸蛋黄波波/隐藏海盐芝士抹茶/隐藏豆腐脑奶茶(3 款隐藏),覆盖 6 大类型,每款奶茶的 eng/price/heat/sugar/rating/shop/tag/desc/sales/emoji 都精心设计以匹配奶茶特色。
4.3 CheckItem 打卡模型、RecipeItem 配方模型、ShopItem 门店模型与 TeaReview 点评模型
interface CheckItem {
id: number; user: string; avatar: string; tea: string;
shop: string; mood: string; date: string; likes: number; content: string;
}
interface RecipeItem {
id: number; name: string; base: string; addons: string;
sweetness: string; level: string; author: string; likes: number;
emoji: string; steps: string[];
}
interface ShopItem {
id: number; name: string; area: string; distance: string;
avg: number; queue: string;
}
interface TeaReview {
id: number; user: string; tea: string; starStr: string; content: string;
}
CheckItem 10 字段记录打卡动态,CHECKS 预置 8 条(奶茶少女阿茶/全糖去冰战士/半糖主义小K/打工不喝咖啡/隐藏菜单猎人/紫薯芋泥脑袋/三天不喝会瘦/低卡求生指南),mood 字段记录此刻心情(幸福到冒泡/续命成功/酸甜上头/勇者无畏/负罪感清零),是奶茶社区打卡的特色字段。RecipeItem 10 字段记录自研配方,RECIPES 预置 5 个(平价版杨枝甘露/藏起来的杨梅季/厚乳提拉米苏杯/芋泥啵啵杀手/隐藏菜单海盐提神),base 记录茶底、addons 记录加料、steps 是字符串数组记录步骤(带圆形序号渲染),是配方页的核心数据。ShopItem 6 字段记录门店信息,SHOPS 预置 6 家(茶言观舍江汉路店/鹿角巷江汉路店/沪上阿姨街道口店/喜小茶光谷店/伏见桃山江汉路店/益禾堂司门口店),distance 记录距离、queue 记录排队时间,是常去门店的核心字段。TeaReview 5 字段记录点评,starStr 用 Emoji 星号字符串直接渲染,REVIEWS 预置 3 条(评价用户名「奶茶资深评委」「热饮爱好者联盟」「珍珠口感鉴定师」充满奶茶圈梗文化)。
五、工具函数与筛选函数:6 工具 + 9 筛选驱动视觉映射与数据分发
应用定义了 6 个工具函数和 9 个筛选函数(所有应用中工具函数和筛选函数最多的),工具函数负责将数据值映射为视觉属性,筛选函数负责为 6 大顶部 Tab 和热销榜提供数据来源。
5.1 六个工具函数
function catColor(cat: string): string {
if (cat === 'new') return '#E8B04B'
if (cat === 'classic') return '#8A5A3B'
if (cat === 'fruit') return '#F0788A'
if (cat === 'coffee') return '#6B4F3A'
if (cat === 'collab') return '#B76E79'
return '#3B2B26'
}
function catLabel(cat: string): string {
if (cat === 'new') return '本周新品'
if (cat === 'classic') return '经典回归'
if (cat === 'fruit') return '鲜果现切'
if (cat === 'coffee') return '咖啡因站'
if (cat === 'collab') return 'IP联名'
return '隐藏菜单'
}
function heatLabel(heat: number): string {
if (heat < 250) return '轻负担'
if (heat < 350) return '小放纵'
return '快乐核弹'
}
function sugarColor(sugar: string): string {
if (sugar === '无糖' || sugar === '微糖' || sugar === '三分甜') return '#5FA77A'
if (sugar === '五分甜') return '#E8B04B'
return '#E86A5B'
}
function salesBarHeight(v: number): string {
return Math.round(v / 132 * 96).toString() + 'vp'
}
function badgeCount(likes: number): string {
if (likes >= 150) return '🔥爆赞'
if (likes >= 80) return '👍热评'
return '🌱新人帖'
}
catColor:将奶茶类型映射为 6 色(新品焦糖金/经典奶茶棕/果茶草莓粉/咖啡深咖啡棕/联名玫瑰棕/隐藏珍珠黑),是本应用最具特色的工具函数,用颜色直观区分奶茶类型。catLabel:将奶茶类型映射为 6 档文案(本周新品/经典回归/鲜果现切/咖啡因站/IP联名/隐藏菜单)。heatLabel:将热量映射为 3 档文案(轻负担/小放纵/快乐核弹),「快乐核弹」是奶茶圈梗,形容高热量奶茶的快乐与罪恶并存。sugarColor:将糖度映射为 3 色(低糖草绿/中糖焦糖金/高糖珊瑚红),用颜色直观表达糖度健康度,是本应用的健康化设计。salesBarHeight:将周销量换算为柱状图高度 vp(v / 132 * 96,以周六最高销量 132 为基准归一化到 96vp),在经典 Tab 的周销量柱状图中使用。badgeCount:将打卡点赞数映射为 3 档徽章(🔥爆赞/👍热评/🌱新人帖),在打卡动态列表中使用。
5.2 九个筛选函数
function newLeftTeas(): TeaItem[] { return [TEAS[0], TEAS[2]] }
function newRightTeas(): TeaItem[] { return [TEAS[1]] }
function classicTeas(): TeaItem[] { return [TEAS[3], TEAS[4], TEAS[5]] }
function fruitTeas(): TeaItem[] { return [TEAS[6], TEAS[7], TEAS[8]] }
function coffeeTeas(): TeaItem[] { return [TEAS[9], TEAS[10], TEAS[11]] }
function collabTeas(): TeaItem[] { return [TEAS[12], TEAS[13], TEAS[14]] }
function hiddenTeas(): TeaItem[] { return [TEAS[15], TEAS[16], TEAS[17]] }
function hotTeas(): TeaItem[] { return [TEAS[3], TEAS[9], TEAS[7], TEAS[4], TEAS[12]] }
9 个筛选函数中,newLeftTeas 和 newRightTeas 是新品 Tab 双列瀑布流的左右列数据(左列 2 款、右列 1 款,形成高度错落的瀑布流效果),classicTeas/fruitTeas/coffeeTeas/collabTeas/hiddenTeas 对应经典/果茶/咖啡/联名/隐藏五个顶部 Tab,hotTeas 是热销 TOP5 榜单数据。与其他应用使用 filter() 不同,本应用的筛选函数直接通过数组索引取数据(TEAS[0]/TEAS[2] 等),因为 TEAS 数组已经按类型顺序排列(0-2 新品、3-5 经典、6-8 果茶、9-11 咖啡、12-14 联名、15-17 隐藏),直接索引比 filter 更高效。这种「6 工具函数 + 9 筛选函数」的分层设计,让数据映射与数据分发各司其职,模板中只需调用对应函数即可获得正确数据,保持模板简洁。
六、粒子动画系统:14 粒子最快速度水平向右飘动(奶茶杯传送带意象)
粒子系统是本应用的动态灵魂,选用奶茶杯 🧋、饮料杯 🥤、团子 🍡、闪光 ✨、纸杯蛋糕 🧁 五种图标循环,以所有应用中最快的速度水平向右飘动,模拟奶茶杯在传送带上向右移动、奶茶店出杯口一杯接一杯送出的场景。
6.1 粒子构建:14 粒子 5 图标循环(无 id/y 字段)
buildParticles(): void {
const emojis: string[] = ['🧋', '🥤', '🍡', '✨', '🧁']
const list: ParticleItem[] = []
for (let i = 0; i < 14; i++) {
list.push({
x: Math.random() * 340,
emoji: emojis[i % emojis.length],
size: 12 + Math.random() * 10,
speed: 0.6 + Math.random() * 1.4,
opacity: 0.25 + Math.random() * 0.45
})
}
this.particles = list
}
14 个粒子通过 Math.random() 生成随机分布的 x 坐标(0~340px),图标在 🧋🥤🍡✨🧁 五种中循环(奶茶杯/饮料杯/团子/闪光/纸杯蛋糕),size 范围 12~22,opacity 范围 0.25~0.70,speed 范围 0.6~2.0(每个粒子独立速度,是所有应用中最快的粒子速度,模拟奶茶店出杯的快节奏)。与其他应用不同,ParticleItem 没有 id 和 y 字段——y 坐标在渲染时通过 80 + (i % 5) * 118 计算(5 行分布,每行间隔 118px),x 用 px 而非百分比,这是因为粒子采用水平向右飘动,x 坐标需要精确到像素才能控制飘动速度和边界。
6.2 粒子驱动:水平向右飘动 + 左侧循环
driftParticles(): void {
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].x += this.particles[i].speed
if (this.particles[i].x > 360) {
this.particles[i].x = -20
}
}
this.particles = this.particles.slice()
}
关键点:每帧将每个粒子的 x 坐标加上自身 speed(水平向右飘动),当 x > 360(飘出右侧)时重置为 -20(从左侧重新进入),实现无限循环。y 坐标保持不变(5 行固定分布)。这一方向设计模拟了奶茶杯在传送带上向右移动、奶茶店出杯口一杯接一杯送出的场景,与垂直飘动的常规粒子形成鲜明反差,是所有应用中唯一采用水平飘动粒子的应用。配合最快的速度(0.6~2.0)和最快的刷新频率(80ms),营造出奶茶店出杯的快节奏与奶茶爱好者的快乐氛围。
this.particles = this.particles.slice() 是关键——通过 slice() 创建新数组引用,触发 ArkTS 的状态更新和视图重渲染。如果直接修改数组元素而不改变引用,@State 不会触发更新。
6.3 生命周期与渲染
aboutToAppear(): void {
this.buildParticles()
this.timerId = setInterval(() => {
this.driftParticles()
}, 80)
}
aboutToDisappear(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
aboutToAppear 中先构建粒子数组,再启动 setInterval 80ms 定时器(所有应用中最快的刷新频率,约 12.5fps,配合最快的粒子速度,模拟奶茶店出杯的快节奏),将定时器 ID 存入 private timerId。aboutToDisappear 中清除定时器并重置为 -1。
渲染层在 Stack 中通过 ForEach 遍历粒子,position 使用 { x: p.x, y: 80 + (i % 5) * 118 }(x 用 px,y 用 5 行分布计算),ForEach 的 key 函数使用 'pt' + i.toString()(用索引 i 而非粒子 id,因为 ParticleItem 没有 id 字段)。
七、双层导航架构:底部 4 Tab + 顶部 6 Tab
应用采用双层 Tab 导航,底部 Tab 切换大模块、顶部 Tab 在「奶茶」模块内切换奶茶类型分类。
7.1 头部 headerBuilder:三色渐变 + 搜索条 + 购物车角标
头部使用 linearGradient angle:135 colors:[[#8A5A3B,0],[#B07B55,0.6],[#D9A876,1]] 三色渐变(从深奶茶棕到中奶茶棕到浅焦糖金,模拟奶茶从杯底到杯面的渐变层次,是所有应用中唯一三色渐变头部),底部圆角 18。头部包含:
- 左侧:🧋 茶友圈 22sp 粗体白字 + 「今天也要快乐吨吨吨」11sp 奶油色字。
- 右侧:🔍 搜索 20sp + 💌 消息 20sp + 🛒 购物车 20sp 带「12」角标(蜜桃粉
#F5A3B7底白字 9sp,margin top -6 形成角标效果)。 - 搜索条:🔎 搜索:芋泥 / 生椰 / 隐藏菜单 12sp 奶茶灰字 + 「茶友榜」按钮(
#A96F4C深奶茶棕底奶油色字圆角 10)。
头部是所有应用中最复杂的头部之一——三色渐变 + 三图标 + 购物车角标 + 搜索条 + 茶友榜按钮,完整模拟了电商/奶茶 App 的头部交互体验。
7.2 顶部 Tab topTabsBuilder(仅奶茶 Tab 显示,6 Tab 等宽 + 双下划线)
6 个 Tab 等宽排列(新品✨/经典🏆/果茶🍇/咖啡☕/联名🧸/隐藏🔐),每个 Tab layoutWeight(1) 等宽。每个 Tab 显示图标 17sp + 文字 12sp。激活态为 #8A5A3B 奶茶棕字粗体 + 蜜桃粉 #F5A3B7 圆角下划线(Divider width16 strokeWidth3 borderRadius2),非激活态为 #9C8578 奶茶灰字 + #FFF6EC 奶油色圆角下划线(非激活也有下划线,是本应用顶部 Tab 的特色细节,形成双下划线对比效果)。顶部 Tab 使用纯白底圆角 14 margin top 10。点击切换 currentTopTab。
7.3 底部 Tab bottomTabsBuilder(纯白底 + 顶部圆角)
4 个 Tab(奶茶🧋/打卡📸/配方📒/我的🍫)等宽排列,激活态为 #8A5A3B 奶茶棕字粗体,非激活态为 #9C8578 奶茶灰字。底部 Tab 使用纯白底,顶部圆角 18(borderRadius: { topLeft: 18, topRight: 18 }),与头部的底部圆角呼应,形成圆润的奶茶杯视觉隐喻。点击切换 currentBottomTab。
八、奶茶 Tab:6 大类型差异化布局
奶茶 Tab 是应用最核心的模块,6 个顶部 Tab 对应 6 种完全不同的布局风格,从新品的双列瀑布流、经典的横滑大卡+周销量柱状图+热销榜、果茶的配方进化时间轴、咖啡的三列格子+打工人心得、联名的 IP 对决卡+侧栏统计、到隐藏的标签云+紧凑列表+暗号玩法,每种奶茶类型都有专属的视觉语言。
8.1 新品 newContent:秋日新品季 banner + 双列瀑布流
新品 Tab 由三部分组成:
- 秋日新品季 banner:
angle:120奶茶棕渐变#8A5A3B→#C98A5B,「🍂 秋日新品季」18sp 粗体白字 + 「桂花酒酿系列限时上线 · 第二杯半价」12sp 奶油色字 + 两个标签按钮(🧋 酒酿奶茶 / 🍇 冻冻青提,奶油色底奶茶棕字圆角 12)。 - 本周鲜上架标题:「✨ 本周鲜上架」15sp 粗体珍珠黑字。
- 双列瀑布流:
Row左右两列,左列newLeftTeas()2 款(桂花酒酿奶茶/芝士莓莓果园)纯白底卡,右列newRightTeas()1 款(冻冻青提恋)#FFF0F3蜜桃粉底卡。左右列卡片高度错落(左列 emoji margin top 16、右列 margin top 24;左列 desc maxLines3、右列 maxLines4;左列底部 margin 14、右列 20),形成真正的瀑布流效果。每张卡片包含:emoji 34sp + 奶茶名 14sp 粗体 + 英文名 9sp 灰字 + desc 11sp 灰字(maxLines3/4)+ 价格 15sp 粗体珊瑚红 + 标签(tag 奶油色底 / starStr 星号)。点击打开奶茶详情弹窗。
双列瀑布流是新品 Tab 的核心布局,通过左右列卡片高度错落模拟电商 App 的商品瀑布流,适合新品奶茶的「上新展示」。
8.2 经典 classicContent:横滑大卡 + 周销量柱状图 + 热销 TOP5
经典 Tab 由四部分组成:
- 标题:「🏆 经典永流传」15sp 粗体珍珠黑字。
- 横滑大卡:
Scroll horizontal横向滚动,classicTeas()筛选出 3 款经典奶茶(黑糖珍珠鲜奶/茉莉奶绿/烤奶三分糖),每个渲染为 width:150 的#8A5A3B奶茶棕底圆角 16 卡片,包含:emoji 40sp + 奶茶名 15sp 粗体白字 + 糖度·热量 10sp 奶油色字 + 价格 16sp 粗体奶油色字 + starStr 星号。横滑大卡适合经典奶茶的「招牌展示」,奶茶棕底白字营造经典醇厚感。 - 周销量柱状图:标题「📈 本圈茶友周点单量(万杯)」,下方是纯白底圆角 14 卡片,内含
Row布局的 7 根柱子(SALES_BARS = [62,48,55,43,88,132,118],对应周一~周日)。每根柱子通过salesBarHeight(v)计算高度(v / 132 * 96,以周六最高 132 为基准),使用高亮双色——i === 5(周六)蜜桃粉#F5A3B7,其他日期奶茶棕#C98A5B,柱子顶部标注销量数字(奶茶棕字),底部标注星期。7 根柱子等宽排列,周六 132 最高形成「周六是奶茶快乐日」可视化,底部文字「周六是奶茶快乐日,点单量是周四的3倍」。 - 热销 TOP5:标题「🔥 热销TOP5」,
hotTeas()筛选出 5 款热销奶茶(黑糖珍珠鲜奶/生椰拿铁厚乳/多肉葡萄/茉莉奶绿/联名草莓熊奶昔),每行左侧序号(前 3 名#E86A5B珊瑚红粗体,其他灰字)+ emoji + 奶茶名 + 销量·门店,右侧价格。点击打开奶茶详情弹窗。
经典 Tab 通过横滑大卡 + 周销量柱状图 + 热销榜三段式布局,完整展示经典奶茶的招牌地位和销售数据,是数据可视化最丰富的 Tab。
8.3 果茶 fruitContent:配方进化时间轴
果茶 Tab 由标题 + 配方进化时间轴组成:
- 标题:「🍇 鲜果现切·配方进化史」15sp 粗体珍珠黑字。
- 配方进化时间轴:
fruitTeas()筛选出 3 款果茶(杨枝甘露/多肉葡萄/满杯百香果),每行左侧是时间轴节点(emoji 20sp +#F5A3B7蜜桃粉竖线layoutWeight(1)连接,前 2 款有竖线,最后一款没有),右侧是内容卡(第 2 款多肉葡萄#FFF0F3蜜桃粉底高亮,其他纯白底),包含:奶茶名 + 价格(右对齐)、desc 11sp 灰字、标签行(第 N 代配方#F0788A草莓粉字蜜桃粉底 + 糖度sugarColor映射色 + 热量·heatLabel)。
配方进化时间轴是果茶 Tab 的核心布局,通过左侧 emoji + 蜜桃粉竖线连接,模拟果茶配方从第 1 代到第 3 代的进化历程,第 2 代蜜桃粉底高亮表示当前最受欢迎的配方,适合果茶的「鲜果现切」主题。

8.4 咖啡 coffeeContent:三列格子 + 打工人咖啡心得
咖啡 Tab 由三部分组成:
- 标题:「☕ 咖啡因补给站」15sp 粗体珍珠黑字。
- 三列格子:
Flex wrap justifyContent:SpaceBetween三列布局,coffeeTeas()筛选出 3 款咖啡(生椰拿铁厚乳/燕麦Dirty/焦糖玛奇朵冰),每个格子 width:31%,纯白底圆角 12,包含:emoji 26sp + 奶茶名 11sp 粗体(maxLines1)+ 价格 13sp 粗体珊瑚红。三列格子适合咖啡的「快取展示」,模拟咖啡店菜单的紧凑布局。 - 打工人咖啡心得:标题「💼 打工人咖啡心得」,遍历
CHECKS筛选含「拿铁」/「Dirty」/「玛奇朵」的打卡动态(打工不喝咖啡的生椰拿铁打卡),每条包含:avatar + 用户名 + mood 标签(奶油色底深咖啡棕字)+ content + ❤️点赞·日期。打工人咖啡心得是咖啡 Tab 的特色内容,从社区打卡中筛选咖啡相关动态,增加内容丰富度。
8.5 联名 collabContent:IP 对决卡 + 列表 + 侧栏统计
联名 Tab 由三部分组成:
- 标题:「🧸 IP联名·奶茶大战」15sp 粗体珍珠黑字。
- IP 对决卡:
Row三列布局,左列草莓熊奶昔(#F0788A草莓粉底白字,emoji + 奶茶名 + 178杯销量),中间 VS(18sp 粗体奶茶棕字 + 「本周」9sp 灰字),右列玉桂狗云朵顶(#8FBDE天蓝底白字,emoji + 奶茶名 + 160杯销量)。IP 对决卡是联名 Tab 的核心特色,通过左右对决 + 中间 VS 模拟 IP 联名奶茶的人气对决,草莓粉 vs 天蓝形成强烈视觉对比。 - 列表 + 侧栏统计:
Row两列布局,左列collabTeas()筛选出 3 款联名奶茶列表(emoji + 奶茶名 + tag·价格 + ›箭头),右列侧栏统计(483 本周联名杯数奶油色底 / 9款 历史联名蜜桃粉底 / 62% 杯套收藏率纯白底,三列纵向排列)。侧栏统计是联名 Tab 的特色数据展示,杯套收藏率 62% 反映了 IP 联名奶茶的收藏文化。

8.6 隐藏 hiddenContent:标签云 + 紧凑列表 + 暗号玩法
隐藏 Tab 由四部分组成:
- 标题:「🔐 老茶客才知道的隐藏菜单」15sp 粗体珍珠黑字。
- 标签云:
Flex wrap渲染ADDON_OPTIONS6 个加料标签(#珍珠加满/#芋泥加满/#脆啵啵加满/#西米加满/#青稞加满/#布丁加满,奶油色底珍珠黑字)+SUGAR_LEVELS6 个糖度标签(#无糖/#微糖/#三分甜/#五分甜/#七分甜/#全糖,蜜桃粉底玫瑰棕字)。标签云是隐藏 Tab 的特色内容,展示隐藏菜单的加料和糖度组合。 - 紧凑列表:
hiddenTeas()筛选出 3 款隐藏奶茶(咸蛋黄波波/海盐芝士抹茶/豆腐脑奶茶),每行 emoji + 奶茶名 + tag 标签(珍珠黑底白字,如「暗黑料理」「茶道弟子」「武汉限定」)+ desc(maxLines1)+ 价格。第 3 款豆腐脑奶茶#FFF0F3蜜桃粉底高亮。 - 暗号玩法提示:「💬 暗号玩法:对店员说"老规矩",部分门店有惊喜」10sp 灰字。暗号玩法是隐藏 Tab 的特色互动,增加隐藏菜单的神秘感和趣味性。
九、打卡 Tab:统计 banner + 心情筛选 + 打卡动态列表
打卡 Tab checkContent 由三部分组成:
- 统计 banner:纯白底圆角 14,等宽四列——「28 累计打卡」奶茶棕字、「12 奶茶搭子」玫瑰棕字、「2.6万 获得点赞」珊瑚红字、「+ 发布」按钮(珊瑚红底白字圆角 14,点击打开发布打卡弹窗)。
- 心情筛选 chips:
Flex wrap渲染MOODS5 个心情(幸福到冒泡/续命成功/酸甜上头/勇者无畏/负罪感清零),第 1 个激活奶茶棕底白字,其他奶油色底奶茶棕字。 - 打卡动态列表:8 条打卡,每个卡片包含:
- 头部行:avatar 26sp + 用户名 13sp 粗体 +
badgeCount徽章(🔥爆赞/👍热评/🌱新人帖,珊瑚红字蜜桃粉底)+ 日期·门店 9sp 浅棕字 + ⋯ 更多按钮(点击打开删除弹窗)。 - 内容:content 12sp 深棕字 lineHeight:18。
- 标签行:🧋 奶茶名(奶油色底奶茶棕字)+ mood(蜜桃粉底玫瑰棕字)+ ❤️点赞数(珊瑚红字,右对齐)。
- 头部行:avatar 26sp + 用户名 13sp 粗体 +
打卡 Tab 通过统计 banner + 心情筛选 + 打卡动态列表三段式布局,完整展示奶茶打卡社区的内容,通过 badgeCount 徽章和 mood 标签增加社区互动感,是奶茶社交的内容核心区。

十、配方 Tab:标题 + 创建按钮 + 配方卡(步骤列表)
配方 Tab recipeContent 由标题栏 + 5 个配方卡组成:
- 标题栏:左侧「📒 茶友自研配方」16sp 粗体 + 「点单口诀 · 省钱大法 · 隐藏吃法」10sp 灰字,右侧「✚ 创建」按钮(奶茶棕底白字圆角 14,点击打开创建配方弹窗)。
- 配方卡列表:5 个配方(平价版杨枝甘露/藏起来的杨梅季/厚乳提拉米苏杯/芋泥啵啵杀手/隐藏菜单海盐提神),每个卡片包含:
- 头部行:emoji 28sp + 配方名 14sp 粗体 + by 作者·❤️点赞数 10sp 灰字 + level 标签(奶油色底奶茶棕字,如新手友好/进阶操作/高手玩法)+ sweetness 标签(蜜桃粉底玫瑰棕字)。
- 茶底/加料标签行:茶底:base(奶油色底深咖啡棕字)+ 加料:addons(奶油色底深咖啡棕字)。
- 步骤列表:
ForEach(r.steps)渲染步骤数组,每步左侧圆形序号(#C98A5B焦糖金底白字 18x18 圆角 9)+ 步骤文字 11sp 深棕字,整体#FFF6EC奶油浅橙底圆角 10 padding 10。
配方 Tab 通过配方卡 + 步骤列表的布局,完整展示茶友自研配方的点单口诀,通过圆形序号步骤列表增加可操作性,是奶茶社区的共创核心区。
十一、我的 Tab:个人卡 + 成就墙 + 常去门店
我的 Tab mineContent 由三部分组成:
- 个人卡:
angle:120棕到玫红渐变#8A5A3B→#B76E79(从奶茶棕到玫瑰棕,模拟奶茶从醇厚到甜美的渐变),圆角 16,包含:- 头部行:🧋 40sp + 「奶茶少女阿茶」16sp 粗体白字 + 「LV.7 资深茶友 · 连续打卡28天」10sp 奶油色字 + >箭头。
- 三列统计:「286 喝过杯数」「42 解锁品类」「¥3,842 年度奶茶账单」,均奶油色字。
- 成就墙:纯白底圆角 14,标题「🏅 我的成就」,
Flex wrap渲染 6 个成就(🎖️百杯成就/🌙深夜茶鬼/🕵️隐藏菜单发现者/🧸联名全收集/📸打卡狂魔/💰省钱大师),前 2 个奶油色底奶茶棕字,中间 2 个蜜桃粉底玫瑰棕字,后 2 个奶油浅橙底深咖啡棕字。 - 常去门店:纯白底圆角 14,标题「🏠 常去门店」,
SHOPS6 家门店列表,每行门店名 + 地区·距离·排队时间 + 人均¥价格(珊瑚红粗体)。
我的 Tab 通过个人卡 + 成就墙 + 常去门店三段式布局,完整展示茶友的个人数据和成就,年度奶茶账单 ¥3,842 是我的 Tab 的特色数据,让茶友直观看到自己在奶茶上的投入。

十二、4 个模态弹窗:新增/新增/删除/详情四态闭环
应用实现了 4 个模态弹窗,覆盖发布打卡(新增)、创建配方(新增)、删除打卡(删除)、奶茶详情(查询)四种交互形态,每个弹窗都有专属的头色与表单结构。
12.1 发布打卡 checkModal:粉棕渐变头 + 选奶茶/甜度/心情 + 杯数步进器 + 感想
打卡弹窗 width:88% height:76% 圆角 18,顶部是 angle:120 粉棕渐变头 #B76E79→#8A5A3B(从玫瑰棕到奶茶棕),标题「📸 发布奶茶打卡」17sp 粗体白字 + 「让茶友看看你今天的快乐」11sp 奶油色字。
滚动内容区包含:
- 选奶茶:
Flex wrap8 款奶茶(TEAS 前 8 款,emoji + 奶茶名),选中态奶茶棕底白字,非选中奶油浅橙底深咖啡棕字,点击切换checkTeaIdx。 - 甜度选择:
Flex wrap6 个糖度(SUGAR_LEVELS),选中态#F0788A草莓粉底白字,非选中蜜桃粉底玫瑰棕字,点击切换checkSugarIdx。 - 此刻心情:
Flex wrap5 个心情(MOODS),选中态奶茶棕底白字,非选中奶油色底奶茶棕字,点击切换checkMoodIdx。 - 杯数步进器:「本日杯数」标签 + 步进器(减
#FFF6EC奶油浅橙底奶茶棕字 / 中间 N 杯纯白底深棕字 / 加奶茶棕底白字,1-9 杯)+ 提示(>=3 杯显示「⚠️ 茶友注意糖分摄入」珊瑚红字,其他「适量刚刚好」灰字)。杯数步进器是发布打卡弹窗的特色字段,记录今日喝了几杯奶茶,>=3 杯时给出糖分摄入警告,是健康化设计。 - 一句话感想:TextInput(placeholder「今天喝到了什么好喝的?」),奶油浅橙底圆角 10。
- 操作按钮:「取消」奶油浅橙底灰字 + 「发布打卡」
#B76E79玫瑰棕底白字粗体,均点击关闭弹窗。
12.2 创建配方 recipeModal:奶油头(唯一浅色头)+ 配方名/茶底/加料多选/难度
配方弹窗 width:88% height:72% 圆角 18,顶部是纯奶油 #FFE8D6 底(深字,是所有弹窗中唯一用浅色头的),标题「📒 创建自研配方」17sp 粗体珍珠黑字 + 「好配方值得被全圈茶友复刻」11sp 灰字。
滚动内容区包含:
- 配方脑洞名:TextInput(placeholder「例如:平价版杨枝甘露」),奶油浅橙底圆角 10。
- 基础茶底:
Flex wrap5 个茶底(TEA_BASES:茉莉奶绿/烤奶/生椰厚乳/燕麦奶/厚牛乳),选中态奶茶棕底白字,非选中奶油色底奶茶棕字,点击切换recipeBaseIdx。 - 加料配方(可多选):
Flex wrap6 个加料(ADDON_OPTIONS),选中态#E86A5B珊瑚红底白字带「✓」前缀,非选中奶油浅橙底深咖啡棕字,点击切换pickedAdds[i]布尔值实现多选。加料多选是创建配方弹窗的特色功能,茶友可以自由组合多种小料。 - 操作难度:
Row3 个难度(LEVEL_OPTIONS:新手友好/进阶操作/高手玩法),选中态#B76E79玫瑰棕底白字,非选中蜜桃粉底玫瑰棕字,点击切换recipeLevelIdx。 - 提示框:💡 「发布后茶友可以一键复制你的点单口诀,优质配方会被收录进配方库首页。」奶油浅橙底圆角 10。
- 操作按钮:「再想想」奶油浅橙底灰字 + 「发布配方」奶茶棕底白字粗体,均点击关闭弹窗。
12.3 删除打卡 deleteModal:🗑️ 警告小卡
删除弹窗是 4 个弹窗中最小的一个,width:72% 纯白底圆角 18,居中显示:
- 🗑️ 垃圾桶图标 36sp
- 「删除这条打卡?」16sp 粗体珍珠黑字
- 「删除后好友将无法再看到这条动态,点赞和评论会一并消失,操作不可恢复。」11sp 灰字(lineHeight:17,居中)
- 操作按钮:「手滑了」奶油浅橙底奶茶棕字 + 「确认删除」
#E86A5B珊瑚红底白字粗体,均点击关闭弹窗。「手滑了」是删除弹窗的特色按钮文案,用奶茶圈轻松的语气替代严肃的「取消」,符合甜美奶茶风。
删除弹窗通过 🗑️ 图标 + 珊瑚红确认按钮 + 详细后果说明(好友无法看到/点赞评论一并消失/不可恢复),提醒用户操作不可逆,符合删除类操作的交互规范。
12.4 奶茶详情 teaDetailModal:棕渐变头 + 四列统计 + 品鉴笔记 + 茶友实录 + 操作
详情弹窗 width:88% height:78% 圆角 18,是 4 个弹窗中最高的一个,内容最丰富。顶部是 angle:135 棕渐变头 #8A5A3B→#C98A5B(从奶茶棕到焦糖金):
- emoji 40sp(通过
detailTeaEmoji()查找) - 奶茶名 18sp 粗体白字(通过
detailTeaName()查找) - 英文名 10sp 奶油色字(通过
detailTeaEng()查找) - 标签行:tag 标签(奶油色底奶茶棕字)+ sales 标签(半透明白底奶油色字,如「本周售出 96 杯」)
滚动内容区包含:
- 四列统计卡:
#FFF6EC奶油浅橙底圆角 12,等宽四列——「到手价 price」#E86A5B珊瑚红字、「热量 heat + heatLabel(352) 写死」#8A5A3B奶茶棕字、「建议糖度 sugar + sugarColor 映射色」、「茶友评分 rating」#B76E79玫瑰棕字。 - 品鉴笔记:标题「☕ 品鉴笔记」+ desc 11sp 深棕字 lineHeight:18 + starStr 星号 + shop 门店(右对齐)。
- 茶友实录:标题「💬 茶友实录」+
ForEach(REVIEWS)渲染 3 条点评,每条为#FFF6EC奶油浅橙底圆角 10 卡片,包含 👤 + 用户名 + starStr(右对齐)+ content 10sp 灰字。评价用户名「奶茶资深评委」「热饮爱好者联盟」「珍珠口感鉴定师」充满奶茶圈梗文化。 - 操作按钮:「🧾 点单口诀」奶油色底奶茶棕字 + 「❤️ 加入心愿单」
#B76E79玫瑰棕底白字粗体,均点击关闭弹窗。

12.5 detailTea 系列方法:13 个查找方法
详情弹窗通过 selectedTeaId 从 TEAS 数组中查找对应奶茶,定义了 13 个方法(所有应用中查找方法最多的):detailTeaName()、detailTeaEng()、detailTeaEmoji()、detailTeaPrice()、detailTeaHeat()、detailTeaSugar()、detailTeaShop()、detailTeaTag()、detailTeaStar()、detailTeaRating()、detailTeaDesc()、detailTeaSales()。每个方法都通过 for 循环遍历 TEAS 查找匹配 id 的奶茶,再返回对应字段,若未找到则返回默认值(空字符串/0/‘🧋’)。13 个查找方法对应 TeaItem 的 13 个展示字段,是数据驱动详情弹窗的完整实现。
十三、特性对比表
| 特性维度 | QQ奶茶·茶友圈实现 |
|---|---|
| 主题风格 | 奶茶棕 #8A5A3B + 珍珠黑 #3B2B26 + 蜜桃粉 #F5A3B7 甜美奶茶风 |
| 导航架构 | 底部 4 Tab(奶茶/打卡/配方/我的)+ 顶部 6 Tab(新品/经典/果茶/咖啡/联名/隐藏),顶部 Tab 仅奶茶模块显示,6 Tab 等宽 layoutWeight(1) + 双下划线(激活蜜桃粉/非激活奶油色) |
| 数据模型 | 8 个 interface(ColorPalette/TabItem/ParticleItem/TeaItem/CheckItem/RecipeItem/ShopItem/TeaReview),TeaItem 含 eng/heat/sugar/sales 特色字段,RecipeItem 含 steps 字符串数组,ParticleItem 无 id/y 字段(水平飘动专用) |
| 业务数据量 | 18 奶茶 + 8 打卡 + 5 配方 + 6 门店 + 3 点评 + 7 周销量柱状图 |
| 工具函数 | 6 个(catColor/catLabel/heatLabel/sugarColor/salesBarHeight/badgeCount),所有应用中最多 |
| 筛选函数 | 9 个(newLeftTeas/newRightTeas/classicTeas/fruitTeas/coffeeTeas/collabTeas/hiddenTeas/hotTeas),所有应用中最多,直接数组索引而非 filter |
| 粒子系统 | 14 粒子,🧋🥤🍡✨🧁 五图标循环,独立 speed 0.6~2.0(所有应用中最快),setInterval 80ms(所有应用中最快),水平向右飘动(x+speed,x>360回-20循环,模拟奶茶杯传送带/出杯口),x 用 px 而非百分比,y 固定 5 行分布 80+(i%5)*118,所有应用中唯一水平飘动粒子 |
| 数据可视化 | 经典 Tab 周销量柱状图(7 天,salesBarHeight v/132*96,周六 idx===5 蜜桃粉高亮其他奶茶棕,周六132最高)+ 果茶 Tab 配方进化时间轴(蜜桃粉竖线连接,第2代蜜桃粉底高亮) |
| 场景差异化 | 6 大顶部 Tab 完全不同布局:新品(秋日banner+双列瀑布流左右列高度错落)/经典(横滑大卡奶茶棕底+周销量柱状图+热销TOP5)/果茶(配方进化时间轴蜜桃粉竖线)/咖啡(三列格子+打工人咖啡心得从社区筛选)/联名(IP对决卡草莓粉VS天蓝+列表+侧栏统计杯套收藏率)/隐藏(标签云加料+糖度+紧凑列表+暗号玩法老规矩) |
| 模态弹窗 | 4 个:发布打卡(粉棕渐变头,选奶茶/甜度/心情/杯数步进器1-9杯>=3杯糖分警告/感想TextInput)/创建配方(奶油头,唯一浅色头,配方名/茶底/加料多选pickedAdds布尔数组/难度/提示)/删除打卡(🗑️小卡,手滑了文案/确认删除珊瑚红)/奶茶详情(棕渐变头,四列统计到手价/热量heatLabel写死352/建议糖度sugarColor/茶友评分,品鉴笔记+茶友实录3条,点单口诀+加入心愿单) |
| 状态变量 | 19 个 @State + 1 个 private timerId,含 pickedAdds 布尔数组实现多选 |
| 生命周期 | aboutToAppear 构建粒子+启动定时器80ms,aboutToDisappear clearInterval+重置-1 |
| 头部特色 | 三色渐变 angle:135 #8A5A3B→#B07B55→#D9A876(所有应用中唯一三色渐变头部,模拟奶茶从杯底到杯面渐变)+ 🔍💌🛒三图标+购物车12角标蜜桃粉底+搜索条+茶友榜按钮 |
| 底部 Tab 特色 | 纯白底 + 顶部圆角 18(与头部底部圆角呼应,奶茶杯视觉隐喻) |
| 顶部 Tab 特色 | 6 Tab 等宽,纯白底圆角14,激活态蜜桃粉圆角下划线+非激活奶油色圆角下划线(双下划线对比) |
| 分类色系统 | catColor 6 色(奶茶类型映射,新品焦糖金/经典奶茶棕/果茶草莓粉/咖啡深咖啡棕/联名玫瑰棕/隐藏珍珠黑)+ sugarColor 3 色(糖度映射,低糖草绿/中糖焦糖金/高糖珊瑚红)+ heatLabel 3 档(轻负担/小放纵/快乐核弹) |
| 奶茶圈文化梗 | 快乐吨吨吨/快乐核弹/手滑了/老规矩暗号/百杯成就/深夜茶鬼/隐藏菜单发现者/联名全收集/打卡狂魔/省钱大师/全糖去冰战士/半糖主义小K/低卡求生指南/珍珠口感鉴定师/年度奶茶账单/杯套收藏率/第二杯半价 |
| 特色数据字段 | TeaItem.eng(英文名)+ TeaItem.heat(热量kcal)+ TeaItem.sugar(建议糖度)+ TeaItem.sales(周销量)+ RecipeItem.steps(步骤字符串数组)+ CheckItem.mood(心情) |
| 弹窗跳转 | 奶茶详情「点单口诀」/「加入心愿单」→关闭详情 |
十四、完整代码
应用完整源码位于 1096.ets,总计约 1700 行,包含 8 个 interface、15 个常量数组/对象、6 个工具函数、9 个筛选函数、1 个 @Entry @Component struct Index 主组件(19 个 @State + 2 个生命周期方法 + 19 个 @Builder 方法 + 13 个 detailTea 查找方法 + build 方法)。
核心代码结构如下:
// ============================================================
// 1096.ets - QQ奶茶·茶友圈
// 场景:奶茶爱好者的打卡/配方/测评社区
// 风格:奶茶棕 + 珍珠黑 + 蜜桃粉 甜美奶茶风
// 底部Tab:奶茶 / 打卡 / 配方 / 我的
// 顶部Tab:新品 / 经典 / 果茶 / 咖啡 / 联名 / 隐藏
// 弹框:发布打卡(新增) / 创建配方(新增) / 删除打卡(删除) / 奶茶详情(详情)
// ============================================================
// ---------------- 配色 ----------------
interface ColorPalette {
bg: string;
card: string;
brown: string;
deep: string;
pink: string;
cream: string;
gold: string;
sub: string;
line: string;
}
const PALETTE: ColorPalette = {
bg: '#FFF6EC',
card: '#FFFFFF',
brown: '#8A5A3B',
deep: '#3B2B26',
pink: '#F5A3B7',
cream: '#FFE8D6',
gold: '#E8B04B',
sub: '#9C8578',
line: '#F0E2D6'
};
// ---------------- 通用结构 ----------------
interface TabItem {
label: string;
icon: string;
}
interface ParticleItem {
x: number;
emoji: string;
size: number;
speed: number;
opacity: number;
}
// ---------------- 奶茶条目 ----------------
interface TeaItem {
id: number;
name: string;
eng: string;
cat: string;
price: number;
heat: number;
sugar: string;
rating: number;
starStr: string;
shop: string;
tag: string;
desc: string;
sales: number;
emoji: string;
}
// ---------------- 打卡动态 ----------------
interface CheckItem {
id: number;
user: string;
avatar: string;
tea: string;
shop: string;
mood: string;
date: string;
likes: number;
content: string;
}
// ---------------- 配方条目 ----------------
interface RecipeItem {
id: number;
name: string;
base: string;
addons: string;
sweetness: string;
level: string;
author: string;
likes: number;
emoji: string;
steps: string[];
}
// ---------------- 门店条目 ----------------
interface ShopItem {
id: number;
name: string;
area: string;
distance: string;
avg: number;
queue: string;
}
// ---------------- 点评条目 ----------------
interface TeaReview {
id: number;
user: string;
tea: string;
starStr: string;
content: string;
}
// ---------------- Tab 配置 ----------------
const BOTTOM_TABS: TabItem[] = [
{ label: '奶茶', icon: '🧋' },
{ label: '打卡', icon: '📸' },
{ label: '配方', icon: '📒' },
{ label: '我的', icon: '🍫' }
];
const TOP_TABS: TabItem[] = [
{ label: '新品', icon: '✨' },
{ label: '经典', icon: '🏆' },
{ label: '果茶', icon: '🍇' },
{ label: '咖啡', icon: '☕' },
{ label: '联名', icon: '🧸' },
{ label: '隐藏', icon: '🔐' }
];
// ---------------- 奶茶数据(18款写死) ----------------
const TEAS: TeaItem[] = [
{ id: 1, name: '桂花酒酿奶茶', eng: 'Osmanthus Rice Wine Milk', cat: 'new', price: 19, heat: 342, sugar: '七分甜', rating: 4.8, starStr: '⭐⭐⭐⭐✨', shop: '茶言观舍·江汉路店', tag: '秋日限定', desc: '手打酒酿混入锡兰奶茶,桂花在舌尖炸开,一口入魂的秋天气息。', sales: 96, emoji: '🍂' },
{ id: 2, name: '冻冻青提恋', eng: 'Iced Green Grape Jelly', cat: 'new', price: 18, heat: 268, sugar: '五分甜', rating: 4.7, starStr: '⭐⭐⭐⭐', shop: '茶百趣·光谷店', tag: '脆爽爆汁', desc: '整串阳光青提手剥去皮,配脆脆冻冻,喝前摇一摇快乐加倍。', sales: 88, emoji: '💚' },
{ id: 3, name: '芝士莓莓果园', eng: 'Cheese Berry Orchard', cat: 'new', price: 22, heat: 356, sugar: '微糖', rating: 4.9, starStr: '⭐⭐⭐⭐⭐', shop: '沪上阿姨·街道口店', tag: '店长推荐', desc: '五种莓果慢熬果酱,盖上一层咸芝士奶盖,酸甜咸三重奏。', sales: 122, emoji: '🍓' },
{ id: 4, name: '黑糖珍珠鲜奶', eng: 'Brown Sugar Boba Milk', cat: 'classic', price: 16, heat: 380, sugar: '全糖', rating: 4.9, starStr: '⭐⭐⭐⭐⭐', shop: '鹿角巷·江汉路店', tag: '镇店招牌', desc: '虎纹挂壁现熬黑糖,木薯珍珠Q弹到怀疑人生,奶茶圈的永流传。', sales: 210, emoji: '🧋' },
{ id: 5, name: '茉莉奶绿', eng: 'Jasmine Milk Green', cat: 'classic', price: 12, heat: 260, sugar: '三分甜', rating: 4.8, starStr: '⭐⭐⭐⭐✨', shop: '一点点·中山公园店', tag: '白月光', desc: '横县茉莉九窨打底,绿茶清苦撞上牛乳顺滑,12块的价格初恋的味道。', sales: 185, emoji: '🍵' },
{ id: 6, name: '烤奶三分糖', eng: 'Roasted Milk Tea', cat: 'classic', price: 14, heat: 295, sugar: '三分甜', rating: 4.7, starStr: '⭐⭐⭐⭐', shop: 'CoCo都可·楚河店', tag: '老伙计', desc: '焦香烤奶味浓得像烤面包,冬天捧一杯能从手暖到心。', sales: 156, emoji: '🔥' },
{ id: 7, name: '杨枝甘露', eng: 'Mango Pomelo Sago', cat: 'fruit', price: 18, heat: 312, sugar: '五分甜', rating: 4.9, starStr: '⭐⭐⭐⭐⭐', shop: '满记甜品·武商店', tag: '港式经典', desc: '台农芒果打泥,西柚粒爆苦回甘,椰浆西米一碗装下整个夏天。', sales: 168, emoji: '🥭' },
{ id: 8, name: '多肉葡萄', eng: 'Grape Cheese Foam', cat: 'fruit', price: 19, heat: 288, sugar: '微糖', rating: 4.8, starStr: '⭐⭐⭐⭐✨', shop: '喜小茶·光谷店', tag: '断货王', desc: '巨峰葡萄手捣带皮,紫到发光的果肉沉底,芝士奶盖一勺封顶。', sales: 195, emoji: '🍇' },
{ id: 9, name: '满杯百香果', eng: 'Passionfruit Full Cup', cat: 'fruit', price: 17, heat: 224, sugar: '五分甜', rating: 4.7, starStr: '⭐⭐⭐⭐', shop: '益禾堂·司门口店', tag: '维C炸弹', desc: '两颗百香果一整杯,酸得眯眼睛也要一口接一口,越酸越上头。', sales: 140, emoji: '🥤' },
{ id: 10, name: '生椰拿铁厚乳', eng: 'Coconut Latte Double', cat: 'coffee', price: 20, heat: 245, sugar: '微糖', rating: 4.8, starStr: '⭐⭐⭐⭐✨', shop: '瑞幸咖啡·泛悦城店', tag: '打工续命', desc: 'IIAC金奖豆配厚椰乳,生椰的甜托住咖啡的苦,周一早八的救赎。', sales: 205, emoji: '🥥' },
{ id: 11, name: '燕麦Dirty', eng: 'Oatmeal Dirty', cat: 'coffee', price: 22, heat: 198, sugar: '无糖', rating: 4.6, starStr: '⭐⭐⭐⭐', shop: 'Manner·武商梦时代店', tag: '冷萃控', desc: '冰博克牛奶打底,浓缩直接淋上去,第一口苦到清醒第二口回甘。', sales: 98, emoji: '🧊' },
{ id: 12, name: '焦糖玛奇朵冰', eng: 'Iced Caramel Macchiato', cat: 'coffee', price: 23, heat: 286, sugar: '五分甜', rating: 4.7, starStr: '⭐⭐⭐⭐', shop: '星巴克·江汉路店', tag: '甜度自由', desc: '香草糖浆沉底,焦糖淋奶泡画格纹,搅拌之前记得先拍照。', sales: 112, emoji: '🍮' },
{ id: 13, name: '联名·草莓熊奶昔', eng: 'Strawberry Bear Shake', cat: 'collab', price: 26, heat: 420, sugar: '全糖', rating: 4.6, starStr: '⭐⭐⭐⭐', shop: '茶百道·江宸天街店', tag: '迪士尼联名', desc: '草莓熊杯套萌到犯规,奶昔浓到能立吸管,喝完杯子都舍不得扔。', sales: 178, emoji: '🧸' },
{ id: 14, name: '联名·库洛米紫苏饮', eng: 'Kuromi Perilla Soda', cat: 'collab', price: 25, heat: 186, sugar: '微糖', rating: 4.5, starStr: '⭐⭐⭐⭐', shop: '乐乐茶·武商店', tag: '三丽鸥联名', desc: '紫苏柠檬气泡水,库洛米主题杯和贴纸,为紫色控量身定做。', sales: 145, emoji: '💜' },
{ id: 15, name: '联名·玉桂狗云朵顶', eng: 'Cinnamoroll Cloud Top', cat: 'collab', price: 24, heat: 368, sugar: '七分甜', rating: 4.7, starStr: '⭐⭐⭐⭐', shop: '沪上阿姨·光谷店', tag: '三丽鸥联名', desc: '棉花糖云朵盖顶,热奶茶把云朵慢慢焐化,治愈系下午茶首选。', sales: 160, emoji: '☁️' },
{ id: 16, name: '隐藏·咸蛋黄波波', eng: 'Salted Egg Yolk Boba', cat: 'hidden', price: 21, heat: 392, sugar: '五分甜', rating: 4.4, starStr: '⭐⭐⭐⭐', shop: '伏见桃山·江汉路店', tag: '暗黑料理', desc: '咸蛋黄流沙混进波波里,咸甜党狂喜,没喝过的都说勇。', sales: 66, emoji: '🥚' },
{ id: 17, name: '隐藏·海盐芝士抹茶', eng: 'Sea Salt Matcha Cheese', cat: 'hidden', price: 22, heat: 302, sugar: '微糖', rating: 4.6, starStr: '⭐⭐⭐⭐', shop: '伏见桃山·光谷店', tag: '茶道弟子', desc: '宇治抹茶三段冲泡,海盐芝士咸口中和苦涩,抹茶脑袋的隐藏菜单。', sales: 74, emoji: '🍵' },
{ id: 18, name: '隐藏·豆腐脑奶茶', eng: 'Tofu Pudding Milk Tea', cat: 'hidden', price: 19, heat: 276, sugar: '三分甜', rating: 4.3, starStr: '⭐⭐⭐✨', shop: '茶话弄·楚河店', tag: '武汉限定', desc: '嫩豆腐脑当小料铺底,豆香和奶香缠绵,本地人才知道的吃法。', sales: 52, emoji: '🥄' }
];
// ---------------- 打卡数据(8条写死) ----------------
const CHECKS: CheckItem[] = [
{ id: 1, user: '奶茶少女阿茶', avatar: '🧋', tea: '桂花酒酿奶茶', shop: '茶言观舍·江汉路店', mood: '幸福到冒泡', date: '今天 14:32', likes: 128, content: '排队40分钟终于喝上!酒酿味好浓,桂花是真的在杯里飘,秋天被装进杯子里了~' },
{ id: 2, user: '全糖去冰战士', avatar: '⚔️', tea: '黑糖珍珠鲜奶', shop: '鹿角巷·江汉路店', mood: '续命成功', date: '今天 12:05', likes: 96, content: '周三固定的快乐,珍珠给得超多,吸管都插不下去的程度,老板格局打开了。' },
{ id: 3, user: '半糖主义小K', avatar: '🎤', tea: '芝士莓莓果园', shop: '沪上阿姨·街道口店', mood: '酸甜上头', date: '昨天 18:47', likes: 87, content: '莓果酱是真的有果肉,芝士咸香绝了,喝完连杯子里的果粒都想抠出来。' },
{ id: 4, user: '打工不喝咖啡', avatar: '💼', tea: '生椰拿铁厚乳', shop: '瑞幸咖啡·泛悦城店', mood: '满血复活', date: '昨天 09:15', likes: 203, content: '周一早八的救命水,厚椰乳多到像在喝椰奶,咖啡因含量刚刚好不心慌。' },
{ id: 5, user: '隐藏菜单猎人', avatar: '🕵️', tea: '隐藏·咸蛋黄波波', shop: '伏见桃山·江汉路店', mood: '勇者无畏', date: '前天 20:11', likes: 156, content: '咸蛋黄流沙波波第一口是懵的,第二口就停不下来了,咸甜党集合!' },
{ id: 6, user: '紫薯芋泥脑袋', avatar: '💜', tea: '联名·库洛米紫苏饮', shop: '乐乐茶·武商店', mood: '少女心泛滥', date: '前天 15:38', likes: 142, content: '为了库洛米杯套买的,结果紫苏气泡水意外好喝,酸酸的很有层次!' },
{ id: 7, user: '三天不喝会瘦', avatar: '🥤', tea: '多肉葡萄', shop: '喜小茶·光谷店', mood: '快乐水时间', date: '3天前 16:22', likes: 65, content: '葡萄是真的带皮手捣,果肉沉底厚厚一层,搅拌之后每一口都有果肉。' },
{ id: 8, user: '低卡求生指南', avatar: '🥗', tea: '满杯百香果', shop: '益禾堂·司门口店', mood: '负罪感清零', date: '4天前 11:58', likes: 54, content: '224大卡喝出整杯快乐,百香果籽嚼着超解压,减肥期也能冲。' }
];
// ---------------- 配方数据(5条写死) ----------------
const RECIPES: RecipeItem[] = [
{ id: 1, name: '平价版杨枝甘露', base: '茉莉奶绿', addons: '芒果丁+西米+西柚粒', sweetness: '五分甜', level: '新手友好', author: '甜品复刻家', likes: 342, emoji: '🥭', steps: ['茉莉奶绿去冰五分甜', '加一份芒果丁打底', '再加一份西米', '让店员多给两包西柚粒', '摇匀开喝,成本省一半'] },
{ id: 2, name: '藏起来的杨梅季', base: '满杯百香果', addons: '杨梅+脆啵啵', sweetness: '微糖', level: '季节限定', author: '四季鲜果报', likes: 276, emoji: '🍒', steps: ['百香果改微糖', '追加一份杨梅(仅夏季有)', '加脆啵啵增加口感', '去冰口感更佳', '杨梅季过就要等明年'] },
{ id: 3, name: '厚乳提拉米苏杯', base: '生椰拿铁厚乳', addons: '可可粉+奶油顶', sweetness: '五分甜', level: '进阶操作', author: '咖啡因依赖者', likes: 198, emoji: '🍰', steps: ['生椰拿铁五分甜加热', '要一杯奶油顶(需加购)', '可可粉自行携带撒顶', '用勺子先吃奶油再喝', '仪式感直接拉满'] },
{ id: 4, name: '芋泥啵啵杀手', base: '烤奶三分糖', addons: '芋泥+青稞+波波', sweetness: '三分甜', level: '碳水核弹', author: '芋泥教父', likes: 415, emoji: '🍠', steps: ['烤奶打底三分糖', '双份芋泥必须安排', '加青稞增加嚼劲', '再补一份波波', '喝完这杯可以不吃晚饭'] },
{ id: 5, name: '隐藏菜单·海盐提神', base: '燕麦Dirty', addons: '海盐奶盖+柠檬片', sweetness: '无糖', level: '高手玩法', author: '隐藏菜单猎人', likes: 167, emoji: '🧊', steps: ['燕麦Dirty无糖加冰', '追加一份海盐奶盖', '挤两滴柠檬汁提味', '先喝一口原味再搅拌', '咸柠风味一秒到香港'] }
];
// ---------------- 门店数据(6家写死) ----------------
const SHOPS: ShopItem[] = [
{ id: 1, name: '茶言观舍·江汉路店', area: '江汉路步行街', distance: '320m', avg: 18, queue: '约10分钟' },
{ id: 2, name: '鹿角巷·江汉路店', area: '江汉路地铁C口', distance: '450m', avg: 16, queue: '约25分钟' },
{ id: 3, name: '沪上阿姨·街道口店', area: '街道口创意城', distance: '1.2km', avg: 15, queue: '约8分钟' },
{ id: 4, name: '喜小茶·光谷店', area: '光谷步行街', distance: '3.6km', avg: 19, queue: '约15分钟' },
{ id: 5, name: '伏见桃山·江汉路店', area: '江汉路Happy站台', distance: '510m', avg: 21, queue: '约20分钟' },
{ id: 6, name: '益禾堂·司门口店', area: '司门口黄鹤楼', distance: '2.8km', avg: 12, queue: '约5分钟' }
];
// ---------------- 点评数据(3条写死) ----------------
const REVIEWS: TeaReview[] = [
{ id: 1, user: '奶茶资深评委', tea: '桂花酒酿奶茶', starStr: '⭐⭐⭐⭐✨', content: '酒酿浓度全场第一,桂花香不齁,扣半星是因为排队真的太久。' },
{ id: 2, user: '热饮爱好者联盟', tea: '联名·玉桂狗云朵顶', starStr: '⭐⭐⭐⭐', content: '云朵棉花糖焐化之后和奶茶融为一体,甜度略高,建议配无糖茶解腻。' },
{ id: 3, user: '珍珠口感鉴定师', tea: '黑糖珍珠鲜奶', starStr: '⭐⭐⭐⭐⭐', content: '珍珠煮得恰到好处,中心还带一点韧劲,黑糖挂壁是现熬的证据,满分!' }
];
// ---------------- 周销量图表数据 ----------------
const SALES_LABELS: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const SALES_BARS: number[] = [62, 48, 55, 43, 88, 132, 118];
// ---------------- 弹框选项 ----------------
const MOODS: string[] = ['幸福到冒泡', '续命成功', '酸甜上头', '勇者无畏', '负罪感清零'];
const SUGAR_LEVELS: string[] = ['无糖', '微糖', '三分甜', '五分甜', '七分甜', '全糖'];
const TEA_BASES: string[] = ['茉莉奶绿', '烤奶', '生椰厚乳', '燕麦奶', '厚牛乳'];
const ADDON_OPTIONS: string[] = ['珍珠', '芋泥', '脆啵啵', '西米', '青稞', '布丁'];
const LEVEL_OPTIONS: string[] = ['新手友好', '进阶操作', '高手玩法'];
// ---------------- 全局纯函数 ----------------
function catColor(cat: string): string {
if (cat === 'new') {
return '#E8B04B';
}
if (cat === 'classic') {
return '#8A5A3B';
}
if (cat === 'fruit') {
return '#F0788A';
}
if (cat === 'coffee') {
return '#6B4F3A';
}
if (cat === 'collab') {
return '#B76E79';
}
return '#3B2B26';
}
function catLabel(cat: string): string {
if (cat === 'new') {
return '本周新品';
}
if (cat === 'classic') {
return '经典回归';
}
if (cat === 'fruit') {
return '鲜果现切';
}
if (cat === 'coffee') {
return '咖啡因站';
}
if (cat === 'collab') {
return 'IP联名';
}
return '隐藏菜单';
}
function heatLabel(heat: number): string {
if (heat < 250) {
return '轻负担';
}
if (heat < 350) {
return '小放纵';
}
return '快乐核弹';
}
function sugarColor(sugar: string): string {
if (sugar === '无糖' || sugar === '微糖' || sugar === '三分甜') {
return '#5FA77A';
}
if (sugar === '五分甜') {
return '#E8B04B';
}
return '#E86A5B';
}
function salesBarHeight(v: number): string {
return Math.round(v / 132 * 96).toString() + 'vp';
}
function newLeftTeas(): TeaItem[] {
return [TEAS[0], TEAS[2]];
}
function newRightTeas(): TeaItem[] {
return [TEAS[1]];
}
function classicTeas(): TeaItem[] {
return [TEAS[3], TEAS[4], TEAS[5]];
}
function fruitTeas(): TeaItem[] {
return [TEAS[6], TEAS[7], TEAS[8]];
}
function coffeeTeas(): TeaItem[] {
return [TEAS[9], TEAS[10], TEAS[11]];
}
function collabTeas(): TeaItem[] {
return [TEAS[12], TEAS[13], TEAS[14]];
}
function hiddenTeas(): TeaItem[] {
return [TEAS[15], TEAS[16], TEAS[17]];
}
function hotTeas(): TeaItem[] {
return [TEAS[3], TEAS[9], TEAS[7], TEAS[4], TEAS[12]];
}
function badgeCount(likes: number): string {
if (likes >= 150) {
return '🔥爆赞';
}
if (likes >= 80) {
return '👍热评';
}
return '🌱新人帖';
}
// ============================================================
// 主页面
// ============================================================
@Entry
@Component
struct Index {
@State currentBottomTab: number = 0;
@State currentTopTab: number = 0;
@State selectedTeaId: number = 1;
@State showCheckModal: boolean = false;
@State showRecipeModal: boolean = false;
@State showDeleteModal: boolean = false;
@State showDetailModal: boolean = false;
@State checkTeaIdx: number = 0;
@State checkMoodIdx: number = 0;
@State checkSugarIdx: number = 3;
@State checkCups: number = 1;
@State checkWords: string = '';
@State recipeBaseIdx: number = 0;
@State recipeLevelIdx: number = 0;
@State pickedAdds: boolean[] = [true, false, false, false, false, false];
@State particles: ParticleItem[] = [];
private timerId: number = -1;
aboutToAppear(): void {
this.buildParticles();
this.timerId = setInterval(() => {
this.driftParticles();
}, 80);
}
aboutToDisappear(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
buildParticles(): void {
const emojis: string[] = ['🧋', '🥤', '🍡', '✨', '🧁'];
const list: ParticleItem[] = [];
for (let i = 0; i < 14; i++) {
list.push({
x: Math.random() * 340,
emoji: emojis[i % emojis.length],
size: 12 + Math.random() * 10,
speed: 0.6 + Math.random() * 1.4,
opacity: 0.25 + Math.random() * 0.45
});
}
this.particles = list;
}
driftParticles(): void {
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].x += this.particles[i].speed;
if (this.particles[i].x > 360) {
this.particles[i].x = -20;
}
}
this.particles = this.particles.slice();
}
// 详情弹框数据取值
detailTeaName(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].name;
}
}
return '';
}
detailTeaEng(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].eng;
}
}
return '';
}
detailTeaEmoji(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].emoji;
}
}
return '🧋';
}
detailTeaPrice(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return '¥' + TEAS[i].price.toString();
}
}
return '';
}
detailTeaHeat(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].heat.toString() + ' kcal';
}
}
return '';
}
detailTeaSugar(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].sugar;
}
}
return '';
}
detailTeaShop(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].shop;
}
}
return '';
}
detailTeaTag(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].tag;
}
}
return '';
}
detailTeaStar(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].starStr;
}
}
return '';
}
detailTeaRating(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].rating.toFixed(1) + '分';
}
}
return '';
}
detailTeaDesc(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return TEAS[i].desc;
}
}
return '';
}
detailTeaSales(): string {
for (let i = 0; i < TEAS.length; i++) {
if (TEAS[i].id === this.selectedTeaId) {
return '本周售出 ' + TEAS[i].sales.toString() + ' 杯';
}
}
return '';
}
// ---------------- 头部(电商风,无动画) ----------------
@Builder
headerBuilder() {
Column() {
Row() {
Column() {
Text('🧋 茶友圈')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('今天也要快乐吨吨吨')
.fontSize(11)
.fontColor('#FFE8D6')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row({ space: 14 }) {
Text('🔍')
.fontSize(20)
Text('💌')
.fontSize(20)
Column() {
Text('🛒')
.fontSize(20)
Text('12')
.fontSize(9)
.fontColor('#FFFFFF')
.backgroundColor('#F5A3B7')
.borderRadius(6)
.padding({ left: 4, right: 4 })
.margin({ top: -6 })
}
.alignItems(HorizontalAlign.End)
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 10 })
// 搜索条
Row({ space: 8 }) {
Text('🔎 搜索:芋泥 / 生椰 / 隐藏菜单')
.fontSize(12)
.fontColor('#B99B85')
.layoutWeight(1)
Text('茶友榜')
.fontSize(11)
.fontColor('#FFF6EC')
.backgroundColor('#A96F4C')
.borderRadius(10)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 12 })
}
.linearGradient({
angle: 135,
colors: [['#8A5A3B', 0], ['#B07B55', 0.6], ['#D9A876', 1]]
})
.borderRadius({ bottomLeft: 18, bottomRight: 18 })
}
// ---------------- 顶部Tab(可点击切换) ----------------
@Builder
topTabsBuilder() {
Row() {
ForEach(TOP_TABS, (t: TabItem, idx: number) => {
Column() {
Text(t.icon)
.fontSize(17)
Text(t.label)
.fontSize(12)
.fontColor(this.currentTopTab === idx ? '#8A5A3B' : '#9C8578')
.fontWeight(this.currentTopTab === idx ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
if (this.currentTopTab === idx) {
Divider()
.width(16)
.strokeWidth(3)
.color('#F5A3B7')
.borderRadius(2)
.margin({ top: 4 })
} else {
Divider()
.width(16)
.strokeWidth(3)
.color('#FFF6EC')
.borderRadius(2)
.margin({ top: 4 })
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 6 })
.onClick(() => {
this.currentTopTab = idx;
})
}, (t: TabItem, idx: number) => 'top' + idx.toString() + t.label)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.margin({ top: 10 })
}
// ---------------- 新品Tab:banner + 双列瀑布 ----------------
@Builder
newContent() {
Column({ space: 12 }) {
// 运营banner
Column() {
Text('🍂 秋日新品季')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('桂花酒酿系列限时上线 · 第二杯半价')
.fontSize(12)
.fontColor('#FFE8D6')
.margin({ top: 6 })
Row({ space: 10 }) {
Text('🧋 酒酿奶茶')
.fontSize(11)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
Text('🍇 冻冻青提')
.fontSize(11)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
}
.margin({ top: 10 })
}
.width('100%')
.padding(18)
.alignItems(HorizontalAlign.Start)
.linearGradient({
angle: 120,
colors: [['#8A5A3B', 0], ['#C98A5B', 1]]
})
.borderRadius(16)
Text('✨ 本周鲜上架')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
// 双列瀑布流
Row({ space: 10 }) {
Column({ space: 10 }) {
ForEach(newLeftTeas(), (t: TeaItem) => {
Column() {
Text(t.emoji)
.fontSize(34)
.margin({ top: 16 })
Text(t.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.margin({ top: 8 })
Text(t.eng)
.fontSize(9)
.fontColor('#9C8578')
.margin({ top: 2 })
Text(t.desc)
.fontSize(11)
.fontColor('#9C8578')
.margin({ top: 8 })
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
Text('¥' + t.price.toString())
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
Text(t.tag)
.fontSize(9)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.margin({ top: 10, bottom: 14 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.alignItems(HorizontalAlign.Start)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem) => 'nl' + t.id.toString())
}
.layoutWeight(1)
Column({ space: 10 }) {
ForEach(newRightTeas(), (t: TeaItem) => {
Column() {
Text(t.emoji)
.fontSize(34)
.margin({ top: 24 })
Text(t.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.margin({ top: 8 })
Text(t.eng)
.fontSize(9)
.fontColor('#9C8578')
.margin({ top: 2 })
Text(t.desc)
.fontSize(11)
.fontColor('#9C8578')
.margin({ top: 10 })
.maxLines(4)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
Text('¥' + t.price.toString())
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
Text(t.starStr)
.fontSize(10)
}
.margin({ top: 12, bottom: 20 })
}
.width('100%')
.backgroundColor('#FFF0F3')
.borderRadius(14)
.alignItems(HorizontalAlign.Start)
.padding({ left: 14, right: 14 })
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem) => 'nr' + t.id.toString())
}
.layoutWeight(1)
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 16 })
}
// ---------------- 经典Tab:横滑大卡 + 柱状图 ----------------
@Builder
classicContent() {
Column({ space: 14 }) {
Text('🏆 经典永流传')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
// 横滑大卡
Scroll() {
Row({ space: 12 }) {
ForEach(classicTeas(), (t: TeaItem) => {
Column() {
Text(t.emoji)
.fontSize(40)
.margin({ top: 18 })
Text(t.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 8 })
Text(t.sugar + ' · ' + t.heat.toString() + 'kcal')
.fontSize(10)
.fontColor('#FFE8D6')
.margin({ top: 4 })
Row({ space: 6 }) {
Text('¥' + t.price.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE8D6')
Text(t.starStr)
.fontSize(10)
}
.margin({ top: 10, bottom: 18 })
}
.width(150)
.backgroundColor('#8A5A3B')
.borderRadius(16)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem) => 'cl' + t.id.toString())
}
.padding({ left: 14, right: 14 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 周销量柱状图
Column() {
Text('📈 本圈茶友周点单量(万杯)')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Row({ space: 6 }) {
ForEach(SALES_BARS, (v: number, i: number) => {
Column({ space: 5 }) {
Text(v.toString())
.fontSize(9)
.fontColor('#8A5A3B')
Column()
.width(16)
.height(salesBarHeight(v))
.backgroundColor(i === 5 ? '#F5A3B7' : '#C98A5B')
.borderRadius({ topLeft: 5, topRight: 5 })
Text(SALES_LABELS[i])
.fontSize(9)
.fontColor('#9C8578')
}
.alignItems(HorizontalAlign.Center)
}, (v: number, i: number) => 'bar' + i.toString() + v.toString())
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 12 })
Text('周六是奶茶快乐日,点单量是周四的3倍')
.fontSize(10)
.fontColor('#9C8578')
.margin({ top: 8 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
.margin({ left: 14, right: 14 })
// 热销榜
Column() {
Text('🔥 热销TOP5')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
.margin({ bottom: 8 })
ForEach(hotTeas(), (t: TeaItem, i: number) => {
Row({ space: 10 }) {
Text((i + 1).toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(i < 3 ? '#E86A5B' : '#9C8578')
.width(20)
Text(t.emoji)
.fontSize(18)
Column() {
Text(t.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(t.sales.toString() + '杯 · ' + t.shop)
.fontSize(10)
.fontColor('#9C8578')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('¥' + t.price.toString())
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem, i: number) => 'hot' + i.toString() + t.id.toString())
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
.margin({ left: 14, right: 14, bottom: 16 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding({ top: 10 })
}
// ---------------- 果茶Tab:配方进化时间轴 ----------------
@Builder
fruitContent() {
Column({ space: 0 }) {
Text('🍇 鲜果现切·配方进化史')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
.margin({ top: 10, bottom: 12 })
ForEach(fruitTeas(), (t: TeaItem, i: number) => {
Row({ space: 12 }) {
// 时间轴节点
Column({ space: 4 }) {
Text(t.emoji)
.fontSize(20)
if (i < 2) {
Column()
.width(2)
.layoutWeight(1)
.backgroundColor('#F5A3B7')
.margin({ top: 4 })
}
}
.width(40)
.alignItems(HorizontalAlign.Center)
// 内容卡
Column({ space: 6 }) {
Row({ space: 8 }) {
Text(t.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.layoutWeight(1)
Text('¥' + t.price.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
}
.width('100%')
Text(t.desc)
.fontSize(11)
.fontColor('#9C8578')
.lineHeight(16)
Row({ space: 8 }) {
Text('第' + (i + 1).toString() + '代配方')
.fontSize(9)
.fontColor('#F0788A')
.backgroundColor('#FFF0F3')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(t.sugar)
.fontSize(9)
.fontColor(sugarColor(t.sugar))
.backgroundColor('#FFF6EC')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(t.heat.toString() + 'kcal · ' + heatLabel(t.heat))
.fontSize(9)
.fontColor('#9C8578')
}
}
.layoutWeight(1)
.backgroundColor(i === 1 ? '#FFF0F3' : '#FFFFFF')
.borderRadius(14)
.padding(12)
.margin({ bottom: 14 })
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}
.width('100%')
.alignItems(VerticalAlign.Top)
}, (t: TeaItem, i: number) => 'ft' + i.toString() + t.id.toString())
}
.width('100%')
.padding({ left: 14, right: 14, bottom: 16 })
}
// ---------------- 咖啡Tab:三列格子 + 心得 ----------------
@Builder
coffeeContent() {
Column({ space: 14 }) {
Text('☕ 咖啡因补给站')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(coffeeTeas(), (t: TeaItem) => {
Column() {
Text(t.emoji)
.fontSize(26)
.margin({ top: 12 })
Text(t.name)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.margin({ top: 6 })
.maxLines(1)
Text('¥' + t.price.toString())
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
.margin({ top: 4, bottom: 12 })
}
.width('31%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.alignItems(HorizontalAlign.Center)
.margin({ bottom: 8 })
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem) => 'cf' + t.id.toString())
}
.width('100%')
// 打工人心得
Column() {
Text('💼 打工人咖啡心得')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
.margin({ bottom: 6 })
ForEach(CHECKS, (c: CheckItem) => {
if (c.tea.indexOf('拿铁') >= 0 || c.tea.indexOf('Dirty') >= 0 || c.tea.indexOf('玛奇朵') >= 0) {
Row({ space: 10 }) {
Text(c.avatar)
.fontSize(22)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(c.user)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(c.mood)
.fontSize(9)
.fontColor('#6B4F3A')
.backgroundColor('#FFE8D6')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
Text(c.content)
.fontSize(11)
.fontColor('#9C8578')
.lineHeight(16)
Text('❤️ ' + c.likes.toString() + ' · ' + c.date)
.fontSize(9)
.fontColor('#C4A99A')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
}
}, (c: CheckItem) => 'cc' + c.id.toString())
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 16 })
}
// ---------------- 联名Tab:对决卡 + 侧栏统计 ----------------
@Builder
collabContent() {
Column({ space: 14 }) {
Text('🧸 IP联名·奶茶大战')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
// 对决卡
Row({ space: 10 }) {
Column() {
Text('🧸')
.fontSize(34)
.margin({ top: 14 })
Text('草莓熊奶昔')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 6 })
Text('178杯')
.fontSize(11)
.fontColor('#FFE8D6')
.margin({ top: 4, bottom: 14 })
}
.layoutWeight(1)
.backgroundColor('#F0788A')
.borderRadius(14)
.alignItems(HorizontalAlign.Center)
Column() {
Text('VS')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#8A5A3B')
Text('本周')
.fontSize(9)
.fontColor('#9C8578')
}
.alignItems(HorizontalAlign.Center)
Column() {
Text('☁️')
.fontSize(34)
.margin({ top: 14 })
Text('玉桂狗云朵顶')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 6 })
Text('160杯')
.fontSize(11)
.fontColor('#FFE8D6')
.margin({ top: 4, bottom: 14 })
}
.layoutWeight(1)
.backgroundColor('#8FB8DE')
.borderRadius(14)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
// 列表 + 侧栏统计
Row({ space: 10 }) {
Column({ space: 10 }) {
ForEach(collabTeas(), (t: TeaItem) => {
Row({ space: 10 }) {
Text(t.emoji)
.fontSize(22)
Column({ space: 2 }) {
Text(t.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(t.tag + ' · ¥' + t.price.toString())
.fontSize(10)
.fontColor('#9C8578')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('›')
.fontSize(16)
.fontColor('#C4A99A')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(12)
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem) => 'co' + t.id.toString())
}
.layoutWeight(1)
// 侧栏统计
Column({ space: 10 }) {
Column({ space: 4 }) {
Text('483')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#8A5A3B')
Text('本周联名杯数')
.fontSize(9)
.fontColor('#9C8578')
}
.width('100%')
.backgroundColor('#FFE8D6')
.borderRadius(12)
.padding({ top: 10, bottom: 10 })
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('9款')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#B76E79')
Text('历史联名')
.fontSize(9)
.fontColor('#9C8578')
}
.width('100%')
.backgroundColor('#FFF0F3')
.borderRadius(12)
.padding({ top: 10, bottom: 10 })
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('62%')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
Text('杯套收藏率')
.fontSize(9)
.fontColor('#9C8578')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding({ top: 10, bottom: 10 })
.alignItems(HorizontalAlign.Center)
}
.width(86)
}
.width('100%')
.alignItems(VerticalAlign.Top)
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 16 })
}
// ---------------- 隐藏Tab:标签云 + 紧凑列表 ----------------
@Builder
hiddenContent() {
Column({ space: 14 }) {
Text('🔐 老茶客才知道的隐藏菜单')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
ForEach(ADDON_OPTIONS, (a: string) => {
Text('#' + a + '加满')
.fontSize(11)
.fontColor('#3B2B26')
.backgroundColor('#FFE8D6')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.margin(4)
}, (a: string) => 'chip' + a)
ForEach(SUGAR_LEVELS, (s: string) => {
Text('#' + s)
.fontSize(11)
.fontColor('#B76E79')
.backgroundColor('#FFF0F3')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.margin(4)
}, (s: string) => 'schip' + s)
}
.width('100%')
ForEach(hiddenTeas(), (t: TeaItem) => {
Row({ space: 12 }) {
Text(t.emoji)
.fontSize(24)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(t.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(t.tag)
.fontSize(8)
.fontColor('#FFFFFF')
.backgroundColor('#3B2B26')
.borderRadius(6)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
}
Text(t.desc)
.fontSize(10)
.fontColor('#9C8578')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('¥' + t.price.toString())
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
}
.width('100%')
.backgroundColor(t.id === 18 ? '#FFF0F3' : '#FFFFFF')
.borderRadius(12)
.padding(12)
.onClick(() => {
this.selectedTeaId = t.id;
this.showDetailModal = true;
})
}, (t: TeaItem) => 'hd' + t.id.toString())
Text('💬 暗号玩法:对店员说"老规矩",部分门店有惊喜')
.fontSize(10)
.fontColor('#9C8578')
.width('100%')
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 16 })
}
// ---------------- 奶茶首页(顶部Tab切换) ----------------
@Builder
teaHomeContent() {
Column() {
this.topTabsBuilder()
if (this.currentTopTab === 0) {
this.newContent()
} else if (this.currentTopTab === 1) {
this.classicContent()
} else if (this.currentTopTab === 2) {
this.fruitContent()
} else if (this.currentTopTab === 3) {
this.coffeeContent()
} else if (this.currentTopTab === 4) {
this.collabContent()
} else {
this.hiddenContent()
}
}
.width('100%')
}
// ---------------- 打卡页 ----------------
@Builder
checkContent() {
Column({ space: 12 }) {
// 统计banner
Row({ space: 0 }) {
Column({ space: 4 }) {
Text('28')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#8A5A3B')
Text('累计打卡')
.fontSize(10)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('12')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#B76E79')
Text('奶茶搭子')
.fontSize(10)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('2.6万')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
Text('获得点赞')
.fontSize(10)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 6 }) {
Text('+ 发布')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#E86A5B')
.borderRadius(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.onClick(() => {
this.showCheckModal = true;
})
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding({ top: 14, bottom: 14 })
// 心情筛选chips
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(MOODS, (m: string, i: number) => {
Text(m)
.fontSize(11)
.fontColor(i === 0 ? '#FFFFFF' : '#8A5A3B')
.backgroundColor(i === 0 ? '#8A5A3B' : '#FFE8D6')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin(4)
}, (m: string, i: number) => 'mood' + i.toString() + m)
}
.width('100%')
// 打卡动态列表
ForEach(CHECKS, (c: CheckItem) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Text(c.avatar)
.fontSize(26)
Column({ space: 2 }) {
Row({ space: 6 }) {
Text(c.user)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(badgeCount(c.likes))
.fontSize(8)
.fontColor('#E86A5B')
.backgroundColor('#FFF0F3')
.borderRadius(6)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
}
Text(c.date + ' · ' + c.shop)
.fontSize(9)
.fontColor('#C4A99A')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('⋯')
.fontSize(16)
.fontColor('#9C8578')
.onClick(() => {
this.showDeleteModal = true;
})
}
.width('100%')
Text(c.content)
.fontSize(12)
.fontColor('#4A3830')
.lineHeight(18)
.width('100%')
Row({ space: 8 }) {
Text('🧋 ' + c.tea)
.fontSize(10)
.fontColor('#8A5A3B')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
Text(c.mood)
.fontSize(10)
.fontColor('#B76E79')
.backgroundColor('#FFF0F3')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
Text('❤️ ' + c.likes.toString())
.fontSize(10)
.fontColor('#E86A5B')
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
}, (c: CheckItem) => 'ck' + c.id.toString())
}
.width('100%')
.padding({ left: 14, right: 14, top: 10, bottom: 16 })
}
// ---------------- 配方页 ----------------
@Builder
recipeContent() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('📒 茶友自研配方')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text('点单口诀 · 省钱大法 · 隐藏吃法')
.fontSize(10)
.fontColor('#9C8578')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('✚ 创建')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#8A5A3B')
.borderRadius(14)
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.onClick(() => {
this.showRecipeModal = true;
})
}
.width('100%')
.margin({ top: 10 })
ForEach(RECIPES, (r: RecipeItem) => {
Column({ space: 10 }) {
Row({ space: 10 }) {
Text(r.emoji)
.fontSize(28)
Column({ space: 3 }) {
Text(r.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text('by ' + r.author + ' · ❤️ ' + r.likes.toString())
.fontSize(10)
.fontColor('#9C8578')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 3 }) {
Text(r.level)
.fontSize(9)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
Text(r.sweetness)
.fontSize(9)
.fontColor('#B76E79')
.backgroundColor('#FFF0F3')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row({ space: 8 }) {
Text('茶底:' + r.base)
.fontSize(10)
.fontColor('#6B4F3A')
.backgroundColor('#FFF6EC')
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
Text('加料:' + r.addons)
.fontSize(10)
.fontColor('#6B4F3A')
.backgroundColor('#FFF6EC')
.borderRadius(8)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
}
.width('100%')
Column({ space: 6 }) {
ForEach(r.steps, (s: string, si: number) => {
Row({ space: 8 }) {
Text((si + 1).toString())
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#C98A5B')
.borderRadius(9)
.width(18)
.height(18)
.textAlign(TextAlign.Center)
Text(s)
.fontSize(11)
.fontColor('#4A3830')
.layoutWeight(1)
}
.width('100%')
}, (s: string, si: number) => 'st' + si.toString() + r.id.toString())
}
.width('100%')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.padding(10)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
}, (r: RecipeItem) => 'rp' + r.id.toString())
}
.width('100%')
.padding({ left: 14, right: 14, bottom: 16 })
}
// ---------------- 我的页 ----------------
@Builder
mineContent() {
Column({ space: 14 }) {
// 个人卡
Column() {
Row({ space: 12 }) {
Text('🧋')
.fontSize(40)
Column({ space: 4 }) {
Text('奶茶少女阿茶')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('LV.7 资深茶友 · 连续打卡28天')
.fontSize(10)
.fontColor('#FFE8D6')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('>')
.fontSize(18)
.fontColor('#FFE8D6')
}
.width('100%')
Row({ space: 0 }) {
Column({ space: 3 }) {
Text('286')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE8D6')
Text('喝过杯数')
.fontSize(9)
.fontColor('#FFE8D6')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 3 }) {
Text('42')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE8D6')
Text('解锁品类')
.fontSize(9)
.fontColor('#FFE8D6')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 3 }) {
Text('¥3,842')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFE8D6')
Text('年度奶茶账单')
.fontSize(9)
.fontColor('#FFE8D6')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.margin({ top: 16 })
}
.width('100%')
.padding(18)
.linearGradient({
angle: 120,
colors: [['#8A5A3B', 0], ['#B76E79', 1]]
})
.borderRadius(16)
.margin({ top: 10 })
// 成就墙
Column() {
Text('🏅 我的成就')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
.margin({ bottom: 8 })
Flex({ wrap: FlexWrap.Wrap }) {
Text('🎖️ 百杯成就')
.fontSize(10)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.margin(4)
Text('🌙 深夜茶鬼')
.fontSize(10)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.margin(4)
Text('🕵️ 隐藏菜单发现者')
.fontSize(10)
.fontColor('#B76E79')
.backgroundColor('#FFF0F3')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.margin(4)
Text('🧸 联名全收集')
.fontSize(10)
.fontColor('#B76E79')
.backgroundColor('#FFF0F3')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.margin(4)
Text('📸 打卡狂魔')
.fontSize(10)
.fontColor('#6B4F3A')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.margin(4)
Text('💰 省钱大师')
.fontSize(10)
.fontColor('#6B4F3A')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.margin(4)
}
.width('100%')
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
// 常去门店
Column() {
Text('🏠 常去门店')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
.margin({ bottom: 6 })
ForEach(SHOPS, (s: ShopItem) => {
Row({ space: 10 }) {
Column({ space: 2 }) {
Text(s.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(s.area + ' · ' + s.distance + ' · ' + s.queue)
.fontSize(9)
.fontColor('#9C8578')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('人均¥' + s.avg.toString())
.fontSize(11)
.fontColor('#E86A5B')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding({ top: 9, bottom: 9 })
}, (s: ShopItem) => 'sh' + s.id.toString())
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(14)
.padding(14)
}
.width('100%')
.padding({ left: 14, right: 14, bottom: 16 })
}
// ---------------- 底部Tab ----------------
@Builder
bottomTabsBuilder() {
Row() {
ForEach(BOTTOM_TABS, (t: TabItem, idx: number) => {
Column({ space: 3 }) {
Text(t.icon)
.fontSize(20)
Text(t.label)
.fontSize(10)
.fontColor(this.currentBottomTab === idx ? '#8A5A3B' : '#9C8578')
.fontWeight(this.currentBottomTab === idx ? FontWeight.Bold : FontWeight.Normal)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 })
.onClick(() => {
this.currentBottomTab = idx;
})
}, (t: TabItem, idx: number) => 'bt' + idx.toString() + t.label)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 18, topRight: 18 })
}
// ---------------- 弹框1:发布打卡(新增·粉棕渐变头) ----------------
@Builder
checkModalBuilder() {
Column() {
// 弹框头
Column() {
Text('📸 发布奶茶打卡')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 16 })
Text('让茶友看看你今天的快乐')
.fontSize(11)
.fontColor('#FFE8D6')
.margin({ top: 4, bottom: 16 })
}
.width('100%')
.linearGradient({
angle: 120,
colors: [['#B76E79', 0], ['#8A5A3B', 1]]
})
.borderRadius({ topLeft: 18, topRight: 18 })
Scroll() {
Column({ space: 14 }) {
// 选奶茶
Column({ space: 8 }) {
Text('今天喝的是')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(TEAS, (t: TeaItem, i: number) => {
if (i < 8) {
Text(t.emoji + ' ' + t.name)
.fontSize(10)
.fontColor(this.checkTeaIdx === i ? '#FFFFFF' : '#6B4F3A')
.backgroundColor(this.checkTeaIdx === i ? '#8A5A3B' : '#FFF6EC')
.borderRadius(12)
.padding({ left: 9, right: 9, top: 5, bottom: 5 })
.margin(4)
.onClick(() => {
this.checkTeaIdx = i;
})
}
}, (t: TeaItem, i: number) => 'mt' + i.toString() + t.id.toString())
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 甜度
Column({ space: 8 }) {
Text('甜度选择')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(SUGAR_LEVELS, (s: string, i: number) => {
Text(s)
.fontSize(10)
.fontColor(this.checkSugarIdx === i ? '#FFFFFF' : '#B76E79')
.backgroundColor(this.checkSugarIdx === i ? '#F0788A' : '#FFF0F3')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin(4)
.onClick(() => {
this.checkSugarIdx = i;
})
}, (s: string, i: number) => 'sg' + i.toString() + s)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 心情
Column({ space: 8 }) {
Text('此刻心情')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(MOODS, (m: string, i: number) => {
Text(m)
.fontSize(10)
.fontColor(this.checkMoodIdx === i ? '#FFFFFF' : '#8A5A3B')
.backgroundColor(this.checkMoodIdx === i ? '#8A5A3B' : '#FFE8D6')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin(4)
.onClick(() => {
this.checkMoodIdx = i;
})
}, (m: string, i: number) => 'cm' + i.toString() + m)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 杯数步进器
Row({ space: 12 }) {
Text('本日杯数')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Row({ space: 0 }) {
Text('−')
.fontSize(16)
.fontColor('#8A5A3B')
.backgroundColor('#FFF6EC')
.borderRadius({ topLeft: 10, bottomLeft: 10 })
.width(34)
.height(30)
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.checkCups > 1) {
this.checkCups--;
}
})
Text(this.checkCups.toString() + ' 杯')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.backgroundColor('#FFFFFF')
.height(30)
.textAlign(TextAlign.Center)
.padding({ left: 14, right: 14 })
Text('+')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('#8A5A3B')
.borderRadius({ topRight: 10, bottomRight: 10 })
.width(34)
.height(30)
.textAlign(TextAlign.Center)
.onClick(() => {
if (this.checkCups < 9) {
this.checkCups++;
}
})
}
.borderRadius(10)
Text(this.checkCups >= 3 ? '⚠️ 茶友注意糖分摄入' : '适量刚刚好')
.fontSize(9)
.fontColor(this.checkCups >= 3 ? '#E86A5B' : '#9C8578')
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
// 一句话
Column({ space: 8 }) {
Text('一句话感想')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
TextInput({ placeholder: '今天喝到了什么好喝的?', text: this.checkWords })
.fontSize(12)
.fontColor('#3B2B26')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.height(40)
.padding({ left: 12, right: 12 })
.onChange((v: string) => {
this.checkWords = v;
})
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 提交
Row({ space: 12 }) {
Text('取消')
.fontSize(13)
.fontColor('#9C8578')
.backgroundColor('#FFF6EC')
.borderRadius(20)
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showCheckModal = false;
})
Text('发布打卡')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#B76E79')
.borderRadius(20)
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showCheckModal = false;
})
}
.width('100%')
.margin({ top: 4, bottom: 18 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.align(Alignment.Top)
}
.width('88%')
.height('76%')
.backgroundColor('#FFFFFF')
.borderRadius(18)
.clip(true)
}
// ---------------- 弹框2:创建配方(新增·奶油头) ----------------
@Builder
recipeModalBuilder() {
Column() {
Column() {
Row({ space: 8 }) {
Text('📒')
.fontSize(22)
Text('创建自研配方')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
}
.margin({ top: 16 })
Text('好配方值得被全圈茶友复刻')
.fontSize(11)
.fontColor('#9C8578')
.margin({ top: 4, bottom: 14 })
}
.width('100%')
.backgroundColor('#FFE8D6')
.borderRadius({ topLeft: 18, topRight: 18 })
Scroll() {
Column({ space: 14 }) {
// 配方名
Column({ space: 8 }) {
Text('配方脑洞名')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
TextInput({ placeholder: '例如:平价版杨枝甘露', text: '' })
.fontSize(12)
.fontColor('#3B2B26')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.height(40)
.padding({ left: 12, right: 12 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 茶底
Column({ space: 8 }) {
Text('基础茶底')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(TEA_BASES, (b: string, i: number) => {
Text(b)
.fontSize(10)
.fontColor(this.recipeBaseIdx === i ? '#FFFFFF' : '#8A5A3B')
.backgroundColor(this.recipeBaseIdx === i ? '#8A5A3B' : '#FFE8D6')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin(4)
.onClick(() => {
this.recipeBaseIdx = i;
})
}, (b: string, i: number) => 'rb' + i.toString() + b)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 小料多选
Column({ space: 8 }) {
Text('加料配方(可多选)')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(ADDON_OPTIONS, (a: string, i: number) => {
Text((this.pickedAdds[i] ? '✓ ' : '') + a)
.fontSize(10)
.fontColor(this.pickedAdds[i] ? '#FFFFFF' : '#6B4F3A')
.backgroundColor(this.pickedAdds[i] ? '#E86A5B' : '#FFF6EC')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin(4)
.onClick(() => {
this.pickedAdds[i] = !this.pickedAdds[i];
})
}, (a: string, i: number) => 'ra' + i.toString() + a)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 难度
Column({ space: 8 }) {
Text('操作难度')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Row({ space: 8 }) {
ForEach(LEVEL_OPTIONS, (l: string, i: number) => {
Text(l)
.fontSize(10)
.fontColor(this.recipeLevelIdx === i ? '#FFFFFF' : '#B76E79')
.backgroundColor(this.recipeLevelIdx === i ? '#B76E79' : '#FFF0F3')
.borderRadius(12)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.onClick(() => {
this.recipeLevelIdx = i;
})
}, (l: string, i: number) => 'rl' + i.toString() + l)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 提示
Row({ space: 8 }) {
Text('💡')
.fontSize(14)
Text('发布后茶友可以一键复制你的点单口诀,优质配方会被收录进配方库首页。')
.fontSize(10)
.fontColor('#9C8578')
.lineHeight(15)
.layoutWeight(1)
}
.width('100%')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.padding(10)
Row({ space: 12 }) {
Text('再想想')
.fontSize(13)
.fontColor('#9C8578')
.backgroundColor('#FFF6EC')
.borderRadius(20)
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showRecipeModal = false;
})
Text('发布配方')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#8A5A3B')
.borderRadius(20)
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showRecipeModal = false;
})
}
.width('100%')
.margin({ top: 4, bottom: 18 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.align(Alignment.Top)
}
.width('88%')
.height('72%')
.backgroundColor('#FFFFFF')
.borderRadius(18)
.clip(true)
}
// ---------------- 弹框3:删除打卡(小警告) ----------------
@Builder
deleteModalBuilder() {
Column({ space: 14 }) {
Text('🗑️')
.fontSize(36)
Text('删除这条打卡?')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text('删除后好友将无法再看到这条动态,\n点赞和评论会一并消失,操作不可恢复。')
.fontSize(11)
.fontColor('#9C8578')
.textAlign(TextAlign.Center)
.lineHeight(17)
Row({ space: 12 }) {
Text('手滑了')
.fontSize(13)
.fontColor('#8A5A3B')
.backgroundColor('#FFF6EC')
.borderRadius(20)
.layoutWeight(1)
.height(40)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDeleteModal = false;
})
Text('确认删除')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#E86A5B')
.borderRadius(20)
.layoutWeight(1)
.height(40)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDeleteModal = false;
})
}
.width('100%')
.margin({ top: 6 })
}
.width('72%')
.backgroundColor('#FFFFFF')
.borderRadius(18)
.padding({ left: 20, right: 20, top: 24, bottom: 22 })
.alignItems(HorizontalAlign.Center)
}
// ---------------- 弹框4:奶茶详情(棕渐变头) ----------------
@Builder
teaDetailModalBuilder() {
Column() {
Column() {
Text(this.detailTeaEmoji())
.fontSize(40)
.margin({ top: 18 })
Text(this.detailTeaName())
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 8 })
Text(this.detailTeaEng())
.fontSize(10)
.fontColor('#FFE8D6')
.margin({ top: 2 })
Row({ space: 8 }) {
Text(this.detailTeaTag())
.fontSize(9)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
Text(this.detailTeaSales())
.fontSize(9)
.fontColor('#FFE8D6')
.backgroundColor('rgba(255,255,255,0.2)')
.borderRadius(8)
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
}
.margin({ top: 10, bottom: 18 })
}
.width('100%')
.linearGradient({
angle: 135,
colors: [['#8A5A3B', 0], ['#C98A5B', 1]]
})
.borderRadius({ topLeft: 18, topRight: 18 })
.alignItems(HorizontalAlign.Center)
Scroll() {
Column({ space: 14 }) {
// 数据行
Row({ space: 0 }) {
Column({ space: 4 }) {
Text(this.detailTeaPrice())
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#E86A5B')
Text('到手价')
.fontSize(9)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(this.detailTeaHeat())
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#8A5A3B')
Text(heatLabel(352))
.fontSize(9)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(this.detailTeaSugar())
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(sugarColor(this.detailTeaSugar()))
Text('建议糖度')
.fontSize(9)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text(this.detailTeaRating())
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#B76E79')
Text('茶友评分')
.fontSize(9)
.fontColor('#9C8578')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.backgroundColor('#FFF6EC')
.borderRadius(12)
.padding({ top: 12, bottom: 12 })
// 描述
Column({ space: 8 }) {
Text('☕ 品鉴笔记')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
Text(this.detailTeaDesc())
.fontSize(11)
.fontColor('#4A3830')
.lineHeight(18)
Row({ space: 8 }) {
Text(this.detailTeaStar())
.fontSize(11)
Text(this.detailTeaShop())
.fontSize(10)
.fontColor('#9C8578')
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 点评
Column({ space: 10 }) {
Text('💬 茶友实录')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
.width('100%')
ForEach(REVIEWS, (r: TeaReview) => {
Column({ space: 5 }) {
Row({ space: 8 }) {
Text('👤')
.fontSize(14)
Text(r.user)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#3B2B26')
Text(r.starStr)
.fontSize(9)
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
Text(r.content)
.fontSize(10)
.fontColor('#9C8578')
.lineHeight(15)
}
.width('100%')
.backgroundColor('#FFF6EC')
.borderRadius(10)
.padding(10)
}, (r: TeaReview) => 'rv' + r.id.toString())
}
.width('100%')
.alignItems(HorizontalAlign.Start)
// 操作
Row({ space: 12 }) {
Text('🧾 点单口诀')
.fontSize(13)
.fontColor('#8A5A3B')
.backgroundColor('#FFE8D6')
.borderRadius(20)
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDetailModal = false;
})
Text('❤️ 加入心愿单')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#B76E79')
.borderRadius(20)
.layoutWeight(1)
.height(42)
.textAlign(TextAlign.Center)
.onClick(() => {
this.showDetailModal = false;
})
}
.width('100%')
.margin({ top: 2, bottom: 18 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.align(Alignment.Top)
}
.width('88%')
.height('78%')
.backgroundColor('#FFFFFF')
.borderRadius(18)
.clip(true)
}
// ---------------- 主布局 ----------------
build() {
Stack() {
// 主页面
Column() {
this.headerBuilder()
Scroll() {
Column() {
if (this.currentBottomTab === 0) {
this.teaHomeContent()
} else if (this.currentBottomTab === 1) {
this.checkContent()
} else if (this.currentBottomTab === 2) {
this.recipeContent()
} else {
this.mineContent()
}
}
.width('100%')
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.align(Alignment.Top)
this.bottomTabsBuilder()
}
.width('100%')
.height('100%')
// 粒子特效层
ForEach(this.particles, (p: ParticleItem, i: number) => {
Text(p.emoji)
.fontSize(p.size)
.opacity(p.opacity)
.position({ x: p.x, y: 80 + (i % 5) * 118 })
}, (p: ParticleItem, i: number) => 'pt' + i.toString())
// 弹框4:奶茶详情
if (this.showDetailModal) {
Column() {
this.teaDetailModalBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('99000000')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showDetailModal = false;
})
}
// 弹框1:发布打卡
if (this.showCheckModal) {
Column() {
this.checkModalBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('99000000')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showCheckModal = false;
})
}
// 弹框2:创建配方
if (this.showRecipeModal) {
Column() {
this.recipeModalBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('99000000')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showRecipeModal = false;
})
}
// 弹框3:删除打卡
if (this.showDeleteModal) {
Column() {
this.deleteModalBuilder()
}
.width('100%')
.height('100%')
.backgroundColor('99000000')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showDeleteModal = false;
})
}
}
.width('100%')
.height('100%')
.backgroundColor(PALETTE.bg)
}
}
将 1096.ets 放入 HarmonyOS 项目的 entry/src/main/ets/pages/ 目录下,在 main_pages.json 中注册路由,即可在 DevEco Studio 中预览运行。
十五、开发环境搭建:DevEco Studio + API 24
本应用基于 HarmonyOS NEXT(API 24)开发,使用 ArkTS 声明式 UI 框架。以下是完整的开发环境搭建步骤。
15.1 安装 DevEco Studio
从华为开发者联盟官网下载 DevEco Studio 安装包,双击运行安装程序。安装欢迎界面如下,点击「Next」继续。

15.2 选择安装位置
在安装路径选择界面,建议选择空间充足的磁盘(建议剩余空间 20GB 以上),避免使用中文路径或含空格的路径。点击「Next」继续。

15.3 配置安装选项
在安装选项配置界面,勾选「Create Desktop Shortcut」(创建桌面快捷方式)、「Add to PATH」(添加到环境变量)等选项,方便后续命令行调用。点击「Next」继续。

15.4 新建项目:选择 Empty Ability 模板
安装完成后启动 DevEco Studio,点击「Create Project」新建项目。在模板选择界面,选择「Empty Ability」(空 Ability)模板,这是最基础的 ArkTS 应用模板,适合从零构建自定义 UI。点击「Next」继续。

15.5 配置项目信息:API 24
在项目配置界面,填写项目名称(如 QQMilkTea)、包名(如 com.example.qqmilktea),Compatible SDK 选择 6.1.1(24)(即 API 24),Save location 选择项目保存路径。确认信息无误后点击「Finish」创建项目。

15.6 导入源码与运行
项目创建完成后,将 1096.ets 文件复制到 entry/src/main/ets/pages/ 目录下,打开 entry/src/main/resources/base/profile/main_pages.json,在 src 数组中添加 "pages/1096" 路由注册。
在 DevEco Studio 顶部工具栏选择 Previewer(预览器)或连接真机/模拟器,点击运行按钮即可看到「QQ奶茶·茶友圈」应用的完整运行效果。可以切换底部 4 Tab、顶部 6 Tab,点击奶茶卡片查看详情、在打卡页发布打卡、在配方页创建自研配方、在打卡动态中删除打卡,体验完整的奶茶社交闭环。
十六、总结
「QQ奶茶·茶友圈」是一个以奶茶为主题的完整移动端社区应用,基于 HarmonyOS NEXT(API 24)ArkTS 声明式 UI 框架构建,单文件 1096.ets 约 1700 行代码实现了全部功能。
应用的核心亮点包括:
- 奶茶棕 + 珍珠黑 + 蜜桃粉甜美奶茶风视觉体系:10 色基础调色板 + catColor 6 色奶茶类型映射(新品焦糖金/经典奶茶棕/果茶草莓粉/咖啡深咖啡棕/联名玫瑰棕/隐藏珍珠黑)+ sugarColor 3 色糖度映射(低糖草绿/中糖焦糖金/高糖珊瑚红)+ heatLabel 3 档热量文案(轻负担/小放纵/快乐核弹),通过色彩快速区分奶茶类型与健康度。头部使用 angle:135 三色渐变
#8A5A3B→#B07B55→#D9A876(从深奶茶棕到中奶茶棕到浅焦糖金,模拟奶茶从杯底到杯面的渐变层次,是所有应用中唯一三色渐变头部)+ 🔍💌🛒三图标 + 购物车 12 角标(蜜桃粉底)+ 搜索条 + 茶友榜按钮,完整模拟电商/奶茶 App 的头部交互体验。底部 Tab 纯白底 + 顶部圆角 18(与头部底部圆角呼应,奶茶杯视觉隐喻)。 - 双层导航 + 6 大奶茶差异化布局:底部 4 Tab(奶茶/打卡/配方/我的)+ 顶部 6 Tab(新品/经典/果茶/咖啡/联名/隐藏),6 个奶茶类型采用完全不同的布局风格(秋日 banner+双列瀑布流左右列高度错落/横滑大卡奶茶棕底+周销量柱状图+热销 TOP5/配方进化时间轴蜜桃粉竖线/三列格子+打工人咖啡心得从社区筛选/IP 对决卡草莓粉 VS 天蓝+列表+侧栏统计杯套收藏率/标签云加料+糖度+紧凑列表+暗号玩法老规矩),是奶茶差异化设计的典范。顶部 Tab 激活态蜜桃粉圆角下划线 + 非激活奶油色圆角下划线(双下划线对比),是独特的视觉细节。
- 14 粒子最快速度水平向右飘动动画:🧋🥤🍡✨🧁 五图标循环,每个粒子拥有独立 speed 0.6~2.0(所有应用中最快),水平向右飘动(x+speed,x>360 回 -20 循环,模拟奶茶杯在传送带上向右移动、奶茶店出杯口一杯接一杯送出的场景,与垂直飘动的常规粒子形成鲜明反差,是所有应用中唯一采用水平飘动粒子的应用)+ 左侧循环,x 用 px 而非百分比,y 固定 5 行分布
80+(i%5)*118,setInterval 80ms(所有应用中最快刷新频率) 驱动(配合最快速度模拟奶茶店出杯的快节奏与奶茶爱好者的快乐氛围),this.particles = this.particles.slice()通过 slice() 创建新引用触发状态更新,aboutToAppear/aboutToDisappear 生命周期管理。ParticleItem 无 id/y 字段(水平飘动专用),是本应用粒子系统最具特色的设计决策。 - 双数据可视化 + 配方进化时间轴:经典 Tab 内嵌周销量柱状图(7 天,salesBarHeight 换算高度 v/132*96,周六 idx===5 蜜桃粉
#F5A3B7高亮其他奶茶棕#C98A5B,周六 132 最高形成「周六是奶茶快乐日」可视化,底部文字「周六是奶茶快乐日,点单量是周四的3倍」)+ 果茶 Tab 配方进化时间轴(3 款果茶,左侧 emoji+蜜桃粉竖线连接,第 2 代多肉葡萄蜜桃粉底高亮,第 N 代配方标签+sugarColor 糖度标签+heatLabel 热量标签),双数据可视化是本应用的特色。 - 4 个模态弹窗闭环:发布打卡(粉棕渐变头,选奶茶/甜度/心情/杯数步进器 1-9 杯 >=3 杯给出「⚠️茶友注意糖分摄入」警告/感想 TextInput)、创建配方(奶油头,是所有弹窗中唯一用浅色头的,配方名/茶底/加料多选 pickedAdds 布尔数组/难度/提示「优质配方会被收录进配方库首页」)、删除打卡(🗑️小卡,「手滑了」轻松文案替代取消/确认删除珊瑚红)、奶茶详情(棕渐变头,四列统计到手价珊瑚红/热量奶茶棕+heatLabel(352)写死/建议糖度 sugarColor/茶友评分玫瑰棕,品鉴笔记+茶友实录 3 条充满奶茶圈梗,点单口诀奶油色底+加入心愿单玫瑰棕底),覆盖新增/新增/删除/查询四态。
- 强类型数据模型 + 特色字段 + 工具函数 + 筛选函数三层架构:8 个 interface 约束所有数据结构,TeaItem 含
eng(英文名)、heat(热量 kcal)、sugar(建议糖度)、sales(周销量)四个特色字段,RecipeItem 含steps(步骤字符串数组带圆形序号渲染),CheckItem 含mood(心情),18 奶茶 + 8 打卡 + 5 配方 + 6 门店 + 3 点评的丰富预置数据,6 个工具函数(catColor/catLabel/heatLabel/sugarColor/salesBarHeight/badgeCount,所有应用中最多)负责视觉映射,9 个筛选函数(newLeftTeas/newRightTeas/classicTeas/fruitTeas/coffeeTeas/collabTeas/hiddenTeas/hotTeas,所有应用中最多,直接数组索引而非 filter)负责数据分发,detailTea 系列 13 个查找方法(所有应用中最多)实现详情弹窗的数据驱动。三层架构让模板简洁、逻辑清晰。 - 奶茶圈文化梗贯穿全应用:快乐吨吨吨/快乐核弹/手滑了/老规矩暗号/百杯成就/深夜茶鬼/隐藏菜单发现者/联名全收集/打卡狂魔/省钱大师/全糖去冰战士/半糖主义小K/低卡求生指南/珍珠口感鉴定师/年度奶茶账单/杯套收藏率/第二杯半价/芋泥啵啵杀手/厚乳提拉米苏杯,让应用充满奶茶社区的文化认同感。「老规矩」暗号玩法、「手滑了」删除按钮、「快乐核弹」热量标签、年度奶茶账单 ¥3,842 等梗只有真正的奶茶爱好者才能会心一笑,是本应用的文化深度体现。

对于希望学习 HarmonyOS ArkTS 声明式 UI 开发的读者,本应用提供了一个完整的参考范本:如何通过 @State 驱动状态更新、如何用 @Builder 拆分复杂布局、如何用 Stack 分层实现粒子与弹窗、如何用 setInterval + 生命周期实现动画(特别是如何通过方向反转——从垂直飘动改为水平飘动——模拟不同场景的运动意象,奶茶杯传送带的水平飘动与星空的静谧垂直飘动形成鲜明对比)、如何用 slice() 创建新引用触发数组状态更新、如何用 interface + 特色字段 + 常量数组 + 工具函数 + 筛选函数构建分层数据层、如何通过周销量柱状图 + 配方进化时间轴实现双数据可视化、如何通过三色渐变头部 + 顶部圆角底部 Tab 营造甜美奶茶风、如何通过 6 Tab 差异化布局展示不同奶茶类型、如何通过 pickedAdds 布尔数组实现弹窗多选、如何通过杯数步进器 + 糖分警告实现健康化设计。将源码导入 DevEco Studio(API 24)即可直接运行预览,在此基础上可以扩展更多奶茶数据、接入真实门店 API 与排队数据、增加奶茶外卖下单功能、增加配方点赞与评论互动、增加茶友等级与成就系统、增加奶茶热量计算器与健康建议,打造一个真正可用的奶茶社交平台。
一杯入魂,半糖去冰。愿每一个奶茶爱好者,都能在桂花酒酿的秋日气息里找到属于自己的那杯快乐,在多肉葡萄的爆汁口感里治愈一天的疲惫,在隐藏菜单的暗号玩法里收获老茶客的默契,也能在代码与奶茶之间找到属于自己的那份甜美与醇厚。今天也要快乐吨吨吨。
标签: HarmonyOS · ArkTS · API 24
更多推荐



所有评论(0)