HarmonyOS 6.1 实战:组件回调属性为空,ArkTS 空安全避坑指南
30 款复古零食 · 8 条怀旧记忆 · 6 条社区帖子 · 5 Tab + 2 弹框 · 复古红 × 怀旧金
HarmonyOS ArkTS 声明式 UI 单文件实现,复古零食电商 + 怀旧记忆社区
引言:当五毛钱买回整个童年
校门口的小卖部,是每个 80 后 90 后共同的记忆坐标。攥着皱巴巴的五毛钱,在琳琅满目的零食架前纠结三分钟——是买一包辣条还是大大卷?是来一根冰棍还是一瓶北冰洋?那声"砰"的爆米花巨响,那根能拉很长的大大卷,那颗模仿电视剧仙丹的麦丽素……这些味道,构成了我们童年最甜蜜的底色。
时光小卖部,就是这样一个把童年味道装进手机里的项目。它不是又一个普通的零食电商 App,而是一个复古零食记忆馆——用复古红 #C62828 和怀旧金 #FFC107 包裹着 30 款童年零食,用 8 条怀旧记忆带你穿越回那个夏天,用 6 条社区帖子让你和同代人共鸣"原来你也吃过这个"。
本文将从设计理念、色彩系统、架构设计、数据模型、五大 Tab 页面、弹框组件、属性动画、业务模块、性能优化等维度,逐层拆解这个 1336 行的 ArkTS 单文件项目。
第一章 项目概述与设计理念
1.1 项目定位
时光小卖部是一款面向 80 后 90 后怀旧人群的复古零食电商 + 记忆社区应用,核心功能围绕三个关键词展开:
- 逛:浏览 30 款童年零食,从大白兔奶糖到辣条,从北冰洋汽水到麦丽素,每一款都是记忆符号
- 忆:阅读 8 条怀旧记忆,校门口的小卖部、第一次吃麦丽素、冰棍的夏天……每一条都能唤起共鸣
- 买:加入购物车、结算下单,把童年味道真正买回家
与普通零食电商不同,时光小卖部的目标用户是有怀旧情结、愿意为记忆付费、喜欢分享童年故事的人群。因此界面设计刻意避开了现代电商的高冷极简风,转而采用温暖、热闹、有烟火气的复古港风——米白背景、复古红主色、怀旧金点缀,每一个角落都在提醒你"这是童年的味道"。
1.2 设计理念:小卖部即时光机
整个应用的视觉语言可以概括为"小卖部即时光机":
- 复古配色:复古红 #C62828 是老招牌的颜色,怀旧金 #FFC107 是旧奖状的颜色,米白 #FDF5E6 是泛黄信纸的颜色——三种颜色组合在一起,瞬间把人拉回那个年代
- emoji 即商品图:没有精美的商品摄影,只用 🐰🦐🌀❄️🌶️🥤 这些 emoji 代表零食——简单、可爱、有手绘感,就像小时候小卖部里手写的价格牌
- 卡片即货架:每个零食卡片都是一个小货架,HOT/NEW 标签像小卖部里的"热销"小旗子,分类色标像不同口味的糖纸
- 记忆即故事:怀旧墙的每条记忆都有年代标签(1998、2002、1995……),像一本翻旧的相册,按时间轴排列
- 社区即共鸣:怀旧社区的帖子都是"还记得胡同里的冰棍车吗"、"集齐水浒卡的那天"这种集体记忆,让用户在评论区找到同代人
1.3 核心功能清单
| 模块 | 功能 | 数据量 |
|---|---|---|
| 首页 | 今日推荐 banner、热门零食横滑、小卖部故事 | 4 款热门零食 |
| 零食铺 | 分类筛选、双列宫格、加入购物车弹框 | 30 款零食 / 6 分类 |
| 怀旧墙 | 时光记忆卡片、怀旧社区帖子 | 8 条记忆 / 6 条帖子 |
| 购物车 | 商品清单、数量加减、删除、合计、结算弹框 | 3 件初始商品 |
| 我的 | 个人资料、积分收藏、成就墙、订单记录 | 3 枚成就徽章 |
1.4 技术栈与约束
- 框架:HarmonyOS ArkTS 声明式 UI
- 架构:单文件实现,@Entry 主入口 + @Component 子组件
- 状态管理:@State 组件内状态,布尔变量控制弹框显隐
- 动画:属性动画(.animation),Tab 切换图标缩放
- 布局:Column/Row/Stack/Flex/Scroll 组合,FlexWrap 实现双列宫格
- 约束:禁止使用对象展开(…),全部显式声明属性;数组展开([…list, newItem])是允许的
第二章 色彩系统与视觉风格
2.1 配色方案:复古港风
时光小卖部的配色灵感来自 80 年代 90 年代的港式老招牌和旧奖状,整体色调温暖、怀旧、有烟火气:
| 角色 | 色值 | 用途 |
|---|---|---|
| 背景主色 | #FDF5E6 | 页面背景(米白/旧信纸色) |
| 复古红(主色) | #C62828 | 主按钮、价格、Tab 激活、标题 |
| 复古红(浅) | #E53935 | 强调色、悬浮态 |
| 卡片背景 | #FFFFFF | 内容卡片、头部、底部 Tab |
| 深棕 | #5D4037 | 深色文字、边框 |
| 主文本 | #3E2723 | 标题、正文(深棕黑) |
| 次文本 | #8D6E63 | 说明、时间、占位符(棕灰) |
| 浅橙标签 | #FFE0B2 | 分类标签底、chip 背景 |
| 警告橙 | #FF7043 | HOT 标签、点赞数 |
| 白色 | #FFFFFF | 卡片、按钮文字 |
| 绿色 | #2E7D32 | NEW 标签、下单成功 |
| 怀旧金 | #FFC107 | 强调、VIP、成就 |
2.2 分类颜色编码系统
项目中最有特色的视觉语言是零食分类颜色编码,由 categoryColor() 纯函数实现:
function categoryColor(cat: string): string {
if (cat === '糖果') return '#E91E63' // 粉红
if (cat === '饼干') return '#FF9800' // 橙色
if (cat === '汽水') return '#03A9F4' // 蓝色
if (cat === '辣条') return '#F44336' // 红色
if (cat === '糕饼') return '#9C27B0' // 紫色
return '#607D8B' // 灰蓝(默认)
}
这套编码贯穿零食铺页面:每个零食卡片右上角都有一个分类色标(小方块,背景色即分类色),用户一眼就能通过颜色判断零食类型——粉红是糖果、橙色是饼干、蓝色是汽水、红色是辣条、紫色是糕饼。这就像小时候小卖部里不同口味的糖纸,颜色就是分类。
2.3 复古风格细节
- emoji 商品图:所有零食都用 emoji 代表(🐰大白兔、🦐咪咪虾条、🌀大大卷、❄️旺旺雪饼、🌶️辣条、🥤北冰洋),没有精美的商品摄影,简单可爱有手绘感
- HOT/NEW 标签:热销零食标 HOT(警告橙底白字),新品标 NEW(绿底白字),像小卖部里手写的"热销"小旗子
- 原价删除线:每个零食都有原价(
originalPrice),用TextDecorationType.LineThrough删除线展示,突出优惠价,像老报纸上的促销广告 - 年代标签:怀旧墙每条记忆都有"年代 1998"、"年代 2002"标签,像旧相册的日期戳
- 点赞红心:记忆和帖子的点赞都用 ❤️ 红心 + 警告橙数字,像旧信纸上的红印章
- VIP 标签:我的页面 VIP 会员用复古红文字 + 浅橙背景,像老会员卡的烫金印章
2.4 平板横屏适配
从截图可以看到,应用在平板横屏(2560×1600)下也能良好展示。由于使用了 layoutWeight(1) 和百分比宽度(如零食卡片 width('48.5%')),页面内容会自动拉伸填充,双列宫格在大屏上变成更宽的两列,信息密度舒适。底部 Tab 栏均匀分布,顶部搜索框自适应宽度。


