鸿蒙原生应用 ArkUI 声明式:积分商城首页的原价划线与缺货置灰

App 16「校园积分商城」首页(HomeTab),主题色 #F43F5E 玫瑰红,4 个 Tab 分别为首页(🛍️)、商品(🎁)、兑换(🔄)、我的(👤)。首页采用固定 Header + 玫瑰红渐变 HeroCard(限时抽奖 + 3 栏积分统计 + 搜索框)+ 快捷功能入口(实物/虚拟/秒杀/抽奖)+ 本周积分获取条形图 + 5 个热门商品卡片(原价划线 + 库存进度条 + 缺货置灰)+ 兑换排行榜。本篇基于 16-points-mall/entry/src/main/ets/pages/HomeTab.ets(约 333 行)逐段拆解,附 4 张实机截图。

一、整体结构:固定 Header + 内容流

HomeTab 与 App 14/15 首页骨架一致——固定 Header + Scroll 内容流,7 个 @Builder 块:

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 14 }) {
        this.HeroCard()
        this.QuickActionRow()
        this.WeeklyChart()
        this.SectionTitle('热门商品', '查看全部')
        this.ProductList()
        this.SectionTitle('兑换排行', '更多')
        this.RankList()
      }
      .width('100%')
      .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
    }
    .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
  }
  .width('100%').height('100%').backgroundColor(C.bg)
}

7 个块按"活动 → 入口 → 数据 → 商品 → 排行"五段叙事:HeroCard(限时抽奖 + 3 栏统计 + 搜索)→ QuickActionRow(4 个兑换入口)→ WeeklyChart(本周积分获取)→ SectionTitle('热门商品') + ProductList(5 个商品卡)→ SectionTitle('兑换排行') + RankList(3 条排行)。

与 App 15 首页对比:App 15 主内容是"分类指南"(知识卡),App 16 主内容是"热门商品"(电商卡)——首页主内容反映产品形态(App 15 是知识库、App 16 是商城)。

校园积分商城首页首屏 · 玫瑰红渐变HeroCard+限时抽奖+3栏积分统计+快捷入口+趋势图

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

配图

二、Header + HeroCard:抽奖 Banner + 积分统计

Header 与 App 15 同构("积分商城" + Slogan"积分换好礼,学习更有动力")。HeroCard 的活动横幅是限时抽奖

