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 设计理念:小卖部即时光机

整个应用的视觉语言可以概括为"小卖部即时光机":

  1. 复古配色:复古红 #C62828 是老招牌的颜色,怀旧金 #FFC107 是旧奖状的颜色,米白 #FDF5E6 是泛黄信纸的颜色——三种颜色组合在一起,瞬间把人拉回那个年代
  2. emoji 即商品图:没有精美的商品摄影,只用 🐰🦐🌀❄️🌶️🥤 这些 emoji 代表零食——简单、可爱、有手绘感,就像小时候小卖部里手写的价格牌
  3. 卡片即货架:每个零食卡片都是一个小货架,HOT/NEW 标签像小卖部里的"热销"小旗子,分类色标像不同口味的糖纸
  4. 记忆即故事:怀旧墙的每条记忆都有年代标签(1998、2002、1995……),像一本翻旧的相册,按时间轴排列
  5. 社区即共鸣:怀旧社区的帖子都是"还记得胡同里的冰棍车吗"、"集齐水浒卡的那天"这种集体记忆,让用户在评论区找到同代人

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 背景
警告橙#FF7043HOT 标签、点赞数
白色#FFFFFF卡片、按钮文字
绿色#2E7D32NEW 标签、下单成功
怀旧金#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 复古风格细节

  1. emoji 商品图:所有零食都用 emoji 代表(🐰大白兔、🦐咪咪虾条、🌀大大卷、❄️旺旺雪饼、🌶️辣条、🥤北冰洋),没有精美的商品摄影,简单可爱有手绘感
  2. HOT/NEW 标签:热销零食标 HOT(警告橙底白字),新品标 NEW(绿底白字),像小卖部里手写的"热销"小旗子
  3. 原价删除线:每个零食都有原价(originalPrice),用 TextDecorationType.LineThrough 删除线展示,突出优惠价,像老报纸上的促销广告
  4. 年代标签:怀旧墙每条记忆都有"年代 1998"、"年代 2002"标签,像旧相册的日期戳
  5. 点赞红心:记忆和帖子的点赞都用 ❤️ 红心 + 警告橙数字,像旧信纸上的红印章
  6. 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 组件间通信模式