第三章 项目架构与文件组织
3.1 单文件架构
整个项目采用单文件架构,所有代码集中在 2024.ets 中(用户标注),共 1336 行。这种架构适合中小型项目,优点是:
- 无需处理模块间导入导出,开发效率高
- 全局常量和工具函数可直接访问
- 状态传递简单,通过 @State 和回调函数即可
- 便于整体阅读和调试
3.2 代码组织层次
文件内部按功能划分为清晰的层次结构:
├── 类型定义(5 个 interface)
│ ├── SnackItem 零食数据模型
│ ├── CartItem 购物车数据模型
│ ├── MemoryItem 怀旧记忆数据模型
│ ├── NostalgiaPost 社区帖子数据模型
│ └── Pal 调色板接口
│
├── 调色板(1 个常量)
│ └── P 复古港风配色(13 个色值)
│
├── 静态选项(2 个常量)
│ ├── CATEGORIES 6 个零食分类
│ └── CART_TABS 4 个订单状态
│
├── 纯函数(11 个)
│ ├── categoryColor 分类→颜色映射
│ ├── snackByCategory 按分类筛选零食
│ ├── moneyFormat 价格格式化(1位小数)
│ ├── cartTotal 购物车总价计算
│ ├── cartCount 购物车总件数计算
│ ├── withCartQty 更新购物车商品数量
│ ├── addToCart 添加商品到购物车
│ ├── removeFromCart 从购物车移除商品
│ ├── withMemoryLiked 记忆点赞+1
│ ├── ratingStars 评分星级生成
│ └── hotColor HOT标签颜色
│
├── 数据(3 组常量)
│ ├── SNACKS 30 款复古零食
│ ├── MEMORIES 8 条怀旧记忆
│ └── NOSTALGIA_POSTS 6 条社区帖子
│
├── Tab 枚举
│ └── ShopTab 5 个 Tab(HOME/SNACKS/MEMORY/CART/MINE)
│
├── 入口组件
│ └── RetroSnackShop @Entry,Tab 切换 + 底部导航
│
├── 公共组件
│ └── ShopHeader 复古小卖部头部(logo+搜索+标签)
│
└── Tab 页面组件(5 个)
├── HomeContent 首页·推荐+故事
├── SnackListContent 零食铺·分类+宫格+购买弹框
├── MemoryContent 怀旧墙·记忆+社区
├── CartContent 购物车·清单+结算弹框
└── MineContent 我的·资料+成就+订单
3.3 组件间通信模式
项目采用单向数据流 + 不可变状态更新的通信模式:
- 父→子:通过组件属性传递数据(本项目子组件较少,主要是头部组件)
- 状态更新:所有状态更新都通过纯函数返回新数组,而非直接修改原数组
this.cartItems = withCartQty(this.cartItems, c.id, c.qty - 1)this.cartItems = addToCart(this.cartItems, s)this.cartItems = removeFromCart(this.cartItems, c.id)
- 弹框数据:通过 @State 变量传递,如
buySnack: SnackItem、buyQty: number - 不可变约束:代码注释明确标注"已修正:禁止使用对象展开,全部显式声明属性",但数组展开
[...list, newItem]是允许的
3.4 弹框管理模式
2 个弹框全部采用集中式布尔变量管理:
- 零食铺:
showBuy: boolean控制加入购物车弹框 - 购物车:
showCheckout: boolean控制结算成功弹框 - 弹框通过
if (this.showXxx) { this.xxxModal() }条件渲染 - 弹框内部使用
Column全屏布局:上半部分透明遮罩(点击关闭)+ 下半部分内容卡片 - 加入购物车弹框是底部抽屉式(顶部圆角 20),结算成功弹框是居中卡片式
第四章 数据模型与全局状态管理
4.1 核心数据模型
SnackItem(零食)
零食是整个应用的核心实体,包含 10 个字段:
interface SnackItem {
id: number; name: string; brand: string; category: string
price: number; originalPrice: number; icon: string; desc: string
isHot: boolean; isNew: boolean
}
- 分类体系:糖果、饼干、汽水、辣条、糕饼(还有数据中的"零食"、“饮料”、"冷饮"等,但筛选器只列了 6 类)
- 价格体系:
price现价 +originalPrice原价,原价用删除线展示,突出优惠 - 标签体系:
isHot热销(HOT 标签,警告橙)+isNew新品(NEW 标签,绿色) - 描述:每款零食都有一句怀旧描述,如大白兔"经典原味,奶香浓郁,童年的甜蜜回忆",辣条"麻辣鲜香,筋道Q弹,小时候的’奢侈零食’"
30 款零食覆盖了童年记忆的方方面面:
- 糖果类:大白兔奶糖、大大卷泡泡糖、麦丽素、猪油糖、大大泡泡糖(口哨型)、水晶之恋果冻、泡泡糖(西瓜味)
- 零食类:咪咪虾条、果丹皮、小浣熊干脆面、无花果丝、深井烧鹅干脆面、老北京方便面、烤鱼片、山楂片、老式爆米花
- 汽饮类:北冰洋汽水、健力宝、AD钙奶、大白梨汽水、酸梅汤粉、红豆冰棍
- 辣条类:辣条(大面筋)、五毛辣片、麻辣王子
- 糕饼类:旺旺雪饼、老式桃酥、鸡蛋小饼干、绿豆糕、老式鸡蛋糕
CartItem(购物车)
interface CartItem {
id: number; name: string; price: number; icon: string; qty: number
}
购物车项只保留必要字段(从 SnackItem 精简而来),初始有 3 件商品:大白兔奶糖 x2、咪咪虾条 x10、麦丽素 x1。
MemoryItem(怀旧记忆)
interface MemoryItem {
id: number; title: string; year: string; content: string; icon: string; likes: number
}
8 条怀旧记忆,每条都有年代标签(1992-2010),像一本按时间排列的旧相册:
- 校门口的小卖部(1998,986 赞)
- 第一次吃麦丽素(2002,802 赞)
- 冰棍的夏天(1995,1203 赞)
- 北冰洋与聚餐(2005,654 赞)
- AD钙奶的童年(2008,927 赞)
- 干脆面集卡热(2000,1350 赞)
- 大白兔奶糖的爱(1992,746 赞)
- 麻辣烫里的辣条(2010,503 赞)
NostalgiaPost(社区帖子)
interface NostalgiaPost {
id: number; user: string; avatar: string; title: string; content: string; likes: number; time: string
}
6 条社区帖子,用户都是有怀旧感的昵称(小卖部老板、老北京人、爱啃干脆面的娃、北冰洋铁粉、00后回忆党、记忆收藏家),内容都是集体记忆共鸣。
Pal(调色板)
interface Pal {
bg: string; primary: string; primaryLight: string; card: string; deep: string
text: string; textSub: string; chip: string; warn: string; white: string
green: string; yellow: string
}
调色板接口定义了 13 个颜色角色,由常量 P 实现,全局统一引用,确保配色一致性。
4.2 数据实现方式:对象字面量
与 AUDIO LAB 项目使用 class implements interface 不同,时光小卖部的所有 Mock 数据都直接使用对象字面量:
const SNACKS: SnackItem[] = [
{ id: 1, name: '大白兔奶糖', brand: '冠生园', category: '糖果', price: 8.8, ... },
{ id: 2, name: '咪咪虾条', brand: '福建', category: '零食', price: 1.5, ... },
// ...
]
这种方式更简洁,适合数据结构简单、字段较少的场景。代码注释明确标注"已修正:禁止使用对象展开,全部显式声明属性",即在 addToCart 等函数中返回新对象时必须显式写出所有属性,不能用 {...c, qty: c.qty + 1} 这种展开语法。
4.3 全局状态管理
项目没有使用全局状态管理库,所有状态都是组件级 @State:
- RetroSnackShop:
activeTab: number—— 当前激活的 Tab - HomeContent:
selectedBanner: number—— 当前选中的 banner(未实际使用) - SnackListContent:
snacks: SnackItem[]+curCat: string+showBuy: boolean+buySnack: SnackItem+buyQty: number - MemoryContent:
memories: MemoryItem[]+posts: NostalgiaPost[](数据直接赋值,未做交互) - CartContent:
cartItems: CartItem[]+showCheckout: boolean - MineContent:
showOrders: boolean+currentOrderTab: number(未实际使用)
购物车状态是项目中最复杂的状态管理,通过 4 个纯函数实现不可变更新:
addToCart(list, item):添加商品,已存在则数量+1,不存在则新增withCartQty(list, id, qty):更新指定商品数量(1-99 范围限制)removeFromCart(list, id):移除指定商品cartTotal(list)/cartCount(list):计算总价和总件数(纯计算,不修改状态)
4.4 工具函数层
11 个纯函数构成了项目的工具层,分为四类:
- 分类与颜色:
categoryColor(cat)、hotColor(isHot)、snackByCategory(list, cat) - 价格格式化:
moneyFormat(n)—— 保留 1 位小数(toFixed(1)) - 购物车操作:
cartTotal(list)、cartCount(list)、withCartQty(list, id, qty)、addToCart(list, item)、removeFromCart(list, id) - 其他:
withMemoryLiked(list, id)—— 记忆点赞+1(未在 UI 中调用)、ratingStars(r)—— 生成星级字符串(未在 UI 中调用)
这些函数都是无副作用的纯函数,输入相同则输出相同,便于测试和复用。其中 ratingStars 和 withMemoryLiked 虽然定义了但未在 UI 中调用,是预留的扩展功能。
第五章 五大 Tab 页面深度解析
5.1 Tab1:首页·推荐专区(HomeContent)
首页是整个应用的入口,采用纵向滚动信息流布局,从上到下依次为:
今日推荐 Banner
- 标题栏:
🎁 今日推荐+查看更多 > - 两张 Banner 卡片,复用
bannerCard@Builder:- 童年回忆礼包:复古红 #C62828 背景,“大白兔+辣条+麦丽素”,🍫 图标
- 夏日汽水节:蓝色 #0277BD 背景,“北冰洋/健力宝 冰爽价”,🥤 图标
- 每张 Banner 高 100vp,圆角 16,左侧文字(标题+副标题)+ 右侧大图标
热门怀旧零食横滑
- 标题:
🔥 热门怀旧零食 - 横向 Scroll,展示 4 款热门零食(SNACKS[0-3]:大白兔奶糖、咪咪虾条、大大卷泡泡糖、旺旺雪饼)
- 每款:emoji 图标(30px)+ 零食名(11px 粗体)+ 价格(13px 复古红粗体)
- 卡片宽 80vp,白底圆角 10,居中对齐
小卖部故事
- 标题:
🏆 小卖部故事 - 故事卡片:白底圆角 12,包含
- 人物行:🧑🏫 头像 + “老式爆米花大爷” + “'砰’的一声,是童年最响亮的声音”
- 故事正文:“那时候街头的大铁炉爆米花,是孩子们的期待。一声巨响,满街飘香,那是属于80后90后的集体记忆。”
首页设计特点
首页没有复杂的交互,主要是内容展示和氛围营造——用 Banner、横滑零食、故事卡片三个模块,快速让用户进入"童年怀旧"的情绪状态,然后引导用户点击底部 Tab 进入零食铺购买或怀旧墙浏览。

5.2 Tab2:零食铺·分类宫格(SnackListContent)
零食铺是核心电商页面,采用分类筛选 + 双列宫格 + 购买弹框的结构:
分类筛选横滑
- 标题:
🍫 怀旧零食铺 - 6 个分类 chip:全部、糖果、饼干、汽水、辣条、糕饼
- 选中态:复古红底白字;未选中:浅橙 #FFE0B2 底深棕字
- 点击切换
curCat,通过snackByCategory(SNACKS, label)筛选零食列表
双列零食宫格
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween })实现双列流式布局- 每卡宽度
'48.5%',白底圆角 12,padding 11 - 每张卡片包含:
- 顶栏:emoji 图标(28px)+ HOT/NEW 标签(右上角,垂直排列)+ 分类色标(最右侧,小方块)
- 零食名:13px 中等粗细,1 行省略
- 描述:9px 棕灰,1 行省略
- 分割线:浅橙 #FFE0B2
- 价格栏:现价(15px 复古红粗体)+ 原价删除线(9px 棕灰)+ 🛒 购物车按钮(右侧)
- 点击 🛒 打开加入购物车弹框,传递
buySnack = s和buyQty = 1
零食铺设计特点
零食铺是整个应用信息密度最高的页面,30 款零食通过双列宫格展示,分类筛选快速定位,HOT/NEW 标签和分类色标提供视觉层次,价格对比(现价+原价删除线)刺激购买欲望。