@Builder
HeroCard() {
  Column({ space: 14 }) {
    Row({ space: 12 }) {
      Text('🎰').fontSize(36)
      Column({ space: 4 }) {
        Text('限时抽奖活动').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Text('100 积分抽一次,赢大奖').fontSize(12).fontColor('#FFFFFF').opacity(0.85)
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('参与 >').fontSize(12).fontColor('#FFFFFF')
    }
    .width('100%')
    .onClick(() => { promptAction.showToast({ message: '积分抽奖' }); })

    Row() {
      ForEach(this.stats, (s: StatItem) => {
        Column({ space: 4 }) {
          Text(s.value).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text(s.label).fontSize(11).fontColor('#FFFFFF').opacity(0.8)
        }
        .layoutWeight(1)
      }, (s: StatItem) => s.label)
    }
    .width('100%')

    Row({ space: 8 }) {
      TextInput({ placeholder: '搜索商品...', text: this.keyword })
        .onChange((val: string) => { this.keyword = val; })
        .backgroundColor('#33FFFFFF').borderRadius(D.rSm).height(40)
        .layoutWeight(1).placeholderColor('#FFFFFF').placeholderFont({ size: 13 })
        .fontColor('#FFFFFF')
      Text('🔍')
        .fontSize(16).width(40).height(40).textAlign(TextAlign.Center)
        .backgroundColor('#FFFFFF').borderRadius(D.rSm)
        .onClick(() => {
          promptAction.showToast({ message: this.keyword.length > 0 ? '搜索: ' + this.keyword : '请输入关键词' });
        })
    }
    .width('100%')
  }
  .width('100%')
  .padding(16)
  .borderRadius(D.rLg)
  .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}

"限时抽奖:100 积分抽一次,赢大奖"——电商 App 的"抽奖引流"文案,用"小投入博大奖"的心理驱动用户消耗积分(把积分花出去,形成消耗闭环)。与 App 15 的"减碳种树"(公益激励)不同,App 16 是"抽奖"(利益驱动)——活动 Banner 的激励类型跟随产品定位

3 栏统计可用积分 2,860 / 累计积分 5,120 / 已兑换 12——"可用"与"累计"分开(可用=还能花的,累计=总共赚过的),这是积分体系的专业展示(微信支付余额/总消费同理)。"已兑换 12"与快捷入口/我的页统计一致

搜索框与 App 15 完全同构(半透明白 + 白色占位 + 🔍)。

三、QuickActionRow:4 个兑换入口

QuickActionRow 是 4 个积分功能入口:

private quickActions: QuickAction[] = [
  { id: 1, emoji: '🎁', label: '实物兑换' },
  { id: 2, emoji: '🎫', label: '虚拟兑换' },
  { id: 3, emoji: '⚡', label: '限时秒杀' },
  { id: 4, emoji: '🎰', label: '积分抽奖' }
];

4 个入口对应商城的 4 种兑换形态:实物兑换(🎁)、虚拟兑换(🎫 会员/优惠券)、限时秒杀(⚡ 限时抢购)、积分抽奖(🎰 博彩)。"入口覆盖完整业务"——用户想换啥都能找到入口。

入口卡片:48×48 正圆(C.primarySoft 浅玫瑰红)+ emoji + 文字,与 App 14/15 同构(App 15 是四色,App 16 回归单色浅底——"单色 vs 多色"取决于业务是否需要颜色编码,商城没有"分类色"需求,单色更简洁)。

四、WeeklyChart:本周积分获取

WeeklyChart 与 App 14/15 完全同构(7 天条形图),标题和汇总换为积分语义:

@Builder
WeeklyChart() {
  Column({ space: 12 }) {
    Row() {
      Text('本周积分获取').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
      Blank()
      Text('+385 积分').fontSize(12).fontColor(C.primary)
    }
    .width('100%')

    Row({ space: 4 }) {
      ForEach(this.barData, (b: BarItem) => {
        Column({ space: 6 }) {
          Stack({ alignContent: Alignment.Bottom }) {
            Column()
              .width(18).height(b.value)
              .linearGradient({ angle: 180, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
              .borderRadius(4)
          }
          .width(24).height(90)
          Text(b.day).fontSize(10).fontColor(C.textDim)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
      }, (b: BarItem) => b.day)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card).borderRadius(D.rMd)
  .border({ width: 1, color: C.stroke })
}

数据:一 30 / 二 55 / 三 40 / 四 70 / 五 85 / 六 60 / 日 45——周五 85 最高(周五"刷任务赚积分"高峰)。"+385 积分"的数学:30+55+40+70+85+60+45 = 385——总计 = 各天之和,数据自洽(这是本系列第一次"汇总 = 各柱之和"完全成立,App 15 的"平均 68%"也是,但 App 15 我的页"47 次"与趋势图口径不一致的坑在这里避免了——App 16 的首页与我的页都用同一份 barData + "+385 积分",跨页口径统一)。

五、ProductList:5 个商品卡(本页核心)

ProductList 是首页的"电商主内容"——5 个商品卡,每个含原价划线、库存进度条、缺货置灰、动态按钮

@Builder
ProductList() {
  Column({ space: 12 }) {
    ForEach(this.products, (p: Product) => {
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Row() {
            Text(p.emoji).fontSize(28)
          }
          .width(56).height(56)
          .backgroundColor(C.primarySoft).borderRadius(D.rMd)
          .justifyContent(FlexAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(p.name).fontSize(15).fontWeight(FontWeight.Medium).fontColor(C.text)
              Text(p.tag)
                .fontSize(9).fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                .backgroundColor(p.tagColor).borderRadius(3)
            }
            .width('100%')
            Row({ space: 6 }) {
              Row({ space: 1 }) {
                ForEach([0, 1, 2, 3, 4], (i: number) => {
                  Text(i < p.rating ? '★' : '☆')
                    .fontSize(11).fontColor(i < p.rating ? C.warn : C.textDim)
                }, (i: number) => i.toString())
              }
              Text('原价 ' + p.original).fontSize(11).fontColor(C.textDim).decoration({ type: TextDecorationType.LineThrough })
            }
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)

          Button(p.stock > 0 ? '兑换' : '缺货')
            .fontSize(12).fontColor('#FFFFFF')
            .backgroundColor(p.stock > 0 ? C.primary : C.textDim)
            .borderRadius(D.rSm).height(32)
            .onClick(() => {
              if (p.stock > 0) {
                promptAction.showToast({ message: '兑换 ' + p.name + ' 消耗 ' + p.points + ' 积分' });
              } else {
                promptAction.showToast({ message: '暂无库存' });
              }
            })
        }
        .width('100%')

        Row({ space: 8 }) {
          Text('库存').fontSize(11).fontColor(C.textDim)
          Progress({ value: (p.total - p.stock) * 100 / p.total, total: 100 })
            .color(C.primary).backgroundColor(C.cardSoft)
            .layoutWeight(1).height(6)
          Text(p.stock + '/' + p.total).fontSize(11).fontColor(C.textSub)
        }
        .width('100%')

        Row() {
          Text(p.points + ' 积分').fontSize(14).fontColor(C.primary).fontWeight(FontWeight.Bold)
          Blank()
          Text('立即兑换 >').fontSize(11).fontColor(C.primary)
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor(C.card).borderRadius(D.rMd)
      .border({ width: 1, color: C.stroke })
      .onClick(() => { promptAction.showToast({ message: p.name }); })
    }, (p: Product) => p.id.toString())
  }
  .width('100%')
}

5.1 商品数据与标签

private products: Product[] = [
  { id: 1, emoji: '🎧', name: '蓝牙耳机', tag: '热门', tagColor: C.danger, points: 800, original: 1200, rating: 5, stock: 3, total: 10 },
  { id: 2, emoji: '📚', name: '学习笔记本', tag: '新品', tagColor: C.primary, points: 200, original: 350, rating: 4, stock: 15, total: 20 },
  { id: 3, emoji: '☕', name: '保温杯', tag: '推荐', tagColor: C.warn, points: 500, original: 800, rating: 5, stock: 8, total: 15 },
  { id: 4, emoji: '🎒', name: '校园背包', tag: '限量', tagColor: C.danger, points: 1200, original: 2000, rating: 5, stock: 2, total: 5 },
  { id: 5, emoji: '🎮', name: '游戏手柄', tag: '热门', tagColor: C.danger, points: 600, original: 900, rating: 4, stock: 0, total: 10 }
];

5 个商品的"电商四件套"

  • 名称 + 标签:热门(红)/新品(主色)/推荐(橙)/限量(红)——标签颜色三态(danger/primary/warn),"新品"用主色、其他用警示色
  • 星级:5 星/4 星——评分
  • 原价划线TextDecorationType.LineThrough 删除线(原价 1200)——"划线价"是电商的经典促销手法(显示原价被划掉,暗示"现在便宜了")
  • 积分价:底部 800 积分 加粗主色 + "立即兑换 >"

decoration({ type: TextDecorationType.LineThrough }) 是 ArkUI 的文本装饰——删除线/下划线/上划线。"划线价"用官方 API 实现,比手动加"──"字符专业得多

5.2 库存进度条 + 缺货置灰

库存进度条与 App 14/15 同构(Progress + 已兑换占比):

Row({ space: 8 }) {
  Text('库存').fontSize(11).fontColor(C.textDim)
  Progress({ value: (p.total - p.stock) * 100 / p.total, total: 100 })
    .color(C.primary).backgroundColor(C.cardSoft)
    .layoutWeight(1).height(6)
  Text(p.stock + '/' + p.total).fontSize(11).fontColor(C.textSub)
}

(p.total - p.stock) * 100 / p.total:已兑换占比(蓝牙耳机 7/10=70%、游戏手柄 10/10=100%)。库存数字 "3/10" 双通道

缺货置灰是电商页的关键细节

Button(p.stock > 0 ? '兑换' : '缺货')
  .fontSize(12).fontColor('#FFFFFF')
  .backgroundColor(p.stock > 0 ? C.primary : C.textDim)
  .borderRadius(D.rSm).height(32)
  .onClick(() => {
    if (p.stock > 0) {
      promptAction.showToast({ message: '兑换 ' + p.name + ' 消耗 ' + p.points + ' 积分' });
    } else {
      promptAction.showToast({ message: '暂无库存' });
    }
  })

游戏手柄 stock=0 → 按钮文字变"缺货"、背景变灰色(C.textDim)、点击弹"暂无库存"——三处联动(文案/颜色/点击逻辑)。"缺货状态"是电商的必备状态(淘宝"已售罄"、京东"无货"),"置灰 + 文案 + 拦截"是标准实现

库存 2/5 的校园背包(限量)进度条 60%、0/10 的游戏手柄(热门)进度条 100%——"缺货商品反而是最热门的"(卖光了),符合电商逻辑。

5.3 底部积分价 + 立即兑换

Row() {
  Text(p.points + ' 积分').fontSize(14).fontColor(C.primary).fontWeight(FontWeight.Bold)
  Blank()
  Text('立即兑换 >').fontSize(11).fontColor(C.primary)
}

积分价大字加粗主色(800 积分)+ 右侧"立即兑换 >"——价格是卡片最重的信息(电商原则:价格要醒目)。

六、RankList:兑换排行

RankList 与 App 14/15 同构(3 条排行 + 奖牌 + 次数 + 增长率):

private rankings: RankItem[] = [
  { id: 1, emoji: '🥇', name: '蓝牙耳机', count: '128 次兑换', trend: '+22%' },
  { id: 2, emoji: '🥈', name: '保温杯', count: '96 次兑换', trend: '+15%' },
  { id: 3, emoji: '🥉', name: '学习笔记本', count: '85 次兑换', trend: '+10%' }
];

排行与商品列表呼应:蓝牙耳机(128 次第 1)正好是商品列表第一条、5 星、库存仅 3/10——"最热商品"三重印证(排行第 1 + 列表首位 + 库存快光)。"数据互相印证"让首页自洽可信

增长趋势(+22%/+15%/+10%)递减——第 1 名涨得最快、第 3 名涨得最慢,"强者恒强"的电商马太效应。

校园积分商城首页底部 · 兑换排行完整+底部Tab栏

七、电商首页的 5 个设计要点

App 16 首页是系列第一个"电商型首页",5 个电商专属设计值得沉淀:

  1. 原价划线LineThrough)——"原价 vs 现价"的促销感
  2. 库存进度条 + 库存数字——"稀缺感"驱动尽快兑换(3/10 剩 7 件,用户会想"快没了")
  3. 缺货置灰——stock > 0 三元:按钮文字/颜色/点击逻辑三联动
  4. 标签体系——热门/新品/推荐/限量,颜色区分(红/主色/橙)
  5. 积分价醒目——底部大字加粗主色,"价格 = 页面最重信息"

**"稀缺感(库存进度条)+ 促销感(划线价)+ 紧迫感(限时秒杀)"**是电商转化的三驾马车,App 16 首页集齐了两个半(秒杀在快捷入口)。

校园积分商城首页中段 · 热门商品列表前2个商品卡+原价划线+库存进度条+兑换按钮

八、与 App 14/15 首页的模板对比

维度App 14 运动App 15 环保App 16 商城
主题色绿 #16A34A绿 #22C55E玫红 #F43F5E
Hero 活动运动会报名减碳种树限时抽奖
快捷入口4 运动4 分类(四色)4 兑换形态
主列表场地卡指南卡商品卡(划线价+库存+缺货)
排行场地预约垃圾识别商品兑换
特色预约进度掌握度+学测划线价+缺货态

"内容流首页"模板第 4 次复用(App 13/14/15/16),每次换主题 + 换数据 + 换模块——"模板复用 + 主题演化"方法论稳定输出

校园积分商城首页下部 · 热门商品列表完整+缺货置灰+兑换排行

九、数据一致性与积分体系

App 16 首页的积分数据跨页一致:

  • 可用积分 2,860 = 我的页"积分 2,860" ✓
  • 累计积分 5,120 = 我的页 Slogan"累计 5,120 积分" ✓
  • 已兑换 12 = 兑换页"总兑换 12" ✓
  • +385 积分 = 我的页"+385 积分" ✓(本系列跨页一致性最完整的 App

积分体系的双向流动(App 16 的"积分经济"完整闭环):

赚积分(本周 +385:签到/任务/抽奖…)→ 积分池(可用 2,860)→ 花积分(兑换商品 800/500/200…)→ 记录(兑换页时间线)

首页展示"池"(可用/累计)→ 商品页花"池"(兑换)→ 兑换页记"账"(记录)→ 我的页管"户"(签到/任务)——4 个 Tab 围绕"积分"形成完整闭环,这是 App 16 数据架构最漂亮的地方。

十、电商首页的信息架构与转化设计

App 16 首页的信息架构遵循电商"浏览 → 心动 → 行动"的转化漏斗:

1. 浏览层(视觉吸引):HeroCard 抽奖活动(🎰 参与 >)+ 趋势图——让用户"先感兴趣"。

2. 认知层(建立需求):4 个快捷入口(实物/虚拟/秒杀/抽奖)——让用户知道"这里能换什么"。

3. 心动层(商品展示):5 个热门商品卡——核心转化区。每个卡的"电商心理武器":

  • 划线原价原价 1200 删除线)→ "省了 400 积分"的赚感
  • 库存进度条(3/10)→ "快没了"的稀缺感
  • 标签(热门/限量)→ "大家都在换"的从众感
  • 积分价大字(800 积分)→ 价格的明确呈现

4. 行动层(点击转化):"兑换"按钮(缺货变"缺货"灰按钮拦截)+"立即兑换 >"——每个卡片都有明确的行动入口

"浏览 → 认知 → 心动 → 行动"四层漏斗是电商首页的标准结构(淘宝/京东首页都如此)。每一层都有"让用户往下走"的设计:抽奖(好奇心)→ 入口(导航)→ 划线价(赚感)→ 按钮(行动)。

对比 App 14/15 首页:App 14/15 的漏斗是"浏览 → 认知 → 行动"(无"心动"层——场地/指南卡没有促销心理),App 16 多了"心动"层(划线价+稀缺感)——"电商专属的转化设计"

十一、缺货态的完整处理

游戏手柄(stock=0)是本页唯一的缺货商品,它的处理是本页值得细读的"缺货样板":

1. 按钮态:文字"缺货" + 灰色背景(C.textDim)——从"行动按钮"变"状态提示"

2. 点击拦截if (p.stock > 0) { 兑换 } else { Toast "暂无库存" }——缺货点击不执行兑换,只提示。

3. 库存条(10-0)*100/10 = 100% 满进度条 + "0/10"——"卖光了"的事实呈现

4. 排名呼应:游戏手柄在排行里没出现(排行是蓝牙耳机/保温杯/笔记本)——缺货商品不入排行(没货还排第一会误导)。

"缺货 ≠ 隐藏":游戏手柄仍展示(用户能看到"这个商品很抢手卖光了"),只是不能买——"缺货展示"本身是营销(制造"下次要抢"的期待感)。淘宝的"已售罄"同理——"缺货即广告"

真实项目的缺货处理进阶:① 缺货商品支持"到货提醒"(订阅按钮);② 缺货原因展示("限量 5 件已售罄");③ 相似商品推荐("看看保温杯")。"缺货态的转化设计"是电商的进阶能力。App 16 首页把这套"电商漏斗 + 缺货样板"完整呈现,读者可直接照搬到自己项目的商品列表页。记住:电商首页的每一寸设计都在为"转化"服务

十二、小结

App 16 首页解析完毕。原价划线 + 库存进度条 + 缺货置灰 + 标签体系 + 积分闭环是五大亮点,配合玫瑰红商城主题(抽奖/秒杀/兑换),让首页成为"电商型首页"的标准范本。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