项目采用单向数据流 + 不可变状态更新的通信模式:

  1. 父→子:通过组件属性传递数据(本项目子组件较少,主要是头部组件)
  2. 状态更新:所有状态更新都通过纯函数返回新数组,而非直接修改原数组
    • this.cartItems = withCartQty(this.cartItems, c.id, c.qty - 1)
    • this.cartItems = addToCart(this.cartItems, s)
    • this.cartItems = removeFromCart(this.cartItems, c.id)
  3. 弹框数据:通过 @State 变量传递,如 buySnack: SnackItembuyQty: number
  4. 不可变约束:代码注释明确标注"已修正:禁止使用对象展开,全部显式声明属性",但数组展开 [...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),像一本按时间排列的旧相册:

  1. 校门口的小卖部(1998,986 赞)
  2. 第一次吃麦丽素(2002,802 赞)
  3. 冰棍的夏天(1995,1203 赞)
  4. 北冰洋与聚餐(2005,654 赞)
  5. AD钙奶的童年(2008,927 赞)
  6. 干脆面集卡热(2000,1350 赞)
  7. 大白兔奶糖的爱(1992,746 赞)
  8. 麻辣烫里的辣条(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

  • RetroSnackShopactiveTab: number —— 当前激活的 Tab
  • HomeContentselectedBanner: number —— 当前选中的 banner(未实际使用)
  • SnackListContentsnacks: SnackItem[] + curCat: string + showBuy: boolean + buySnack: SnackItem + buyQty: number
  • MemoryContentmemories: MemoryItem[] + posts: NostalgiaPost[](数据直接赋值,未做交互)
  • CartContentcartItems: CartItem[] + showCheckout: boolean
  • MineContentshowOrders: boolean + currentOrderTab: number(未实际使用)

购物车状态是项目中最复杂的状态管理,通过 4 个纯函数实现不可变更新:

  • addToCart(list, item):添加商品,已存在则数量+1,不存在则新增
  • withCartQty(list, id, qty):更新指定商品数量(1-99 范围限制)
  • removeFromCart(list, id):移除指定商品
  • cartTotal(list) / cartCount(list):计算总价和总件数(纯计算,不修改状态)

4.4 工具函数层

11 个纯函数构成了项目的工具层,分为四类:

  1. 分类与颜色categoryColor(cat)hotColor(isHot)snackByCategory(list, cat)
  2. 价格格式化moneyFormat(n) —— 保留 1 位小数(toFixed(1)
  3. 购物车操作cartTotal(list)cartCount(list)withCartQty(list, id, qty)addToCart(list, item)removeFromCart(list, id)
  4. 其他withMemoryLiked(list, id) —— 记忆点赞+1(未在 UI 中调用)、ratingStars(r) —— 生成星级字符串(未在 UI 中调用)

这些函数都是无副作用的纯函数,输入相同则输出相同,便于测试和复用。其中 ratingStarswithMemoryLiked 虽然定义了但未在 UI 中调用,是预留的扩展功能。


第五章 五大 Tab 页面深度解析

5.1 Tab1:首页·推荐专区(HomeContent)

首页是整个应用的入口,采用纵向滚动信息流布局,从上到下依次为:

今日推荐 Banner
  • 标题栏:🎁 今日推荐 + 查看更多 >
  • 两张 Banner 卡片,复用 bannerCard @Builder:
    1. 童年回忆礼包:复古红 #C62828 背景,“大白兔+辣条+麦丽素”,🍫 图标
    2. 夏日汽水节:蓝色 #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 = sbuyQty = 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 交互反馈的其他方式

除了属性动画,项目还通过以下方式提供交互反馈:

  1. 颜色变化:选中态使用复古红填充(分类 chip、订单 tab),未选中使用浅橙/棕灰
  2. 标签变化:HOT(警告橙)、NEW(绿色)、VIP(复古红+浅橙)等标签提供状态信息
  3. 价格对比:现价复古红粗体 + 原价删除线,视觉上突出优惠
  4. 指示条:底部 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 * qty
  • cartCount:遍历累加 qty

8.4 结算流程

结算流程简单直接:

  1. 购物车页面点击"去结算" → 打开结算成功弹框
  2. 弹框显示实付金额(cartTotal 动态计算)
  3. 点击"再看看" → 关闭弹框,保留购物车
  4. 点击"完成" → 关闭弹框并清空购物车(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 怀旧经济的心理学

时光小卖部的设计暗合了怀旧经济的心理学原理:

  1. 味道是最强的记忆触发器:嗅觉和味觉与大脑的海马体(记忆中枢)直接相连,一口大白兔奶糖的味道能瞬间唤起 20 年前的记忆
  2. 集体记忆产生归属感:当用户发现"原来你也吃过这个",会产生强烈的代际归属感
  3. 怀旧缓解焦虑:在快节奏的现代生活中,怀旧提供了一个安全的心理避风港
  4. 为记忆付费:用户愿意为"童年的味道"支付溢价,因为买到的不只是零食,还有一段回不去的时光

第十章 个人中心与成就体系

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 可优化点

数据层面
  1. 图片资源:当前使用 emoji 作为商品图,零网络请求。若替换为真实商品图片,应使用 Imagealtoffset 等属性优化加载体验,并考虑图片懒加载
  2. 数据持久化:当前购物车、订单等数据为内存状态,重启后丢失。可接入 Preferences 或数据库持久化用户购物车、收藏、订单
  3. 大列表虚拟化:当前 30 款零食数据量小,无需虚拟化。若扩展到数百款,应使用 LazyForEach 实现懒加载
组件层面
  1. 组件拆分:当前单文件 1336 行,可按功能拆分为多个文件(如 components/models/utils/pages/),提升可维护性
  2. 自定义组件复用bannerCardcatChip 等已通过 @Builder 复用,可进一步抽取为独立 @Component 组件
  3. 状态管理:当前使用组件级 @State,购物车状态在 CartContent 内部,零食铺无法直接添加。可引入 AppStorageProvide/Inject 实现全局购物车状态
交互层面
  1. 下拉刷新:零食列表、记忆列表可增加下拉刷新,模拟数据更新
  2. 无限滚动:当前数据量固定,可扩展为分页加载
  3. 搜索功能:头部搜索框为展示用,可实现真实的零食搜索、记忆搜索
  4. 分享功能:记忆和帖子可增加分享到微信、朋友圈等功能
  5. 购买流程完善:当前 buyModal 的"加入购物车"按钮未实际调用 addToCart,应完善购买流程

11.3 扩展方向

功能扩展
  1. 商品详情页:点击零食卡片进入详情页,展示大图、详细描述、用户评价、相关推荐
  2. 评价系统:用户购买后可对零食进行评价(味道、包装、性价比),生成评分
  3. 收藏系统:用户可收藏喜欢的零食,在"我的收藏"中查看
  4. 搜索历史:记录用户搜索历史,提供搜索建议
  5. 地址管理:收货地址管理,支持多个地址
  6. 支付接入:接入真实支付(微信支付、支付宝),完成交易闭环
  7. 物流追踪:订单发货后可追踪物流状态
社区扩展
  1. 发布记忆:用户可发布自己的怀旧记忆,带图片、文字、年代标签
  2. 评论互动:记忆和帖子下可评论、回复、点赞
  3. 关注系统:用户可关注其他怀旧爱好者,查看关注动态
  4. 话题广场:按话题分类(#童年零食#、#校门口的味道#、#集卡回忆#)
  5. 活动运营:定期举办怀旧主题活动(“晒出你的童年零食”、“年代味道评选”)
技术扩展
  1. 多端适配:当前已适配平板横屏,可进一步适配折叠屏、车机、智慧屏
  2. 国际化:当前为中文界面,可扩展多语言支持
  3. 深色模式:当前为固定浅色复古风,可支持深色模式(复古夜景风)
  4. 无障碍:增加屏幕阅读器支持、颜色对比度优化、字体大小适配
  5. 性能监控:接入性能监控 SDK,跟踪页面加载时间、帧率、内存占用

第十二章 复古零食文化与怀旧经济

12.1 复古零食的文化符号学

30 款复古零食不只是食品,更是文化符号:

  • 大白兔奶糖:国民糖果,承载了几代人的甜蜜记忆,甚至成为国礼
  • 辣条:从五毛辣片到麻辣王子,是"校园奢侈零食"的代表,也是中国零食产业的奇迹
  • 北冰洋汽水:北京人的集体记忆,玻璃瓶橘子汽水,是老北京文化的符号
  • 麦丽素:因为电视剧里的"仙丹"梗,成为了童年角色扮演的道具
  • 小浣熊干脆面:水浒卡是 90 后集体的集卡记忆,为了卡片买空一整箱干脆面
  • AD钙奶:娃哈哈的经典产品,是"小朋友的最爱",也是 00 后的童年符号

每一款零食都有自己的故事,这些故事共同构成了 80 后 90 后 00 后的味觉记忆图谱。

12.2 怀旧经济的商业逻辑

怀旧经济是近年来的消费热点,其商业逻辑包括:

  1. 情绪价值 > 功能价值:用户买的不只是零食,更是"童年的味道"、“回不去的时光”,愿意为情绪价值支付溢价
  2. 集体记忆 = 社交货币:在社交媒体上分享"你吃过这个吗"能快速引发同代人共鸣,产生自发传播
  3. IP 化运营:大白兔、北冰洋、健力宝等老品牌通过 IP 联名(大白兔香水、北冰洋雪糕)焕发新生
  4. 场景化消费:怀旧零食适合特定场景(追剧、聚会、送礼),场景化营销能刺激消费
  5. 代际传递:80 后 90 后成为父母后,会把自己的童年零食分享给孩子,实现代际传递

12.3 复古电商的设计启示

时光小卖部的设计为复古主题电商提供了以下启示:

  1. 配色即情绪:复古红 + 怀旧金 + 米白的配色组合,一进入应用就唤起怀旧情绪,比任何文案都有效
  2. emoji 即商品图:用 emoji 代替精美的商品摄影,反而更有"手绘感"、“小卖部感”,降低了内容生产成本,也形成了独特的视觉风格
  3. 记忆即内容:8 条怀旧记忆是最好的"商品详情页"——用户不是因为零食好吃才买,而是因为记忆里的味道才买
  4. 社区即共鸣:6 条社区帖子让用户感受到"不是我一个人在怀旧",社区互动能显著提升用户粘性
  5. 成就即身份: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

DevEco Studio 安装界面

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

13.3 配置SDK

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 代码完整实现了五大核心模块:

  1. 首页·推荐专区:今日推荐 Banner、热门零食横滑、小卖部故事,营造怀旧氛围
  2. 零食铺·分类宫格:30 款复古零食,6 分类筛选,双列宫格,加入购物车弹框
  3. 怀旧墙·时光记忆:8 条怀旧记忆(1992-2010),6 条社区帖子,情感核心
  4. 购物车·清单结算:商品清单、数量加减、删除、合计、结算成功弹框
  5. 我的·个人中心:个人资料、积分收藏、成就墙(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 条社区帖子都是精心挑选的集体记忆——校门口的小卖部、第一次吃麦丽素、冰棍的夏天、干脆面集卡热、北冰洋配撸串……每一条都能让目标用户会心一笑,唤起"原来你也吃过这个"的共鸣。这种集体共鸣是怀旧社区的核心价值,也是用户粘性的来源。

未来展望

短期优化
  1. 完善购买流程:当前 buyModal 的"加入购物车"按钮未实际调用 addToCart,应完善为点击后真正添加到购物车,并跳转到购物车页面
  2. 全局购物车状态:当前购物车状态在 CartContent 内部,零食铺无法直接添加。应引入 AppStorageProvide/Inject 实现全局购物车状态
  3. 商品详情页:点击零食卡片进入详情页,展示大图、详细描述、用户评价、相关推荐
  4. 搜索功能:头部搜索框实现真实的零食搜索、记忆搜索
  5. 数据持久化:购物车、收藏、订单等数据本地存储,重启不丢失
中期扩展
  1. 评价系统:用户购买后可对零食进行评价(味道、包装、性价比),生成评分
  2. 收藏系统:用户可收藏喜欢的零食,在"我的收藏"中查看
  3. 发布记忆:用户可发布自己的怀旧记忆,带图片、文字、年代标签
  4. 评论互动:记忆和帖子下可评论、回复、点赞
  5. 关注系统:用户可关注其他怀旧爱好者,查看关注动态
  6. 支付接入:接入真实支付(微信支付、支付宝),完成交易闭环
  7. 物流追踪:订单发货后可追踪物流状态
长期愿景
  1. IP 化运营:与大白兔、北冰洋、健力宝等老品牌合作,推出联名款零食、周边
  2. 线下快闪店:在线下开设"时光小卖部"快闪店,线上线下联动
  3. 年代主题活动:定期举办 80 后、90 后、00 后年代主题活动,“晒出你的童年零食”
  4. AI 怀旧助手:基于用户的童年记忆,AI 推荐"你可能还记得的零食",生成个性化怀旧清单
  5. 多端联动:手机、平板、车机、智慧屏多端同步,在车机上也能"听"怀旧故事
    平板横屏·零食铺

时光小卖部 · 童年不散场。

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%')
  }
}
Logo

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

更多推荐