5.3 Tab3:怀旧墙·时光记忆(MemoryContent)
怀旧墙是情感核心页面,采用时光记忆卡片 + 怀旧社区帖子的双层结构:
时光怀旧墙
- 标题:
📼 时光怀旧墙 - 8 条记忆卡片,每条:
- 顶栏:emoji 图标(24px)+ 标题(13px 粗体)+ “年代 XXXX”(9px 棕灰)+ ❤️ 点赞数(10px 警告橙)
- 正文:11px 棕灰,行高 18,完整展示
- 卡片白底圆角 12,宽度 94%,margin top 10
- 记忆按时间排列:1992→1995→1998→2000→2002→2005→2008→2010,像一本翻旧的相册
怀旧社区
- 标题:
💬 怀旧社区 - 6 条帖子,每条:
- 顶栏:头像(emoji + 浅橙背景圆角 18,38×38)+ 用户名(12px 中等)+ 时间(10px 棕灰)+ ❤️ 点赞数
- 标题:14px 粗体深棕
- 正文:11px 棕灰,行高 18
- 卡片白底圆角 14,宽度 94%
- 底部:
— 更多回忆等你补充 —居中提示
怀旧墙设计特点
怀旧墙是整个应用最有温度的页面,8 条记忆和 6 条帖子都是精心编写的集体记忆——“校门口的小卖部”、“第一次吃麦丽素”、“冰棍的夏天”、“干脆面集卡热”……每一条都能让目标用户会心一笑,唤起共同的童年记忆。

5.4 Tab4:购物车·清单结算(CartContent)
购物车是交易闭环页面,采用商品清单 + 底部结算栏 + 结算弹框的结构:
购物车清单
- 标题栏:
🛒 我的购物车+共 X 件(右侧,cartCount动态计算) - 商品列表(Scroll),每行:
- emoji 图标(24px,40vp 宽居中)
- 商品名(13px 中等)+ 单价(11px 复古红)
- 数量控制:− 按钮(浅橙底)+ 数量(13px 粗体)+ + 按钮(复古红底白字)
- 🗑️ 删除按钮(右侧)
- 数量加减通过
withCartQty纯函数更新,删除通过removeFromCart - 空购物车提示:
— 购物车空空如也 —
底部结算栏
- 白底顶部圆角 16,固定在底部
- 合计行:
合计(12px 棕灰)+¥XX.X(18px 复古红粗体,cartTotal动态计算) 去结算按钮:15px 白字,复古红底,圆角 20,padding 左右 60- 点击打开结算成功弹框
购物车设计特点
购物车页面实现了完整的电商购物车逻辑:添加、增减数量、删除、合计计算、结算。初始有 3 件商品(大白兔 x2、咪咪虾条 x10、麦丽素 x1),合计 ¥42.5,用户可以直接体验完整的购买流程。

5.5 Tab5:我的·个人中心(MineContent)
我的页面是用户数据和成就展示页,采用资料卡 + 成就墙 + 订单记录的结构:
个人资料卡
- 白底圆角 14,宽度 94%
- 左侧:头像(🧑🎤 emoji + 浅橙背景圆角 30,60×60)
- 中间:用户名"怀旧收藏家"(16px 粗体)+ “LV.6 零食鉴赏师 · 365天”(10px 棕灰)+ VIP 会员标签(复古红字 + 浅橙背景,“全场88折”)
- 右侧:积分 3280(复古红粗体)+ 收藏 12(绿色粗体),各带小字标签
怀旧成就墙
- 标题:
🏅 怀旧成就墙 - 副标题:
📼 怀旧口味鉴赏家 · 解锁全系列 - 3 枚成就徽章横排:
- 🎖️ 辣条王者(复古红字 + 浅橙底)
- 🍫 糖果达人(粉红字 + 粉底)
- 🥤 汽水收藏家(蓝字 + 浅蓝底)
我的订单
- 标题栏:
📦 我的订单+查看全部 >(右侧复古红) - 4 个订单状态 tab:全部、待付款、待收货、已完成
- 选中态:复古红文字 + 1px 复古红边框;未选中:棕灰文字 + 透明边框
- 点击切换
currentOrderTab(当前仅 UI 切换,无实际订单数据)
底部标语
— 时光小卖部 · 童年不散场 —居中棕灰小字
我的页面设计特点
我的页面用 LV.6 零食鉴赏师、3280 积分、3 枚成就徽章构建了一个"怀旧收藏家"的用户画像,VIP 全场 88 折标签增加了会员感,订单记录提供了电商功能的完整性。

第六章 弹框组件设计
时光小卖部共包含 2 个弹框,分别在零食铺和购物车页面。
6.1 弹框设计模式
遮罩层
两个弹框都使用 rgba(0,0,0,0.6) 半透明黑色遮罩,zIndex(999) 确保在最上层。
定位方式
| 弹框 | 定位方式 | 特点 |
|---|---|---|
| 加入购物车(buyModal) | 底部抽屉 | 上半部分透明遮罩(点击关闭)+ 下半部分内容卡片(顶部圆角 20) |
| 结算成功(checkoutModal) | 居中卡片 | 上下透明遮罩(点击关闭)+ 中间内容卡片(圆角 18,宽度 78%) |
6.2 弹框 A:加入购物车(buyModal)
零食铺页面点击 🛒 按钮触发,底部抽屉式:
- 标题栏:
🛒 加入购物车(16px 粗体)+ ✕ 关闭按钮(右侧) - 商品信息:emoji 图标(44px + 浅橙背景圆角 14 + padding 14)+ 商品名(15px 粗体)+ 描述(10px 棕灰)+ 价格(15px 复古红粗体)
- 数量选择:
数量标签 + − 按钮(浅橙底 30×30)+X 件(13px)+ + 按钮(复古红底白字 30×30)- 数量范围 1-20,− 按钮在数量 >1 时才减
- 加入购物车按钮:15px 白字,复古红底,圆角 20,padding 左右 60
- 点击关闭弹框(当前未实际调用
addToCart,是预留的扩展点)
- 点击关闭弹框(当前未实际调用
6.3 弹框 B:结算成功(checkoutModal)
购物车页面点击"去结算"触发,居中卡片式:
- 成功标识:
✅ 下单成功(16px 粗体绿色) - 提示文案:
你的怀旧零食正在打包中!(13px 深棕) - 实付金额:
实付金额 ¥XX.X(15px 复古红粗体,cartTotal动态计算) - 操作按钮:
再看看:12px 棕灰,1px 棕灰边框,圆角 16,点击关闭弹框完成:12px 白字,复古红底,圆角 16,点击关闭弹框并清空购物车(this.cartItems = [])

第七章 属性动画与交互体验
时光小卖部的动画设计遵循简洁、温暖、不打扰的原则,只有 1 处属性动画。
7.1 底部 Tab 切换动画
Text(icon)
.fontSize(20)
.scale({
x: this.activeTab === tab ? 1.15 : 1,
y: this.activeTab === tab ? 1.15 : 1
})
.animation({ duration: 200, curve: Curve.EaseOut })
- 激活 Tab:图标放大 1.15 倍 + 标签复古红 + 底部 2px 复古红指示条(宽 16,圆角 1)
- 未激活 Tab:图标原始大小 + 标签棕灰 + 透明指示条
- 动画时长 200ms,EaseOut 曲线,自然弹出后减速
- 5 个 Tab 均匀分布,
layoutWeight(1)
7.2 无动画的交互
项目中大部分交互没有使用动画,这是刻意的设计选择:
- 弹框显隐:直接条件渲染,无淡入淡出/滑动动画
- 分类筛选:直接切换,无过渡动画
- 数量加减:直接更新数字,无动效
- 列表滚动:使用系统默认 Scroll 行为
这种"少即是多"的动画策略符合复古小卖部的质朴气质——没有花哨的特效,只有实实在在的内容和交互。
7.3 交互反馈的其他方式
除了属性动画,项目还通过以下方式提供交互反馈:
- 颜色变化:选中态使用复古红填充(分类 chip、订单 tab),未选中使用浅橙/棕灰
- 标签变化:HOT(警告橙)、NEW(绿色)、VIP(复古红+浅橙)等标签提供状态信息
- 价格对比:现价复古红粗体 + 原价删除线,视觉上突出优惠
- 指示条:底部 Tab 激活时有 2px 复古红指示条
第八章 复古零食电商系统
8.1 零食分类体系
30 款零食覆盖了童年记忆的全品类,筛选器列出 6 个主分类:
| 分类 | 颜色 | 代表零食 |
|---|---|---|
| 糖果 | 粉红 #E91E63 | 大白兔奶糖、大大卷、麦丽素、猪油糖、水晶之恋果冻 |
| 饼干 | 橙色 #FF9800 | (数据中较少,主要是糕饼类) |
| 汽水 | 蓝色 #03A9F4 | 北冰洋汽水、健力宝、大白梨汽水 |
| 辣条 | 红色 #F44336 | 辣条(大面筋)、五毛辣片、麻辣王子 |
| 糕饼 | 紫色 #9C27B0 | 旺旺雪饼、老式桃酥、鸡蛋小饼干、绿豆糕、老式鸡蛋糕 |
注意:数据中还有"零食"(咪咪虾条、小浣熊干脆面、无花果丝等)、“饮料”(AD钙奶、酸梅汤粉)、“冷饮”(红豆冰棍)等分类,但筛选器只列了 6 类,这些分类的零食在"全部"中可见。
8.2 价格策略
30 款零食的价格从 1 元到 12.8 元不等,覆盖了不同消费层次:
- 1 元档:五毛辣片(1.0)、无花果丝(1.0)、老北京方便面(1.2)、泡泡糖西瓜味(1.5)、咪咪虾条(1.5)
- 2-5 元档:小浣熊干脆面(2.0)、山楂片(2.5)、果丹皮(3.0)、大白梨汽水(3.0)、辣条(3.5)、健力宝(4.5)、酸梅汤粉(4.0)、老式爆米花(5.0)、大大卷(5.0)
- 5-10 元档:北冰洋汽水(6.0)、AD钙奶(6.0)、鸡蛋小饼干(6.5)、水晶之恋果冻(7.5)、烤鱼片(7.0)、绿豆糕(9.0)、麦丽素(9.9)
- 10 元以上:旺旺雪饼(10.5)、老式鸡蛋糕(8.0)、老式桃酥(12.0)、麻辣王子(12.8)
每款零食都有 originalPrice 原价,比现价高 20%-50%,用删除线展示,营造"怀旧优惠价"的感觉。
8.3 购物车逻辑
购物车实现了完整的电商逻辑,通过 4 个纯函数操作:
addToCart(添加商品)
function addToCart(list: CartItem[], item: SnackItem): CartItem[] {
const found = list.find((c: CartItem) => c.id === item.id)
if (found) {
return list.map((c: CartItem) => {
if (c.id !== item.id) return c
return { id: c.id, name: c.name, price: c.price, icon: c.icon, qty: c.qty + 1 }
})
}
const newItem: CartItem = { id: item.id, name: item.name, price: item.price, icon: item.icon, qty: 1 }
return [...list, newItem]
}
- 已存在商品:数量 +1(显式声明所有属性,禁止对象展开)
- 新商品:创建新 CartItem,数组展开添加到末尾
withCartQty(更新数量)
- 数量范围限制:
qty < 1 ? 1 : (qty > 99 ? 99 : qty) - 通过
map返回新数组,不修改原数组
removeFromCart(删除商品)
- 通过
filter过滤掉指定 id 的商品
cartTotal / cartCount(计算)
cartTotal:遍历累加price * qtycartCount:遍历累加qty
8.4 结算流程
结算流程简单直接:
- 购物车页面点击"去结算" → 打开结算成功弹框
- 弹框显示实付金额(
cartTotal动态计算) - 点击"再看看" → 关闭弹框,保留购物车
- 点击"完成" → 关闭弹框并清空购物车(
this.cartItems = [])
当前结算流程是模拟的,没有实际支付环节,适合演示和原型验证。
第九章 怀旧记忆与社区
9.1 时光记忆系统
8 条怀旧记忆是整个应用的情感核心,每条记忆都包含:
- 标题:简洁有力的记忆关键词(校门口的小卖部、第一次吃麦丽素、冰棍的夏天……)
- 年代:具体年份(1992-2010),像旧相册的日期戳
- 内容:50-80 字的短文,描绘一个具体的童年场景
- 图标:emoji 代表记忆主题(🏫🍫🧊🥤🥛🐻🐰🌶️)
- 点赞数:503-1350 不等,模拟社区热度
8 条记忆按时间排列,构成了一部"童年味道简史":
- 1992 大白兔奶糖的爱
- 1995 冰棍的夏天
- 1998 校门口的小卖部
- 2000 干脆面集卡热
- 2002 第一次吃麦丽素
- 2005 北冰洋与聚餐
- 2008 AD钙奶的童年
- 2010 麻辣烫里的辣条
9.2 记忆内容设计
每条记忆的内容都经过精心设计,力求"一句话唤起共鸣":
校门口的小卖部(1998):放学后攥着五毛钱,看着琳琅满目的零食,纠结买辣条还是泡泡糖。
第一次吃麦丽素(2002):模仿电视剧里的仙丹,一颗麦丽素在嘴里慢慢化开,是童年的武侠梦。
冰棍的夏天(1995):推着自行车的大爷,后座的木箱里藏着全世界最甜的冰棍。
干脆面集卡热(2000):为了集齐水浒卡,买空了小卖部一整箱干脆面,至今还记得那张闪卡。
这些内容没有华丽的辞藻,只有最朴素的场景描述,但每一个场景都是 80 后 90 后共同的记忆符号,能在一瞬间把用户拉回那个夏天。
9.3 怀旧社区
6 条社区帖子是用户生成内容(UGC)的模拟,每条包含:
- 用户:有怀旧感的昵称(小卖部老板、老北京人、爱啃干脆面的娃、北冰洋铁粉、00后回忆党、记忆收藏家)
- 头像:emoji(🏪🍦🐻🥤🍭📼)
- 标题:集体记忆话题(开店三十年见证零食变迁、还记得胡同里的冰棍车吗、集齐水浒卡的那天……)
- 内容:50-80 字的短文
- 点赞数:96-235
- 时间:相对时间(1小时前、3小时前、昨天、前天)
社区帖子的设计目的是让用户感受到"不是我一个人在怀旧"——原来还有这么多人记得胡同里的冰棍车、集齐水浒卡的那天、北冰洋配撸串的夏天。这种集体共鸣是怀旧社区的核心价值。
9.4 怀旧经济的心理学
时光小卖部的设计暗合了怀旧经济的心理学原理:
- 味道是最强的记忆触发器:嗅觉和味觉与大脑的海马体(记忆中枢)直接相连,一口大白兔奶糖的味道能瞬间唤起 20 年前的记忆
- 集体记忆产生归属感:当用户发现"原来你也吃过这个",会产生强烈的代际归属感
- 怀旧缓解焦虑:在快节奏的现代生活中,怀旧提供了一个安全的心理避风港
- 为记忆付费:用户愿意为"童年的味道"支付溢价,因为买到的不只是零食,还有一段回不去的时光
第十章 个人中心与成就体系
10.1 用户画像构建
我的页面构建了一个"怀旧收藏家"的用户画像:
- 用户名:怀旧收藏家
- 等级:LV.6 零食鉴赏师 · 365天(最高等级,老用户)
- 会员:VIP会员 · 全场88折(付费会员,有特权感)
- 积分:3280(高积分,活跃用户)
- 收藏:12(有收藏习惯)
这个画像让用户一进入"我的"页面就感受到"我是一个资深怀旧玩家",增强归属感和粘性。
10.2 成就徽章体系
3 枚成就徽章代表了不同的怀旧方向:
| 徽章 | 颜色 | 含义 |
|---|---|---|
| 🎖️ 辣条王者 | 复古红 + 浅橙底 | 辣条爱好者,吃遍各种辣条 |
| 🍫 糖果达人 | 粉红 + 粉底 | 糖果收藏家,尝遍各种糖果 |
| 🥤 汽水收藏家 | 蓝色 + 浅蓝底 | 汽水爱好者,收集各种汽水 |
徽章设计采用了与零食分类一致的颜色编码(辣条红、糖果粉、汽水蓝),保持视觉统一性。
10.3 订单记录
4 个订单状态 tab:全部、待付款、待收货、已完成。当前仅实现了 UI 切换(currentOrderTab),没有实际订单数据,是预留的扩展功能。
订单记录的设计让电商功能更完整,用户可以追踪自己的怀旧零食订单,增强"真实购物"的体验感。
10.4 底部标语
— 时光小卖部 · 童年不散场 — 这句标语是整个应用的情感总结,放在"我的"页面底部,每次用户滑到最底部都能看到,强化"童年不散场"的品牌理念。
第十一章 性能优化与扩展方向
11.1 当前性能特点
单文件架构的优势
- 启动快:无需模块加载,所有代码在一个文件中,编译后直接运行
- 内存占用低:无额外的模块管理开销,全局常量和函数直接访问
- 渲染高效:ArkTS 声明式 UI 的 diff 算法在单文件中更容易优化
- 开发效率高:无需处理导入导出,状态传递简单
列表渲染优化
ForEach的第二个参数(key 函数)确保列表项的唯一标识,如(s: SnackItem) => s.id.toString()- 筛选场景下 key 包含筛选条件,确保筛选切换时正确重建列表
maxLines(1)+TextOverflow.Ellipsis限制长文本渲染,避免布局计算开销- 双列宫格使用
FlexWrap而非Grid,更轻量,适合固定高度的卡片
动画性能
- 仅 1 处属性动画(Tab 切换图标缩放),GPU 加速友好
- 无复杂的转场动画、物理动画、手势动画
- 动画时长 200ms,短平快,不影响交互响应
11.2 可优化点
数据层面
- 图片资源:当前使用 emoji 作为商品图,零网络请求。若替换为真实商品图片,应使用
Image的alt、offset等属性优化加载体验,并考虑图片懒加载 - 数据持久化:当前购物车、订单等数据为内存状态,重启后丢失。可接入
Preferences或数据库持久化用户购物车、收藏、订单 - 大列表虚拟化:当前 30 款零食数据量小,无需虚拟化。若扩展到数百款,应使用
LazyForEach实现懒加载
组件层面
- 组件拆分:当前单文件 1336 行,可按功能拆分为多个文件(如
components/、models/、utils/、pages/),提升可维护性 - 自定义组件复用:
bannerCard、catChip等已通过@Builder复用,可进一步抽取为独立@Component组件 - 状态管理:当前使用组件级
@State,购物车状态在 CartContent 内部,零食铺无法直接添加。可引入AppStorage或Provide/Inject实现全局购物车状态
交互层面
- 下拉刷新:零食列表、记忆列表可增加下拉刷新,模拟数据更新
- 无限滚动:当前数据量固定,可扩展为分页加载
- 搜索功能:头部搜索框为展示用,可实现真实的零食搜索、记忆搜索
- 分享功能:记忆和帖子可增加分享到微信、朋友圈等功能
- 购买流程完善:当前
buyModal的"加入购物车"按钮未实际调用addToCart,应完善购买流程
11.3 扩展方向
功能扩展
- 商品详情页:点击零食卡片进入详情页,展示大图、详细描述、用户评价、相关推荐
- 评价系统:用户购买后可对零食进行评价(味道、包装、性价比),生成评分
- 收藏系统:用户可收藏喜欢的零食,在"我的收藏"中查看
- 搜索历史:记录用户搜索历史,提供搜索建议
- 地址管理:收货地址管理,支持多个地址
- 支付接入:接入真实支付(微信支付、支付宝),完成交易闭环
- 物流追踪:订单发货后可追踪物流状态
社区扩展
- 发布记忆:用户可发布自己的怀旧记忆,带图片、文字、年代标签
- 评论互动:记忆和帖子下可评论、回复、点赞
- 关注系统:用户可关注其他怀旧爱好者,查看关注动态
- 话题广场:按话题分类(#童年零食#、#校门口的味道#、#集卡回忆#)
- 活动运营:定期举办怀旧主题活动(“晒出你的童年零食”、“年代味道评选”)
技术扩展
- 多端适配:当前已适配平板横屏,可进一步适配折叠屏、车机、智慧屏
- 国际化:当前为中文界面,可扩展多语言支持
- 深色模式:当前为固定浅色复古风,可支持深色模式(复古夜景风)
- 无障碍:增加屏幕阅读器支持、颜色对比度优化、字体大小适配
- 性能监控:接入性能监控 SDK,跟踪页面加载时间、帧率、内存占用
第十二章 复古零食文化与怀旧经济
12.1 复古零食的文化符号学
30 款复古零食不只是食品,更是文化符号:
- 大白兔奶糖:国民糖果,承载了几代人的甜蜜记忆,甚至成为国礼
- 辣条:从五毛辣片到麻辣王子,是"校园奢侈零食"的代表,也是中国零食产业的奇迹
- 北冰洋汽水:北京人的集体记忆,玻璃瓶橘子汽水,是老北京文化的符号
- 麦丽素:因为电视剧里的"仙丹"梗,成为了童年角色扮演的道具
- 小浣熊干脆面:水浒卡是 90 后集体的集卡记忆,为了卡片买空一整箱干脆面
- AD钙奶:娃哈哈的经典产品,是"小朋友的最爱",也是 00 后的童年符号
每一款零食都有自己的故事,这些故事共同构成了 80 后 90 后 00 后的味觉记忆图谱。
12.2 怀旧经济的商业逻辑
怀旧经济是近年来的消费热点,其商业逻辑包括:
- 情绪价值 > 功能价值:用户买的不只是零食,更是"童年的味道"、“回不去的时光”,愿意为情绪价值支付溢价
- 集体记忆 = 社交货币:在社交媒体上分享"你吃过这个吗"能快速引发同代人共鸣,产生自发传播
- IP 化运营:大白兔、北冰洋、健力宝等老品牌通过 IP 联名(大白兔香水、北冰洋雪糕)焕发新生
- 场景化消费:怀旧零食适合特定场景(追剧、聚会、送礼),场景化营销能刺激消费
- 代际传递:80 后 90 后成为父母后,会把自己的童年零食分享给孩子,实现代际传递
12.3 复古电商的设计启示
时光小卖部的设计为复古主题电商提供了以下启示:
- 配色即情绪:复古红 + 怀旧金 + 米白的配色组合,一进入应用就唤起怀旧情绪,比任何文案都有效
- emoji 即商品图:用 emoji 代替精美的商品摄影,反而更有"手绘感"、“小卖部感”,降低了内容生产成本,也形成了独特的视觉风格
- 记忆即内容:8 条怀旧记忆是最好的"商品详情页"——用户不是因为零食好吃才买,而是因为记忆里的味道才买
- 社区即共鸣:6 条社区帖子让用户感受到"不是我一个人在怀旧",社区互动能显著提升用户粘性
- 成就即身份:LV.6 零食鉴赏师、辣条王者、糖果达人等成就徽章,让用户在怀旧社区中找到身份认同
12.4 童年味道清单
最后,附上时光小卖部的 30 款童年味道清单,看看你吃过几款:
糖果类:大白兔奶糖、大大卷泡泡糖、麦丽素、猪油糖、大大泡泡糖(口哨型)、水晶之恋果冻、泡泡糖(西瓜味)
零食类:咪咪虾条、果丹皮、小浣熊干脆面、无花果丝、深井烧鹅干脆面、老北京方便面、烤鱼片、山楂片、老式爆米花
汽饮类:北冰洋汽水、健力宝、AD钙奶、大白梨汽水、酸梅汤粉、红豆冰棍
辣条类:辣条(大面筋)、五毛辣片、麻辣王子
糕饼类:旺旺雪饼、老式桃酥、鸡蛋小饼干、绿豆糕、老式鸡蛋糕
十三、开发环境搭建
13.1 环境要求
DevEco Studio 5.0+(支持HarmonyOS 6.1.1)、HarmonyOS SDK API 24、Node.js 18+、至少8GB内存。
13.2 安装DevEco Studio

从华为开发者官网下载最新版本,运行安装包,选择安装路径,勾选创建桌面快捷方式,点击安装。
13.3 配置SDK

首次启动后进入File > Settings > HarmonyOS SDK,勾选API 24及以上版本,点击Apply等待下载完成。
13.4 创建项目

选择Create Project > Empty Ability,配置项目名称、包名、保存路径,选择Compile SDK为API 24,点击Finish。
13.5 运行应用

将1316.ets代码替换到entry/src/main/ets/pages/Index.ets,连接HarmonyOS设备或启动模拟器,点击运行按钮预览。
13.6 预览调试

DevEco Studio提供实时预览功能,编辑器右侧打开Previewer面板,支持多设备尺寸预览、深色模式切换、组件树inspect等调试能力。
总结与展望
项目总结
时光小卖部是一个面向 80 后 90 后怀旧人群的复古零食电商 + 记忆社区应用,用 1336 行 ArkTS 代码完整实现了五大核心模块:
- 首页·推荐专区:今日推荐 Banner、热门零食横滑、小卖部故事,营造怀旧氛围
- 零食铺·分类宫格:30 款复古零食,6 分类筛选,双列宫格,加入购物车弹框
- 怀旧墙·时光记忆:8 条怀旧记忆(1992-2010),6 条社区帖子,情感核心
- 购物车·清单结算:商品清单、数量加减、删除、合计、结算成功弹框
- 我的·个人中心:个人资料、积分收藏、成就墙(3 枚徽章)、订单记录
技术亮点:
- 复古港风配色(复古红 #C62828 × 怀旧金 #FFC107 × 米白 #FDF5E6)
- 零食分类颜色编码(糖果粉/饼干橙/汽水蓝/辣条红/糕饼紫)
- emoji 商品图,手绘感,低成本高辨识度
- 单文件架构,@Entry + @Component,@State 状态管理
- 2 个弹框(加入购物车底部抽屉 + 结算成功居中卡片)
- 1 处属性动画(Tab 切换图标缩放 1.15 倍)
- 11 个纯函数工具层(购物车操作、价格格式化、分类颜色等)
- 不可变状态更新(禁止对象展开,全部显式声明属性)
- 平板横屏自适应
数据规模:
- 30 款复古零食(糖果/零食/汽饮/辣条/糕饼全品类)
- 8 条怀旧记忆(1992-2010 时间轴)
- 6 条社区帖子(集体记忆共鸣)
- 3 件初始购物车商品(大白兔 x2、咪咪虾条 x10、麦丽素 x1)
设计哲学回顾
时光小卖部的设计哲学可以用三个关键词概括:
复古:配色是复古的,emoji 是怀旧的,记忆是童年的。整个应用从视觉到内容都在刻意营造"回到小时候"的氛围——米白背景像泛黄的信纸,复古红像老招牌,怀旧金像旧奖状,emoji 像手画的价格牌。这种复古不是简单的"做旧",而是通过色彩、图标、内容、文案的全方位配合,让用户一进入应用就感受到"这是童年的味道"。
温暖:没有高冷的极简风,没有花哨的特效,只有温暖的米白背景、圆润的卡片、可爱的 emoji、朴实的文案。小卖部故事里的"老式爆米花大爷"、怀旧记忆里的"推着自行车的大爷"、社区帖子里的"还记得胡同里的冰棍车吗"——这些内容都在传递一种温暖的人情味,让用户觉得这不是一个冰冷的电商 App,而是一个有温度的记忆馆。
共鸣:8 条怀旧记忆和 6 条社区帖子都是精心挑选的集体记忆——校门口的小卖部、第一次吃麦丽素、冰棍的夏天、干脆面集卡热、北冰洋配撸串……每一条都能让目标用户会心一笑,唤起"原来你也吃过这个"的共鸣。这种集体共鸣是怀旧社区的核心价值,也是用户粘性的来源。
未来展望
短期优化
- 完善购买流程:当前
buyModal的"加入购物车"按钮未实际调用addToCart,应完善为点击后真正添加到购物车,并跳转到购物车页面 - 全局购物车状态:当前购物车状态在 CartContent 内部,零食铺无法直接添加。应引入
AppStorage或Provide/Inject实现全局购物车状态 - 商品详情页:点击零食卡片进入详情页,展示大图、详细描述、用户评价、相关推荐
- 搜索功能:头部搜索框实现真实的零食搜索、记忆搜索
- 数据持久化:购物车、收藏、订单等数据本地存储,重启不丢失
中期扩展
- 评价系统:用户购买后可对零食进行评价(味道、包装、性价比),生成评分
- 收藏系统:用户可收藏喜欢的零食,在"我的收藏"中查看
- 发布记忆:用户可发布自己的怀旧记忆,带图片、文字、年代标签
- 评论互动:记忆和帖子下可评论、回复、点赞
- 关注系统:用户可关注其他怀旧爱好者,查看关注动态
- 支付接入:接入真实支付(微信支付、支付宝),完成交易闭环
- 物流追踪:订单发货后可追踪物流状态
长期愿景
- IP 化运营:与大白兔、北冰洋、健力宝等老品牌合作,推出联名款零食、周边
- 线下快闪店:在线下开设"时光小卖部"快闪店,线上线下联动
- 年代主题活动:定期举办 80 后、90 后、00 后年代主题活动,“晒出你的童年零食”
- AI 怀旧助手:基于用户的童年记忆,AI 推荐"你可能还记得的零食",生成个性化怀旧清单
- 多端联动:手机、平板、车机、智慧屏多端同步,在车机上也能"听"怀旧故事

时光小卖部 · 童年不散场。
30 款童年零食 · 8 条怀旧记忆 · 6 条社区帖子 · 5 Tab + 2 弹框
本文基于时光小卖部 ArkTS 单文件源码深度解析,项目采用 HarmonyOS ArkTS 声明式 UI 框架,复古红 × 怀旧金港风配色,完整实现复古零食电商 + 怀旧记忆社区核心业务。完整源码含 30 款零食 / 8 条记忆 / 6 条帖子等全部数据,可按需获取。
完整代码(精简版)
精简版保留核心架构与关键组件,零食 30→15 款、记忆 8→4 条、帖子 6→3 条,重复 UI 用注释说明。完整版含全部数据可按需获取。
// ============================================================
// 时光小卖部 · 复古零食记忆馆 —— 精简版核心源码
// 复古红 #C62828 × 怀旧金 #FFC107,5 Tab 单排
// 完整版含 30 款零食 / 8 条记忆 / 6 条帖子 / 2 弹框
// ============================================================
// ---------- 接口定义 ----------
interface SnackItem {
id: number; name: string; brand: string; category: string
price: number; originalPrice: number; icon: string; desc: string
isHot: boolean; isNew: boolean
}
interface CartItem { id: number; name: string; price: number; icon: string; qty: number }
interface MemoryItem { id: number; title: string; year: string; content: string; icon: string; likes: number }
interface NostalgiaPost { id: number; user: string; avatar: string; title: string; content: string; likes: number; time: string }
interface Pal {
bg: string; primary: string; primaryLight: string; card: string; deep: string
text: string; textSub: string; chip: string; warn: string; white: string
green: string; yellow: string
}
// ---------- 调色板(复古港风) ----------
const P: Pal = {
bg: '#FDF5E6', primary: '#C62828', primaryLight: '#E53935', card: '#FFFFFF', deep: '#5D4037',
text: '#3E2723', textSub: '#8D6E63', chip: '#FFE0B2', warn: '#FF7043', white: '#FFFFFF',
green: '#2E7D32', yellow: '#FFC107'
}
// ---------- 静态选项 ----------
const CATEGORIES: string[] = ['全部', '糖果', '饼干', '汽水', '辣条', '糕饼']
const CART_TABS: string[] = ['全部', '待付款', '待收货', '已完成']
// ---------- 纯函数(11 个) ----------
function categoryColor(cat: string): string {
if (cat === '糖果') return '#E91E63'
if (cat === '饼干') return '#FF9800'
if (cat === '汽水') return '#03A9F4'
if (cat === '辣条') return '#F44336'
if (cat === '糕饼') return '#9C27B0'
return '#607D8B'
}
function snackByCategory(list: SnackItem[], cat: string): SnackItem[] {
if (cat === '全部') return list
return list.filter((s: SnackItem) => s.category === cat)
}
function moneyFormat(n: number): string { return n.toFixed(1) }
function cartTotal(list: CartItem[]): number {
let t: number = 0
list.forEach((c: CartItem) => { t += c.price * c.qty })
return t
}
function cartCount(list: CartItem[]): number {
let t: number = 0
list.forEach((c: CartItem) => { t += c.qty })
return t
}
// 已修正:禁止使用对象展开,全部显式声明属性
function withCartQty(list: CartItem[], id: number, qty: number): CartItem[] {
return list.map((c: CartItem) => {
if (c.id !== id) return c
return { id: c.id, name: c.name, price: c.price, icon: c.icon, qty: qty < 1 ? 1 : (qty > 99 ? 99 : qty) }
})
}
function addToCart(list: CartItem[], item: SnackItem): CartItem[] {
const found = list.find((c: CartItem) => c.id === item.id)
if (found) {
return list.map((c: CartItem) => {
if (c.id !== item.id) return c
return { id: c.id, name: c.name, price: c.price, icon: c.icon, qty: c.qty + 1 }
})
}
const newItem: CartItem = { id: item.id, name: item.name, price: item.price, icon: item.icon, qty: 1 }
return [...list, newItem] // 数组展开是允许的
}
function removeFromCart(list: CartItem[], id: number): CartItem[] {
return list.filter((c: CartItem) => c.id !== id)
}
function withMemoryLiked(list: MemoryItem[], id: number): MemoryItem[] {
return list.map((m: MemoryItem) => {
if (m.id !== id) return m
return { id: m.id, title: m.title, year: m.year, content: m.content, icon: m.icon, likes: m.likes + 1 }
})
}
function ratingStars(r: number): string {
let t: string = ''; let i: number = 1
while (i <= 5) { if (i <= r) t += '★'; else t += '☆'; i += 1 }
return t
}
function hotColor(isHot: boolean): string { return isHot ? P.warn : P.textSub }
// ---------- 数据:复古零食(精简版 15 款,完整版 30 款) ----------
const SNACKS: SnackItem[] = [
{ id: 1, name: '大白兔奶糖', brand: '冠生园', category: '糖果', price: 8.8, originalPrice: 12.0, icon: '🐰', desc: '经典原味,奶香浓郁,童年的甜蜜回忆', isHot: true, isNew: false },
{ id: 2, name: '咪咪虾条', brand: '福建', category: '零食', price: 1.5, originalPrice: 2.0, icon: '🦐', desc: '金黄酥脆,鲜虾味十足,一元一包快乐源泉', isHot: true, isNew: false },
{ id: 3, name: '大大卷泡泡糖', brand: '箭牌', category: '糖果', price: 5.0, originalPrice: 6.5, icon: '🌀', desc: '长长一条,粉色草莓味,拉长扯断的快乐', isHot: false, isNew: false },
{ id: 4, name: '旺旺雪饼', brand: '旺旺', category: '糕饼', price: 10.5, originalPrice: 13.8, icon: '❄️', desc: '雪白米饼,清甜不腻,旺旺大礼包必备', isHot: true, isNew: true },
{ id: 5, name: '辣条(大面筋)', brand: '卫龙', category: '辣条', price: 3.5, originalPrice: 5.0, icon: '🌶️', desc: '麻辣鲜香,筋道Q弹,小时候的"奢侈零食"', isHot: true, isNew: false },
{ id: 6, name: '北冰洋汽水', brand: '北冰洋', category: '汽水', price: 6.0, originalPrice: 8.0, icon: '🥤', desc: '童年记忆里的橘子汽水,气泡十足', isHot: true, isNew: false },
{ id: 7, name: '健力宝', brand: '健力宝', category: '汽水', price: 4.5, originalPrice: 6.0, icon: '🍹', desc: '国民运动饮料,经典银罐,橙蜜味道', isHot: false, isNew: false },
{ id: 8, name: '麦丽素', brand: '梁丰', category: '糖果', price: 9.9, originalPrice: 12.5, icon: '🍫', desc: '外层巧克力,内芯麦芽酥,入口即化', isHot: true, isNew: true },
{ id: 11, name: '小浣熊干脆面', brand: '统一', category: '零食', price: 2.0, originalPrice: 2.5, icon: '🐻', desc: '捏碎撒粉,集齐水浒卡是童年目标', isHot: true, isNew: false },
{ id: 12, name: 'AD钙奶', brand: '娃哈哈', category: '饮料', price: 6.0, originalPrice: 8.0, icon: '🥛', desc: '酸酸甜甜,奶香四溢,小朋友的最爱', isHot: true, isNew: false },
{ id: 14, name: '五毛辣片', brand: '老式', category: '辣条', price: 1.0, originalPrice: 1.5, icon: '🔥', desc: '薄薄辣片,又辣又香,校门口爆款', isHot: true, isNew: false },
{ id: 15, name: '老式桃酥', brand: '宫廷', category: '糕饼', price: 12.0, originalPrice: 15.0, icon: '🍪', desc: '酥脆掉渣,芝麻增香,配茶绝佳', isHot: false, isNew: true },
{ id: 19, name: '麻辣王子', brand: '麻辣王子', category: '辣条', price: 12.8, originalPrice: 15.8, icon: '👑', desc: '高端辣条,不添加甜蜜素,麻辣味足', isHot: true, isNew: true },
{ id: 24, name: '无花果丝', brand: '传统', category: '零食', price: 1.0, originalPrice: 1.5, icon: '🥭', desc: '不是无花果的"无花果丝",酸酸甜甜', isHot: true, isNew: false },
{ id: 26, name: '老北京方便面', brand: '南街村', category: '零食', price: 1.2, originalPrice: 1.5, icon: '🍜', desc: '北京麻辣方便面,干吃泡吃两相宜', isHot: true, isNew: false },
// ... 完整版共 30 款(含 果丹皮 / 猪油糖 / 大白梨汽水 / 鸡蛋小饼干 / 绿豆糕 / 老式鸡蛋糕 / 山楂片 / 深井烧鹅干脆面 / 红豆冰棍 / 酸梅汤粉 / 烤鱼片 / 泡泡糖西瓜味 / 水晶之恋果冻 / 老式爆米花)
]
// ---------- 数据:怀旧记忆(精简版 4 条,完整版 8 条) ----------
const MEMORIES: MemoryItem[] = [
{ id: 1, title: '校门口的小卖部', year: '1998', content: '放学后攥着五毛钱,看着琳琅满目的零食,纠结买辣条还是泡泡糖。', icon: '🏫', likes: 986 },
{ id: 2, title: '第一次吃麦丽素', year: '2002', content: '模仿电视剧里的仙丹,一颗麦丽素在嘴里慢慢化开,是童年的武侠梦。', icon: '🍫', likes: 802 },
{ id: 3, title: '冰棍的夏天', year: '1995', content: '推着自行车的大爷,后座的木箱里藏着全世界最甜的冰棍。', icon: '🧊', likes: 1203 },
{ id: 6, title: '干脆面集卡热', year: '2000', content: '为了集齐水浒卡,买空了小卖部一整箱干脆面,至今还记得那张闪卡。', icon: '🐻', likes: 1350 },
// ... 完整版共 8 条(含 北冰洋与聚餐 / AD钙奶的童年 / 大白兔奶糖的爱 / 麻辣烫里的辣条)
]
// ---------- 数据:怀旧社区帖子(精简版 3 条,完整版 6 条) ----------
const NOSTALGIA_POSTS: NostalgiaPost[] = [
{ id: 1, user: '小卖部老板', avatar: '🏪', title: '开店三十年,见证零食的变迁', content: '从五毛辣片到十块钱一包的高端零食,变的不是价格,是孩子们的口味。', likes: 158, time: '1小时前' },
{ id: 3, user: '爱啃干脆面的娃', avatar: '🐻', title: '集齐水浒卡的那天', content: '被妈妈痛骂了一顿,但摸到那张闪卡的时候,觉得一切都值了。', likes: 235, time: '6小时前' },
{ id: 6, user: '记忆收藏家', avatar: '📼', title: '零食就是时光机', content: '有些味道,会在一瞬间带你回到童年。大白兔奶糖的甜,是那年夏天的味道。', likes: 143, time: '前天' },
// ... 完整版共 6 条(含 老北京人 / 北冰洋铁粉 / 00后回忆党)
]
// ---------- Tab 枚举 ----------
enum ShopTab { HOME = 0, SNACKS = 1, MEMORY = 2, CART = 3, MINE = 4 }
// ---------- 入口组件 ----------
@Entry
@Component
struct RetroSnackShop {
@State activeTab: number = ShopTab.HOME
build() {
Column() {
ShopHeader()
Stack() {
if (this.activeTab === ShopTab.HOME) HomeContent()
if (this.activeTab === ShopTab.SNACKS) SnackListContent()
if (this.activeTab === ShopTab.MEMORY) MemoryContent()
if (this.activeTab === ShopTab.CART) CartContent()
if (this.activeTab === ShopTab.MINE) MineContent()
}
.layoutWeight(1).width('100%')
Row() {
this.tabItem('🏠', '首页', ShopTab.HOME)
this.tabItem('🍫', '零食铺', ShopTab.SNACKS)
this.tabItem('📼', '怀旧墙', ShopTab.MEMORY)
this.tabItem('🛒', '购物车', ShopTab.CART)
this.tabItem('👤', '我的', ShopTab.MINE)
}
.width('100%').height(58).backgroundColor(P.white).padding({ left: 6, right: 6, top: 4 })
}
.width('100%').height('100%').backgroundColor(P.bg)
}
@Builder
tabItem(icon: string, label: string, tab: number) {
Column() {
Text(icon).fontSize(20)
.scale({ x: this.activeTab === tab ? 1.15 : 1, y: this.activeTab === tab ? 1.15 : 1 })
.animation({ duration: 200, curve: Curve.EaseOut })
Text(label).fontSize(10)
.fontColor(this.activeTab === tab ? P.primary : P.textSub).margin({ top: 1 })
Divider().strokeWidth(2).color(this.activeTab === tab ? P.primary : Color.Transparent)
.width(16).borderRadius(1).margin({ top: 2 })
}
.layoutWeight(1).onClick(() => { this.activeTab = tab })
}
}
// ---------- 头部组件 ----------
@Component
struct ShopHeader {
build() {
Column() {
Row() {
Text('🏪').fontSize(24)
Column() {
Text('时光小卖部').fontSize(19).fontWeight(FontWeight.Bold).fontColor(P.primary)
Text('时光小卖部 · 回味童年好味道').fontSize(9).fontColor(P.textSub).margin({ top: 1 })
}.alignItems(HorizontalAlign.Start).margin({ left: 8 })
Column().layoutWeight(1)
Text('🔔').fontSize(19).margin({ right: 14 })
Text('🛒').fontSize(19)
}.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 8 })
Row() {
Text('⌕').fontSize(15).fontColor(P.textSub).margin({ left: 12 })
Text('搜零食:大白兔 / 辣条 / 汽水').fontSize(12).fontColor(P.textSub).margin({ left: 6 })
Column().layoutWeight(1)
Text('搜索').fontSize(12).fontColor(P.white).backgroundColor(P.primary).borderRadius(13)
.padding({ left: 12, right: 12, top: 5, bottom: 5 }).margin({ right: 8 })
}.width('92%').height(34).backgroundColor(P.card).borderRadius(17)
Row() {
Text('🔥 童年爆款').fontSize(10).fontColor(P.primary).backgroundColor('#FFE0B2').borderRadius(9)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
Text('🧧 怀旧礼包').fontSize(10).fontColor('#B71C1C').backgroundColor('#FFCDD2').borderRadius(9)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
Text('⭐ 经典国货').fontSize(10).fontColor('#E65100').backgroundColor('#FFE0B2').borderRadius(9)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
Text('🥤 汽水专区').fontSize(10).fontColor('#0277BD').backgroundColor('#B3E5FC').borderRadius(9)
.padding({ left: 9, right: 9, top: 4, bottom: 4 })
}.width('92%').margin({ top: 10 }).justifyContent(FlexAlign.SpaceBetween)
}
.width('100%').backgroundColor(P.card).borderRadius({ bottomLeft: 16, bottomRight: 16 }).padding({ bottom: 12 })
}
}
// ---------- Tab1 首页:推荐专区 + 文化展示 ----------
@Component
struct HomeContent {
@State selectedBanner: number = 0
@Builder
bannerCard(title: string, sub: string, icon: string, bg: string, idx: number) {
Row() {
Column() {
Text(title).fontSize(18).fontWeight(FontWeight.Bold).fontColor(P.white)
Text(sub).fontSize(11).fontColor('#FFE082').margin({ top: 4 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 16 })
Text(icon).fontSize(32).margin({ right: 16 })
}.width('100%').height(100).backgroundColor(bg).borderRadius(16).margin({ top: 8 })
}
build() {
Scroll() {
Column() {
Row() {
Text('🎁 今日推荐').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.text)
Column().layoutWeight(1)
Text('查看更多 >').fontSize(10).fontColor(P.primary)
}.width('94%').margin({ top: 12 })
this.bannerCard('童年回忆礼包', '大白兔+辣条+麦丽素', '🍫', '#C62828', 0)
this.bannerCard('夏日汽水节', '北冰洋/健力宝 冰爽价', '🥤', '#0277BD', 1)
Text('🔥 热门怀旧零食').fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.text)
.width('94%').margin({ top: 16 })
// 热门零食横滑(4 款:大白兔/咪咪虾条/大大卷/旺旺雪饼,每卡 emoji+名称+价格)
Scroll() {
Row({ space: 10 }) {
ForEach([SNACKS[0], SNACKS[1], SNACKS[2], SNACKS[3]], (s: SnackItem) => {
Column() {
Text(s.icon).fontSize(30)
Text(s.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor(P.text).margin({ top: 5 })
Text('¥' + moneyFormat(s.price)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(P.primary).margin({ top: 4 })
}.width(80).backgroundColor(P.card).borderRadius(10).padding(10).alignItems(HorizontalAlign.Center)
})
}.padding({ left: 4, right: 4 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('94%').margin({ top: 6 })
Text('🏆 小卖部故事').fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.text)
.width('94%').margin({ top: 16 })
// 小卖部故事卡(老式爆米花大爷:头像+标题+描述+正文)
Column() {
Row() {
Text('🧑🏫').fontSize(30)
Column() {
Text('老式爆米花大爷').fontSize(13).fontWeight(FontWeight.Bold).fontColor(P.text)
Text('"砰"的一声,是童年最响亮的声音').fontSize(10).fontColor(P.textSub).margin({ top: 4 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
}.width('100%')
Text('那时候街头的大铁炉爆米花,是孩子们的期待。一声巨响,满街飘香,那是属于80后90后的集体记忆。')
.fontSize(11).fontColor(P.textSub).lineHeight(18).margin({ top: 10 })
}.width('94%').backgroundColor(P.card).borderRadius(12).padding(12).margin({ top: 10, bottom: 16 })
}.width('100%')
}.scrollable(ScrollDirection.Vertical).width('100%').height('100%')
}
}
// ---------- Tab2 零食铺:分类 + 双列宫格 + 购买弹窗 ----------
@Component
struct SnackListContent {
@State snacks: SnackItem[] = SNACKS
@State curCat: string = '全部'
@State showBuy: boolean = false
@State buySnack: SnackItem = SNACKS[0]
@State buyQty: number = 1
@Builder
catChip(label: string) {
Text(label).fontSize(11)
.fontColor(this.curCat === label ? P.white : P.text)
.backgroundColor(this.curCat === label ? P.primary : P.chip)
.borderRadius(11).padding({ left: 12, right: 12, top: 5, bottom: 5 }).margin({ left: 8 })
.onClick(() => { this.curCat = label; this.snacks = snackByCategory(SNACKS, label) })
}
build() {
Stack() {
Scroll() {
Column() {
Text('🍫 怀旧零食铺').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.text)
.width('94%').margin({ top: 12 })
// 分类筛选横滑(6 分类:全部/糖果/饼干/汽水/辣条/糕饼)
Scroll() {
Row() {
this.catChip('全部'); this.catChip('糖果'); this.catChip('饼干')
this.catChip('汽水'); this.catChip('辣条'); this.catChip('糕饼')
}
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('94%').margin({ top: 12 })
// 双列宫格(FlexWrap,每卡 48.5%,零食卡片:图标+HOT/NEW标签+分类色标+名称+描述+价格+原价删除线+🛒按钮)
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.snacks, (s: SnackItem) => {
Column() {
Row() {
Text(s.icon).fontSize(28)
Column() {
if (s.isHot) Text('HOT').fontSize(8).fontColor(P.white).backgroundColor(P.warn)
.borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 })
if (s.isNew) Text('NEW').fontSize(8).fontColor(P.white).backgroundColor(P.green)
.borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 }).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).margin({ left: 6 })
Column().layoutWeight(1)
Text(categoryColor(s.category)).fontSize(8).fontColor(P.white)
.backgroundColor(categoryColor(s.category)).borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
}.width('100%')
Text(s.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor(P.text)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%').margin({ top: 8 })
Text(s.desc).fontSize(9).fontColor(P.textSub).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%').margin({ top: 2 })
Divider().color('#FFE0B2').margin({ top: 8 })
Row() {
Text('¥' + moneyFormat(s.price)).fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.primary)
Text(' ¥' + moneyFormat(s.originalPrice)).fontSize(9).fontColor(P.textSub)
.decoration({ type: TextDecorationType.LineThrough })
Column().layoutWeight(1)
Text('🛒').fontSize(14).onClick(() => {
this.buySnack = s; this.buyQty = 1; this.showBuy = true
})
}.width('100%').margin({ top: 6 })
}.width('48.5%').backgroundColor(P.card).borderRadius(12).padding(11).margin({ top: 10 })
})
}.width('94%').margin({ top: 4 }).padding({ bottom: 16 })
}.width('100%')
}.scrollable(ScrollDirection.Vertical).width('100%').height('100%')
if (this.showBuy) { this.buyModal() }
}.width('100%').height('100%')
}
// 弹框:加入购物车(底部抽屉,商品信息+数量选择+加入按钮)
@Builder
buyModal() {
Column() {
Column().layoutWeight(1).width('100%').onClick(() => { this.showBuy = false })
Column() {
Row() {
Text('🛒 加入购物车').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.text)
Column().layoutWeight(1)
Text('✕').fontSize(16).fontColor(P.textSub).onClick(() => { this.showBuy = false })
}.width('100%').padding({ bottom: 12 })
Row() {
Text(this.buySnack.icon).fontSize(44).backgroundColor(P.chip).borderRadius(14).padding(14)
Column() {
Text(this.buySnack.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.text)
Text(this.buySnack.desc).fontSize(10).fontColor(P.textSub).margin({ top: 4 })
Text('¥' + moneyFormat(this.buySnack.price)).fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.primary).margin({ top: 4 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 12 })
}.width('100%')
// 数量选择(− / X件 / +,范围1-20)
Row() {
Text('数量').fontSize(12).fontColor(P.textSub)
Column().layoutWeight(1)
Text('−').fontSize(17).fontColor(P.textSub).backgroundColor(P.chip).borderRadius(8)
.width(30).height(30).textAlign(TextAlign.Center).onClick(() => { if (this.buyQty > 1) this.buyQty -= 1 })
Text(this.buyQty + ' 件').fontSize(13).fontColor(P.text).margin({ left: 10, right: 10 })
Text('+').fontSize(17).fontColor(P.white).backgroundColor(P.primary).borderRadius(8)
.width(30).height(30).textAlign(TextAlign.Center).onClick(() => { if (this.buyQty < 20) this.buyQty += 1 })
}.width('100%').margin({ top: 16 })
Text('加入购物车').fontSize(15).fontColor(P.white).backgroundColor(P.primary).borderRadius(20)
.padding({ left: 60, right: 60, top: 10, bottom: 10 }).margin({ top: 14 })
.onClick(() => { this.showBuy = false })
}.width('100%').backgroundColor(P.card).borderRadius({ topLeft: 20, topRight: 20 }).padding(18).padding({ bottom: 28 })
}.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.6)').zIndex(999)
}
}
// ---------- Tab3 怀旧墙:时光记忆 + 社区帖 ----------
@Component
struct MemoryContent {
@State memories: MemoryItem[] = MEMORIES
@State posts: NostalgiaPost[] = NOSTALGIA_POSTS
build() {
Scroll() {
Column() {
Text('📼 时光怀旧墙').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.text)
.width('94%').margin({ top: 12 })
// 时光记忆卡片(8 条,每条:图标+标题+年代+❤️点赞+正文)
ForEach(this.memories, (m: MemoryItem) => {
Column() {
Row() {
Text(m.icon).fontSize(24)
Column() {
Text(m.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor(P.text)
Text('年代 ' + m.year).fontSize(9).fontColor(P.textSub).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
Text('❤️ ' + m.likes).fontSize(10).fontColor(P.warn)
}.width('100%')
Text(m.content).fontSize(11).fontColor(P.textSub).lineHeight(18)
.width('100%').margin({ top: 8 })
}.width('94%').backgroundColor(P.card).borderRadius(12).padding(12).margin({ top: 10 })
})
Text('💬 怀旧社区').fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.text)
.width('94%').margin({ top: 16 })
// 怀旧社区帖子(6 条,每条:头像+用户名+时间+❤️点赞+标题+正文)
ForEach(this.posts, (p: NostalgiaPost) => {
Column() {
Row() {
Text(p.avatar).fontSize(20).backgroundColor(P.chip).borderRadius(18)
.width(38).height(38).textAlign(TextAlign.Center)
Column() {
Text(p.user).fontSize(12).fontWeight(FontWeight.Medium).fontColor(P.text)
Text(p.time).fontSize(10).fontColor(P.textSub).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
Text('❤️ ' + p.likes).fontSize(10).fontColor(P.warn)
}.width('100%')
Text(p.title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(P.text)
.width('100%').margin({ top: 8 })
Text(p.content).fontSize(11).fontColor(P.textSub).lineHeight(18)
.width('100%').margin({ top: 4 })
}.width('94%').backgroundColor(P.card).borderRadius(14).padding(12).margin({ top: 10, bottom: 4 })
})
Text('— 更多回忆等你补充 —').fontSize(10).fontColor(P.textSub)
.width('94%').textAlign(TextAlign.Center).margin({ top: 12, bottom: 16 })
}.width('100%')
}.scrollable(ScrollDirection.Vertical).width('100%').height('100%')
}
}
// ---------- Tab4 购物车:清单 + 结算弹框 ----------
@Component
struct CartContent {
@State cartItems: CartItem[] = [
{ id: 1, name: '大白兔奶糖', price: 8.8, icon: '🐰', qty: 2 },
{ id: 2, name: '咪咪虾条', price: 1.5, icon: '🦐', qty: 10 },
{ id: 3, name: '麦丽素', price: 9.9, icon: '🍫', qty: 1 }
]
@State showCheckout: boolean = false
build() {
Stack() {
Column() {
Row() {
Text('🛒 我的购物车').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.text)
Column().layoutWeight(1)
Text('共 ' + cartCount(this.cartItems) + ' 件').fontSize(10).fontColor(P.textSub)
}.width('94%').margin({ top: 12 })
Scroll() {
Column() {
// 购物车商品列表(每行:图标+名称+单价+数量加减+删除)
ForEach(this.cartItems, (c: CartItem) => {
Row() {
Text(c.icon).fontSize(24).width(40).textAlign(TextAlign.Center)
Column() {
Text(c.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor(P.text)
Text('¥' + moneyFormat(c.price)).fontSize(11).fontColor(P.primary).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 10 })
Text('−').fontSize(15).fontColor(P.textSub).backgroundColor(P.chip).borderRadius(8)
.width(24).height(24).textAlign(TextAlign.Center)
.onClick(() => { this.cartItems = withCartQty(this.cartItems, c.id, c.qty - 1) })
Text(c.qty + '').fontSize(13).fontWeight(FontWeight.Bold).fontColor(P.text)
.width(30).textAlign(TextAlign.Center)
Text('+').fontSize(15).fontColor(P.white).backgroundColor(P.primary).borderRadius(8)
.width(24).height(24).textAlign(TextAlign.Center)
.onClick(() => { this.cartItems = withCartQty(this.cartItems, c.id, c.qty + 1) })
Text('🗑️').fontSize(14).margin({ left: 8 })
.onClick(() => { this.cartItems = removeFromCart(this.cartItems, c.id) })
}.width('94%').backgroundColor(P.card).borderRadius(12).padding(10).margin({ top: 8 })
})
Text('— 购物车空空如也 —').fontSize(10).fontColor(P.textSub)
.width('94%').textAlign(TextAlign.Center).margin({ top: 16 })
}.width('100%').padding({ bottom: 16 })
}.layoutWeight(1)
// 底部结算栏(合计+去结算按钮)
Column() {
Row() {
Text('合计').fontSize(12).fontColor(P.textSub)
Column().layoutWeight(1)
Text('¥' + moneyFormat(cartTotal(this.cartItems))).fontSize(18).fontWeight(FontWeight.Bold).fontColor(P.primary)
}.width('94%').margin({ top: 10 })
Text('去结算').fontSize(15).fontColor(P.white).backgroundColor(P.primary).borderRadius(20)
.padding({ left: 60, right: 60, top: 10, bottom: 10 }).margin({ top: 10, bottom: 12 })
.onClick(() => { this.showCheckout = true })
}.width('100%').backgroundColor(P.card).borderRadius({ topLeft: 16, topRight: 16 })
}.width('100%').height('100%')
if (this.showCheckout) { this.checkoutModal() }
}.width('100%').height('100%')
}
// 弹框:结算成功(居中卡片,✅下单成功+实付金额+再看看/完成按钮)
@Builder
checkoutModal() {
Column() {
Column().layoutWeight(1).width('100%').onClick(() => { this.showCheckout = false })
Column() {
Text('✅ 下单成功').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.green)
Text('你的怀旧零食正在打包中!').fontSize(13).fontColor(P.text).margin({ top: 8 })
Text('实付金额 ¥' + moneyFormat(cartTotal(this.cartItems))).fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(P.primary).margin({ top: 10 })
Row() {
Text('再看看').fontSize(12).fontColor(P.textSub)
.border({ width: 1, color: P.textSub }).borderRadius(16)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.onClick(() => { this.showCheckout = false })
Text('完成').fontSize(12).fontColor(P.white).backgroundColor(P.primary).borderRadius(16)
.padding({ left: 20, right: 20, top: 8, bottom: 8 }).margin({ left: 12 })
.onClick(() => { this.showCheckout = false; this.cartItems = [] })
}.margin({ top: 18 }).justifyContent(FlexAlign.Center)
}.width('78%').backgroundColor(P.card).borderRadius(18).padding(20)
Column().layoutWeight(1).width('100%').onClick(() => { this.showCheckout = false })
}.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.6)').zIndex(999)
}
}
// ---------- Tab5 我的:个人资料 + 积分 + 订单记录 ----------
@Component
struct MineContent {
@State showOrders: boolean = false
@State currentOrderTab: number = 0
build() {
Scroll() {
Column() {
// 个人资料卡(头像+用户名+等级+VIP标签+积分+收藏)
Row() {
Text('🧑🎤').fontSize(34).backgroundColor(P.chip).borderRadius(30)
.width(60).height(60).textAlign(TextAlign.Center)
Column() {
Text('怀旧收藏家').fontSize(16).fontWeight(FontWeight.Bold).fontColor(P.text)
Text('LV.6 零食鉴赏师 · 365天').fontSize(10).fontColor(P.textSub).margin({ top: 3 })
Text('VIP会员 · 全场88折').fontSize(9).fontColor(P.primary).backgroundColor('#FFE0B2')
.borderRadius(8).padding({ left: 7, right: 7, top: 2, bottom: 2 }).margin({ top: 5 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 12 })
Column() {
Text('3280').fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.primary)
Text('积分').fontSize(9).fontColor(P.textSub).margin({ top: 2 })
}.margin({ right: 10 })
Column() {
Text('12').fontSize(15).fontWeight(FontWeight.Bold).fontColor(P.green)
Text('收藏').fontSize(9).fontColor(P.textSub).margin({ top: 2 })
}
}.width('94%').backgroundColor(P.card).borderRadius(14).padding(14).margin({ top: 12 })
// 怀旧成就墙(3 枚徽章:辣条王者/糖果达人/汽水收藏家)
Column() {
Text('🏅 怀旧成就墙').fontSize(14).fontWeight(FontWeight.Bold).fontColor(P.text).width('100%')
Text('📼 怀旧口味鉴赏家 · 解锁全系列').fontSize(11).fontColor(P.textSub).width('100%').margin({ top: 6 })
Row() {
Text('🎖️ 辣条王者').fontSize(10).fontColor(P.primary).backgroundColor('#FFE0B2').borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).margin({ right: 8 })
Text('🍫 糖果达人').fontSize(10).fontColor('#E91E63').backgroundColor('#FCE4EC').borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).margin({ right: 8 })
Text('🥤 汽水收藏家').fontSize(10).fontColor('#0277BD').backgroundColor('#E1F5FE').borderRadius(10)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).margin({ right: 8 })
}.width('100%').margin({ top: 10 })
}.width('94%').backgroundColor(P.card).borderRadius(14).padding(14).margin({ top: 12 })
// 我的订单(4 状态 tab:全部/待付款/待收货/已完成)
Column() {
Row() {
Text('📦 我的订单').fontSize(14).fontWeight(FontWeight.Bold).fontColor(P.text)
Column().layoutWeight(1)
Text('查看全部 >').fontSize(10).fontColor(P.primary).onClick(() => { this.showOrders = true })
}.width('100%')
Row() {
ForEach(CART_TABS, (t: string, idx: number) => {
Text(t).fontSize(11)
.fontColor(this.currentOrderTab === idx ? P.primary : P.textSub)
.border({ width: 1, color: this.currentOrderTab === idx ? P.primary : Color.Transparent })
.borderRadius(10).padding({ left: 12, right: 12, top: 5, bottom: 5 }).margin({ right: 8 })
.onClick(() => { this.currentOrderTab = idx })
})
}.width('100%').margin({ top: 10 })
}.width('94%').backgroundColor(P.card).borderRadius(14).padding(14).margin({ top: 12 })
Text('— 时光小卖部 · 童年不散场 —').fontSize(10).fontColor(P.textSub)
.width('94%').textAlign(TextAlign.Center).margin({ top: 14, bottom: 16 })
}.width('100%')
}.scrollable(ScrollDirection.Vertical).width('100%').height('100%')
}
}
更多推荐


所有评论(0)