引言

在移动互联网社交电商浪潮持续深入的今天,"砍价免费拿"作为一种极具病毒传播性的营销模式,已经成为众多电商平台获客与促活的核心策略。这种模式通过好友间的社交裂变,将商品价格从原价逐步砍至零元,既满足了用户对实惠的心理预期,又天然具备社交分享的传播动力。本应用正是基于HarmonyOS ArkTS声明式UI框架,完整构建了一个从砍价大厅、个人砍价管理、助力记录、免费战绩到省钱日报的五模块全链路社交电商应用,涵盖了商品展示、进度追踪、社交互动、数据可视化等核心业务场景。

从技术架构层面来看,本应用采用了典型的"数据模型层—纯函数业务层—声明式UI层"三段式架构。数据模型层通过一系列TypeScript接口(interface)定义了色彩调色板、砍价商品、我的砍价、助力记录、免费商品、每日统计、省钱达人等七个核心数据结构,为整个应用提供了强类型的数据契约保障。纯函数业务层封装了进度计算、活跃数量统计、金额汇总等业务逻辑,确保数据处理的可测试性和可复用性。声明式UI层则充分利用了ArkTS的@Entry、@Component、@State、@Builder等装饰器能力,构建出高度组件化、状态驱动的用户界面。

从设计哲学层面来看,本应用遵循了"情感化设计"与"游戏化激励"两大原则。主题色采用砍价红(#D32F2F)搭配助力金(#FFC107),红色传递紧迫感与行动力,金色暗示价值与收获,两种色彩形成强烈的视觉张力。在交互层面,应用设计了砍价进度条、助力头像堆叠、聊天气泡式记录、报纸式日报、柱状图数据可视化等丰富的视觉元素,将原本枯燥的价格削减过程转化为一场充满社交乐趣的"砍价游戏"。底部五Tab导航结构清晰,用户可以在大厅发现商品、管理自己的砍价进度、查看好友助力、回顾免费战绩、阅读省钱日报,形成完整的用户使用闭环。

一、数据模型定义与色彩设计系统

在任何复杂应用中,数据模型的定义是整个架构的基石。本应用在文件顶部定义了七个核心接口和一个色彩调色板常量,为后续所有组件提供了统一的数据契约。通过TypeScript的interface机制,每个数据实体的字段类型、命名规范和业务含义都得到了精确约束,这在大规模协作开发中尤为重要。

interface ColorPalette {
  primary: string
  primaryLight: string
  primaryDark: string
  accent: string
  accentLight: string
  bg: string
  cardBg: string
  textPrimary: string
  textSecondary: string
  textHint: string
  border: string
  success: string
  warning: string
  danger: string
  white: string
  gold: string
}

interface BargainItem {
  id: number
  name: string
  category: string
  originalPrice: number
  targetPrice: number
  currentPrice: number
  cutAmount: number
  participants: number
  hot: boolean
  imageColor: string
  description: string
}

interface MyBargainItem {
  id: number
  itemName: string
  originalPrice: number
  targetPrice: number
  currentPrice: number
  progress: number
  remainingTime: string
  helpers: string[]
  imageColor: string
  status: string
  category: string
}

interface HelpRecord {
  id: number
  friendName: string
  avatarColor: string
  itemName: string
  cutAmount: number
  timestamp: string
  message: string
  isMe: boolean
}

interface FreeItem {
  id: number
  itemName: string
  originalPrice: number
  obtainedDate: string
  imageColor: string
  category: string
  daysUsed: number
  review: string
  rating: number
}

interface DailyStat {
  date: string
  bargainCount: number
  savedAmount: number
}

interface TopSaver {
  name: string
  amount: number
  avatarColor: string
}

在这里插入图片描述

上述代码定义了应用的核心数据模型体系。ColorPalette接口定义了完整的16色设计令牌系统,包含主色、辅助色、背景色、文字色、边框色、状态色等,确保整个应用的色彩使用统一规范。BargainItem接口描述了砍价大厅中展示的商品信息,包含原价、砍后价、每刀可砍金额、参砍人数等关键字段。MyBargainItem接口则描述了用户自己发起的砍价任务,额外包含进度百分比、剩余时间、助力好友列表等追踪字段。

HelpRecord接口用于助力记录页的聊天气泡展示,其中isMe布尔字段巧妙地区分了好友助力和我方发起的砍价消息。FreeItem接口描述了已免费获得的商品,包含使用天数、评价文本和星级评分,为用户提供了使用后的反馈闭环。DailyStatTopSaver则分别服务于省钱日报页的数据统计和排行榜展示。

二、设计令牌常量与分类配置

设计令牌(Design Tokens)是现代前端设计系统的核心概念,它将颜色、字体、间距等视觉属性抽象为可复用的常量,确保设计一致性。本应用通过const COLORSBARGAIN_CATEGORY_CONFIG两个常量定义了全局设计系统。

const COLORS: ColorPalette = {
  primary: '#D32F2F',
  primaryLight: '#FFCDD2',
  primaryDark: '#B71C1C',
  accent: '#FFC107',
  accentLight: '#FFF8E1',
  bg: '#FFF8F6',
  cardBg: '#FFFFFF',
  textPrimary: '#212121',
  textSecondary: '#757575',
  textHint: '#BDBDBD',
  border: '#FFEBEE',
  success: '#43A047',
  warning: '#FB8C00',
  danger: '#D32F2F',
  white: '#FFFFFF',
  gold: '#FFD700'
}

const BARGAIN_CATEGORY_CONFIG: Record<string, string> = {
  '数码': '📱',
  '家电': '📺',
  '美妆': '💄',
  '服饰': '👔',
  '食品': '🍪',
  '家居': '🛋️',
  '母婴': '🍼',
  '运动': '⚽'
}

色彩系统以砍价红#D32F2F为主色调,搭配浅红#FFCDD2和深红#B71C1C形成完整的红色色阶。辅助色采用助力金#FFC107,用于强调砍价金额、进度等关键信息。背景色#FFF8F6是一种带有极淡暖色调的白色,为应用营造出温暖而不刺眼的视觉基底。文字色彩分为三级:主文字#212121、次要文字#757575、提示文字#BDBDBD,形成清晰的视觉层次。

BARGAIN_CATEGORY_CONFIG使用Record<string, string>类型映射了八个商品分类到Emoji图标的对应关系。这种设计避免了在UI组件中硬编码图标,使得分类图标的管理集中化,后续新增分类只需修改此常量即可。在商品卡片渲染时,通过BARGAIN_CATEGORY_CONFIG[item.category]即可获取对应的Emoji图标,实现了数据与展示的解耦。

三、Mock数据层与业务纯函数

在缺少后端API的开发阶段,Mock数据是支撑前端独立开发的关键基础设施。本应用定义了六组Mock数据,覆盖了砍价商品、我的砍价、助力记录、免费商品、每日统计和省钱达人等全部业务场景。配合纯函数封装的数据处理逻辑,实现了完整的数据驱动UI效果。

const mockBargainItems: BargainItem[] = [
  { id: 1, name: '智能扫地机器人扫拖一体', category: '家电', originalPrice: 1899, targetPrice: 0, currentPrice: 850, cutAmount: 30, participants: 12847, hot: true, imageColor: '#546E7A', description: '激光导航扫拖一体,自动集尘' },
  { id: 2, name: '苹果平板电脑10.2英寸', category: '数码', originalPrice: 2499, targetPrice: 0, currentPrice: 1200, cutAmount: 50, participants: 9823, hot: true, imageColor: '#455A64', description: '官方正品,全新未拆封' },
  { id: 3, name: '进口大牌香水50ml', category: '美妆', originalPrice: 899, targetPrice: 0, currentPrice: 380, cutAmount: 25, participants: 8432, hot: false, imageColor: '#AD1457', description: '经典款淡香水,持久留香' },
  { id: 4, name: '轻奢真皮双肩包', category: '服饰', originalPrice: 699, targetPrice: 0, currentPrice: 290, cutAmount: 20, participants: 7321, hot: false, imageColor: '#5D4037', description: '头层牛皮,大容量防盗' },
  { id: 5, name: '破壁机家用静音款', category: '家电', originalPrice: 1099, targetPrice: 0, currentPrice: 520, cutAmount: 35, participants: 6542, hot: true, imageColor: '#37474F', description: '低音降噪,八叶刀头' },
  { id: 6, name: '蓝牙降噪耳机旗舰版', category: '数码', originalPrice: 1299, targetPrice: 0, currentPrice: 640, cutAmount: 40, participants: 11205, hot: true, imageColor: '#263238', description: '主动降噪,40小时续航' }
]

function getBargainProgress(item: BargainItem): number {
  if (item.originalPrice <= 0) { return 0 }
  return Math.floor((item.originalPrice - item.currentPrice) / item.originalPrice * 100)
}

function getActiveBargainCount(): number {
  let count: number = 0
  for (let i = 0; i < mockMyBargains.length; i++) {
    if (mockMyBargains[i].status === '砍价中') { count++ }
  }
  return count
}

function getFreeItemTotal(): number {
  let total: number = 0
  for (let i = 0; i < mockFreeItems.length; i++) { total += mockFreeItems[i].originalPrice }
  return total
}

function getTotalSavedAmount(): number {
  let total: number = 0
  for (let i = 0; i < mockDailyStats.length; i++) { total += mockDailyStats[i].savedAmount }
  return total
}

function getHelpRecordCount(): number {
  let count: number = 0
  for (let i = 0; i < mockHelpRecords.length; i++) {
    if (!mockHelpRecords[i].isMe) { count++ }
  }
  return count
}

Mock数据层采用了贴近真实业务的商品数据,涵盖了家电、数码、美妆、服饰、食品、家居、母婴、运动等八大品类共16件商品,每件商品都配有名称、分类、原价、当前价、每刀可砍金额、参砍人数、热度标记、图标颜色和描述信息。这种丰富的数据为UI展示提供了真实的视觉效果。

纯函数是业务逻辑层的核心设计。getBargainProgress函数接收一个BargainItem,通过计算原价与当前价的差值占原价的百分比来得出砍价进度,并使用Math.floor取整确保返回整数百分比。getActiveBargainCount遍历我的砍价列表,统计状态为"砍价中"的记录数量,该函数被用于"我的砍价"页头部展示进行中数量。getFreeItemTotalgetTotalSavedAmount分别汇总免费商品总价值和每日累计省钱金额。getHelpRecordCount则通过isMe字段过滤出好友助力的记录数,用于助力记录页的统计展示。

四、Tab枚举与入口组件架构

入口组件是整个应用的骨架,负责页面路由管理和底部导航控制。本应用使用枚举类型定义Tab索引,通过@State状态驱动条件渲染实现页面切换,这是一种轻量级但高效的路由方案。

enum BargainTab {
  HALL = 0,
  MY = 1,
  HELP = 2,
  FREE = 3,
  DAILY = 4
}

@Entry
@Component
struct BargainApp {
  @State activeTab: BargainTab = BargainTab.HALL

  @Builder contentArea() {
    Column() {
      if (this.activeTab === BargainTab.HALL) {
        BargainHallContent()
      } else if (this.activeTab === BargainTab.MY) {
        MyBargainContent()
      } else if (this.activeTab === BargainTab.HELP) {
        HelpRecordContent()
      } else if (this.activeTab === BargainTab.FREE) {
        FreeItemsContent()
      } else {
        DailyReportContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: BargainTab) {
    Column() {
      Text(icon).fontSize(20).opacity(this.activeTab === tab ? 1.0 : 0.4)
      Text(label).fontSize(9)
        .fontColor(this.activeTab === tab ? COLORS.primary : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Column() {
      this.contentArea()
      Row() {
        this.bottomTabItem('🪓', '砍价大厅', BargainTab.HALL)
        this.bottomTabItem('⏳', '我的砍价', BargainTab.MY)
        this.bottomTabItem('🤝', '助力记录', BargainTab.HELP)
        this.bottomTabItem('🎁', '免费拿', BargainTab.FREE)
        this.bottomTabItem('📰', '省钱日报', BargainTab.DAILY)
      }
      .width('100%')
      .backgroundColor(COLORS.white)
      .padding({ top: 4, bottom: 8 })
      .shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
    }
    .width('100%').height('100%')
    .backgroundColor(COLORS.bg)
  }
}

在这里插入图片描述

BargainTab枚举定义了五个标签页的索引值,使用枚举而非魔法数字使代码意图更清晰。@Entry装饰器标记BargainApp为应用入口组件,@Component声明其为自定义组件。@State activeTab是驱动页面切换的核心状态变量,初始值为HALL即砍价大厅页。

contentArea是一个@Builder方法,通过if-else条件渲染链根据activeTab的值渲染对应的子组件。这种设计将页面切换逻辑集中在一处,便于维护和扩展。bottomTabItem是另一个@Builder方法,它接收图标、标签和Tab枚举三个参数,生成可复用的底部导航项。通过this.activeTab === tab的三元运算,动态设置选中态的透明度、字体颜色和粗细,实现了视觉反馈。底部导航栏整体设置了shadow阴影效果,通过offsetY: -2使阴影向上投射,营造出导航栏悬浮于内容区域之上的层次感。

五、砍价大厅页与商品卡片构建

砍价大厅是应用的核心页面,展示所有可砍价的商品列表。该页面包含头部统计区、分类横滚筛选条、商品大卡片列表,以及发起砍价和商品详情两个模态弹框。页面使用了Stack容器来实现弹框的层叠展示效果。

@Component
struct BargainHallContent {
  @State showStartModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedBargain: BargainItem | null = null
  @State selectedCategory: string = '全部'
  categories: string[] = ['全部', '数码', '家电', '美妆', '服饰', '食品', '家居', '母婴', '运动']

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.5)')
      .onClick(onClose)
  }

  @Builder bargainCardBuilder(item: BargainItem) {
    Column() {
      Row() {
        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[item.category] ?? '🎁').fontSize(36)
        }.width(90).height(90).backgroundColor(item.imageColor + '20').borderRadius(14)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Row() {
            Text(item.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
            if (item.hot) {
              Text('🔥').fontSize(12).margin({ left: 4 })
            }
          }
          Text(item.description).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 3 }).maxLines(1)
          Row() {
            Text('¥0').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
            Text('砍后免费拿').fontSize(9).fontColor(COLORS.primary).margin({ left: 4 })
            Text('¥' + item.originalPrice).fontSize(10).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 8 })
          }
          .margin({ top: 5 })
          Row() {
            Text('每人可砍 ¥' + item.cutAmount).fontSize(9).fontColor(COLORS.accent)
            Text('· ' + item.participants + '人已参砍').fontSize(9).fontColor(COLORS.textHint).margin({ left: 6 })
          }
          .margin({ top: 4 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
      }
      .width('100%')

      Row() {
        Column() {
          Text('砍价进度').fontSize(9).fontColor(COLORS.textSecondary)
          Row() {
            Column()
              .width(getBargainProgress(item) + '%')
              .height(6).backgroundColor(COLORS.primary).borderRadius(3)
            Column().layoutWeight(1)
          }
          .width('100%').height(6).backgroundColor(COLORS.border).borderRadius(3).margin({ top: 3 })
        }.layoutWeight(1)

        Text('发起砍价').fontSize(12).fontColor('#FFFFFF')
          .backgroundColor(COLORS.primary).borderRadius(16)
          .padding({ left: 18, right: 18, top: 8, bottom: 8 })
          .margin({ left: 12 })
          .onClick(() => { this.selectedBargain = item; this.showStartModal = true })
        Text('详情').fontSize(12).fontColor(COLORS.primary)
          .backgroundColor(COLORS.primaryLight).borderRadius(16)
          .padding({ left: 14, right: 14, top: 8, bottom: 8 })
          .margin({ left: 6 })
          .onClick(() => { this.selectedBargain = item; this.showDetailModal = true })
      }
      .width('100%').margin({ top: 10 })
    }
    .width('100%').backgroundColor(COLORS.white).borderRadius(14)
    .padding(14)
    .margin({ left: 12, right: 12, top: 8 })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('🪓').fontSize(22).margin({ left: 14 })
            TextInput({ placeholder: '搜索想砍的商品...' })
              .placeholderColor('#FFFFFF99').fontSize(13).layoutWeight(1)
              .backgroundColor('#FFFFFF33').borderRadius(20)
              .margin({ left: 8, right: 8 })
            Text('📢').fontSize(22).margin({ right: 14 })
          }
          .width('100%').padding({ top: 10, bottom: 8 })

          Row() {
            Column() {
              Text('16,847').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFEE58')
              Text('今日发起砍价').fontSize(9).fontColor('#FFFFFFCC')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text('2,356').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFEE58')
              Text('今日砍至免费').fontSize(9).fontColor('#FFFFFFCC')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text('¥580万').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFEE58')
              Text('累计省钱').fontSize(9).fontColor('#FFFFFFCC')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').padding({ top: 4, bottom: 12 })
        }
        .width('100%').backgroundColor(COLORS.primary)

        Scroll() {
          Row() {
            ForEach(this.categories, (cat: string) => {
              Text(cat)
                .fontSize(11).fontColor(this.selectedCategory === cat ? '#FFFFFF' : COLORS.primary)
                .backgroundColor(this.selectedCategory === cat ? COLORS.primary : COLORS.primaryLight)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(14)
                .margin({ left: 3, right: 3 })
                .onClick(() => { this.selectedCategory = cat })
            })
          }
          .padding({ left: 8, right: 8 })
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36)
        .margin({ top: 8 })

        Scroll() {
          Column() {
            this.bargainCardBuilder(mockBargainItems[0])
            this.bargainCardBuilder(mockBargainItems[1])
            this.bargainCardBuilder(mockBargainItems[2])
            this.bargainCardBuilder(mockBargainItems[3])
            this.bargainCardBuilder(mockBargainItems[4])
            this.bargainCardBuilder(mockBargainItems[5])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showStartModal) { this.startModal() }
      if (this.showDetailModal) { this.detailModal() }
    }
    .width('100%').height('100%')
  }
}

在这里插入图片描述

砍价大厅页的组件设计体现了高度的内聚性。BargainHallContent组件管理了四个状态变量:showStartModalshowDetailModal控制两个弹框的显示隐藏,selectedBargain记录当前选中的商品,selectedCategory追踪当前筛选的分类。modalOverlay是一个通用的遮罩层Builder,接收一个回调函数作为关闭参数,通过半透明黑色背景覆盖全屏,点击时触发关闭回调。

商品卡片bargainCardBuilder是本页最核心的UI构建器。卡片采用左右布局:左侧是90x90的商品图标区域,使用item.imageColor + '20'拼接出带有20%透明度的背景色(十六进制颜色后追加’20’即添加Alpha通道值0x20),配合borderRadius(14)形成圆角图标容器。右侧是商品信息区,包含商品名称(带热度标记)、描述、价格区(砍后价¥0用主色加粗、原价用删除线)、砍价金额和参砍人数。卡片底部是进度条和操作按钮,进度条通过Column().width(getBargainProgress(item) + '%')实现百分比宽度,配合外层固定宽度容器形成进度条效果。

头部区域设计了一个红色背景的统计条,展示今日发起砍价数、今日砍至免费数和累计省钱金额三项数据,数字使用金黄色#FFEE58突出显示。分类筛选条使用Scroll容器配合scrollable(ScrollDirection.Horizontal)实现横向滚动,通过ForEach渲染分类标签,选中态使用主色背景白色文字,未选中态使用浅红背景主色文字。商品列表通过多次调用bargainCardBuilder渲染各商品,这种方式虽然不如ForEach简洁,但在Mock数据量固定时具有更好的可读性。

六、发起砍价与商品详情模态弹框

模态弹框是移动端应用中常见的交互模式,用于在不离开当前页面的情况下展示临时信息或操作选项。本应用在砍价大厅页设计了两个弹框:发起砍价弹框和商品详情弹框,均采用Stack层叠和绝对定位的方式实现。

  // 弹框1:发起砍价弹框
  @Builder startModal() {
    Column() {
      this.modalOverlay(() => { this.showStartModal = false })
      Column() {
        Text('🪓 发起砍价').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
          .margin({ top: 18 })

        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[this.selectedBargain?.category ?? '数码'] ?? '🎁').fontSize(48)
            .margin({ top: 8 })
          Text(this.selectedBargain?.name ?? '').fontSize(15).fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary).margin({ top: 6 })
          Text(this.selectedBargain?.description ?? '').fontSize(11).fontColor(COLORS.textSecondary)
            .margin({ top: 4 })
          Row() {
            Text('原价 ¥' + (this.selectedBargain?.originalPrice ?? 0)).fontSize(12).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough })
            Text('→ 砍至 ¥0').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.primary).margin({ left: 10 })
          }
          .margin({ top: 8 })
        }
        .width('100%').alignItems(HorizontalAlign.Center)

        Column() {
          Text('每人可砍金额').fontSize(11).fontColor(COLORS.textSecondary)
          Text('¥' + (this.selectedBargain?.cutAmount ?? 0) + ' / 刀').fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent).margin({ top: 4 })
          Text('已有' + (this.selectedBargain?.participants ?? 0) + '人参砍').fontSize(10)
            .fontColor(COLORS.textHint).margin({ top: 4 })
        }
        .width('100%').alignItems(HorizontalAlign.Center).padding({ top: 14 })

        Row() {
          Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.border).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .onClick(() => { this.showStartModal = false })
          Text('🔥 立即发起').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor(COLORS.primary).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showStartModal = false })
        }
        .justifyContent(FlexAlign.Center).margin({ top: 16, bottom: 20 })
      }
      .width('85%').backgroundColor(COLORS.white).borderRadius(20)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '7.5%', y: '25%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // 弹框2:商品详情弹框
  @Builder detailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetailModal = false })
      Column() {
        Row() {
          Text('商品详情').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
          Blank()
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showDetailModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 16, bottom: 12 })
        Divider().color(COLORS.border)

        Scroll() {
          Column() {
            Column() {
              Text(BARGAIN_CATEGORY_CONFIG[this.selectedBargain?.category ?? '数码'] ?? '🎁').fontSize(56)
            }.width('100%').height(140).backgroundColor((this.selectedBargain?.imageColor ?? '#999') + '20')
            .borderRadius(12).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
            .margin({ top: 12 })

            Text(this.selectedBargain?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary).width('100%').margin({ top: 12 })
            Text(this.selectedBargain?.description ?? '').fontSize(12).fontColor(COLORS.textSecondary)
              .width('100%').margin({ top: 6 })

            Row() {
              Text('¥0').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
              Text('砍后价').fontSize(10).fontColor(COLORS.primary).margin({ left: 4 })
              Blank()
              Text('原价 ¥' + (this.selectedBargain?.originalPrice ?? 0)).fontSize(12).fontColor(COLORS.textHint)
                .decoration({ type: TextDecorationType.LineThrough })
            }
            .width('100%').margin({ top: 10 })

            Column() {
              Row() { Text('📦 发货时效').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text('砍价成功后48小时内').fontSize(12).fontColor(COLORS.textPrimary) }.width('100%').padding({ top: 8 })
              Row() { Text('🚚 运费').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text('全国包邮').fontSize(12).fontColor(COLORS.success) }.width('100%').padding({ top: 8 })
              Row() { Text('🛡️ 质保').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text('7天无理由退换').fontSize(12).fontColor(COLORS.textPrimary) }.width('100%').padding({ top: 8 })
              Row() { Text('👥 已参砍人数').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text((this.selectedBargain?.participants ?? 0) + '人').fontSize(12).fontColor(COLORS.accent).fontWeight(FontWeight.Bold) }.width('100%').padding({ top: 8 })
            }
            .width('100%').backgroundColor(COLORS.bg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 }).margin({ top: 12 })
          }
          .padding({ left: 16, right: 16 })
        }
        .layoutWeight(1)

        Row() {
          Text('收藏').fontSize(13).fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.border).borderRadius(18)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.showDetailModal = false })
          Text('🪓 发起砍价').fontSize(13).fontColor('#FFFFFF')
            .backgroundColor(COLORS.primary).borderRadius(18)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 10 })
            .onClick(() => { this.showDetailModal = false; this.showStartModal = true })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 10, bottom: 16 })
      }
      .width('92%').height('85%').backgroundColor(COLORS.white).borderRadius(16)
      .position({ x: '4%', y: '7%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

在这里插入图片描述

发起砍价弹框startModal采用了居中弹出式设计。整个弹框容器设置position({ x: 0, y: 0 }).zIndex(999)确保其覆盖在所有内容之上。内部内容卡片宽度为85%,通过position({ x: '7.5%', y: '25%' })实现水平居中和垂直偏下定位。弹框内容从上到下依次为标题、商品图标和名称、价格对比区(原价删除线+砍至零元)、每刀可砍金额展示、操作按钮区。值得注意的是,所有对selectedBargain的访问都使用了可选链操作符?.和空值合并操作符??,确保在selectedBargain为null时不会崩溃。

商品详情弹框detailModal是一个更大型的弹框,占据92%宽度和85%高度。它包含可滚动的商品详情区域和固定的底部操作栏。详情区域展示了商品大图标(56号字体)、名称、描述、价格对比、服务保障信息(发货时效、运费、质保、参砍人数)和用户好评。服务保障信息采用Row-Blank-Row的三列布局模式,左侧标签、中间弹性留白、右侧值,通过Blank()组件实现两端对齐。底部操作栏包含"收藏"和"发起砍价"两个按钮,点击"发起砍价"会先关闭详情弹框再打开发起砍价弹框,实现了弹框间的联动跳转。

流程图

HALL

MY

HELP

FREE

DAILY

点击发起砍价

点击详情

发起砍价按钮

砍价成功

点击放弃

点击邀请

确认发起

取消

填写地址

确认放弃

发送邀请

应用启动 BargainApp入口

初始化 activeTab = HALL

渲染内容区域

砍价大厅 BargainHallContent

我的砍价 MyBargainContent

助力记录 HelpRecordContent

免费拿 FreeItemsContent

省钱日报 DailyReportContent

头部统计栏

分类横滚筛选

商品大卡片列表

发起砍价弹框 startModal

商品详情弹框 detailModal

进行中砍价列表

进度条与助力头像

成功庆祝弹框 successModal

放弃确认弹框 giveUpConfirm

聊天气泡式助力记录

邀请好友弹框 inviteModal

战绩统计卡片

免费商品双列网格

今日头条

每日砍价柱状图

省钱达人榜

省钱小贴士

七、我的砍价页与进度追踪卡片

"我的砍价"页面是用户管理自己发起的砍价任务的核心页面。该页面展示了所有进行中和已过期的砍价记录,每条记录都包含进度条、剩余时间、助力好友头像和操作按钮。页面还设计了砍价成功庆祝弹框和放弃砍价确认弹框两种交互场景。

@Component
struct MyBargainContent {
  @State showSuccessModal: boolean = false
  @State showGiveUpConfirm: boolean = false
  @State selectedMy: MyBargainItem | null = null

  @Builder myBargainCardBuilder(m: MyBargainItem) {
    Column() {
      Row() {
        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[m.category] ?? '🎁').fontSize(32)
        }.width(70).height(70).backgroundColor(m.imageColor + '20').borderRadius(14)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Text(m.itemName).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
          Row() {
            Text('当前 ¥' + m.currentPrice).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
            Text('原价 ¥' + m.originalPrice).fontSize(10).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 6 })
          }
          .margin({ top: 3 })
          Row() {
            Text('⏰ ' + m.remainingTime).fontSize(10)
              .fontColor(m.status === '已过期' ? COLORS.textHint : COLORS.danger)
              .fontWeight(FontWeight.Bold)
            Text('· ' + m.helpers.length + '人助力').fontSize(10).fontColor(COLORS.textSecondary).margin({ left: 6 })
          }
          .margin({ top: 3 })
          Row() {
            ForEach([0, 1, 2], (i: number) => {
              Column() {
                Text('👤').fontSize(10)
              }.width(22).height(22).backgroundColor(COLORS.border).borderRadius(11)
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              .margin({ left: i > 0 ? -6 : 0 })
            })
            if (m.helpers.length > 3) {
              Text('+' + (m.helpers.length - 3)).fontSize(8).fontColor(COLORS.textSecondary)
                .margin({ left: 4 })
            }
          }
          .margin({ top: 4 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
      }
      .width('100%')

      Column() {
        Row() {
          Column()
            .width(m.progress + '%')
            .height(8).backgroundColor(m.status === '已过期' ? '#CCCCCC' : COLORS.primary).borderRadius(4)
          Column().layoutWeight(1)
        }
        .width('100%').height(8).backgroundColor(COLORS.border).borderRadius(4)
        Row() {
          Text('已砍 ' + m.progress + '%').fontSize(9).fontColor(m.status === '已过期' ? COLORS.textHint : COLORS.primary)
          Blank()
          Text('还差 ¥' + m.currentPrice + ' 到免费').fontSize(9).fontColor(COLORS.accent)
        }
        .width('100%').margin({ top: 3 })
      }
      .width('100%').margin({ top: 8 })

      Row() {
        Text('邀请助力').fontSize(11).fontColor('#FFFFFF')
          .backgroundColor(m.status === '已过期' ? '#CCCCCC' : COLORS.primary).borderRadius(14)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 })
        Text('分享').fontSize(11).fontColor(COLORS.primary)
          .backgroundColor(COLORS.primaryLight).borderRadius(14)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 }).margin({ left: 8 })
        Blank()
        if (m.status === '已过期') {
          Text('重新发起').fontSize(11).fontColor(COLORS.accent)
            .backgroundColor(COLORS.accentLight).borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        } else {
          Text('放弃').fontSize(11).fontColor(COLORS.danger)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .onClick(() => { this.selectedMy = m; this.showGiveUpConfirm = true })
        }
      }
      .width('100%').margin({ top: 8 })
    }
    .width('100%').backgroundColor(COLORS.white).borderRadius(14)
    .padding(14)
    .margin({ left: 12, right: 12, top: 8 })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('⏳ 我的砍价').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Blank()
            Text(getActiveBargainCount() + '个进行中').fontSize(10).fontColor('#FFFFFF')
              .backgroundColor('#FFFFFF33').padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .borderRadius(10).margin({ right: 14 })
          }
          .width('100%').padding({ left: 16, top: 12, bottom: 8 })
          Row() {
            Text('叫上好友一起砍,好物免费拿到家').fontSize(10).fontColor('#FFFFFFCC')
          }
          .width('100%').padding({ left: 16, bottom: 12 })
        }
        .width('100%').backgroundColor(COLORS.primaryDark)
        .borderRadius({ bottomLeft: 16, bottomRight: 16 })

        Scroll() {
          Column() {
            this.myBargainCardBuilder(mockMyBargains[0])
            this.myBargainCardBuilder(mockMyBargains[1])
            this.myBargainCardBuilder(mockMyBargains[2])
            this.myBargainCardBuilder(mockMyBargains[3])
            this.myBargainCardBuilder(mockMyBargains[4])
            this.myBargainCardBuilder(mockMyBargains[5])
            this.myBargainCardBuilder(mockMyBargains[6])
            this.myBargainCardBuilder(mockMyBargains[7])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showSuccessModal) { this.successModal() }
      if (this.showGiveUpConfirm) { this.giveUpConfirm() }
    }
    .width('100%').height('100%')
  }
}

在这里插入图片描述

我的砍价卡片myBargainCardBuilder是本页最复杂的基础组件。卡片采用上下结构:上方是商品信息行,下方是进度条和操作按钮区。商品信息行的左侧是70x70的图标区域,右侧信息区包含商品名称、当前价与原价对比、倒计时与助力人数、以及助力好友头像堆叠。头像堆叠使用了ForEach渲染前三个头像,通过margin({ left: i > 0 ? -6 : 0 })实现负边距叠加效果,当助力人数超过3人时显示"+N"文本。

进度条区域是该卡片的亮点设计。外层Column固定高度8vp并设置浅红色背景COLORS.border作为轨道,内层Column宽度设为m.progress + '%'并设置主色背景作为填充条。这种"固定容器+百分比宽度内层"的双层结构是ArkTS中实现进度条的经典模式。进度条下方是双行文字说明,左侧显示已砍百分比,右侧显示还差多少金额到免费。操作按钮区根据m.status的值进行条件渲染:已过期状态显示"重新发起"按钮(金色),进行中状态显示"放弃"按钮(红色,点击触发确认弹框)。这种基于状态的动态渲染体现了ArkTS声明式UI的灵活性。

页面头部使用COLORS.primaryDark深红色背景,与砍价大厅页的主红色形成色彩层次区分。头部右侧通过getActiveBargainCount()函数实时统计进行中的砍价数量,并使用半透明白色背景#FFFFFF33的胶囊标签展示。底部圆角通过borderRadius({ bottomLeft: 16, bottomRight: 16 })仅设置左下和右下圆角,使头部与下方内容区域形成平滑过渡。

八、助力记录页聊天气泡与邀请好友弹框

助力记录页采用了类似即时通讯应用的聊天气泡式布局来展示好友助力信息。这种设计将砍价助力过程社交化,让用户感受到好友的参与和支持。页面还包含一个邀请好友助力弹框,用于通过好友选择网格发起社交邀请。

@Component
struct HelpRecordContent {
  @State showInviteModal: boolean = false

  @Builder helpBubbleBuilder(h: HelpRecord) {
    Row() {
      if (!h.isMe) {
        Column() {
          Text('👤').fontSize(24)
        }.width(40).height(40).backgroundColor(h.avatarColor + '30').borderRadius(20)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
        Column() {
          Row() {
            Text(h.friendName).fontSize(10).fontColor(COLORS.textHint)
            Text(h.timestamp).fontSize(9).fontColor(COLORS.textHint).margin({ left: 6 })
          }
          .width('100%')
          Column() {
            Text(h.message).fontSize(12).fontColor(COLORS.textPrimary)
            Row() {
              Text('🪓 帮砍了 ¥' + h.cutAmount).fontSize(11).fontWeight(FontWeight.Bold)
                .fontColor(COLORS.primary)
            }
            .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .backgroundColor(COLORS.white).borderRadius({ topLeft: 2, topRight: 12, bottomLeft: 12, bottomRight: 12 })
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start).padding({ left: 8 })
        .constraintSize({ maxWidth: '75%' })
      } else {
        Column() {
          Row() {
            Text(h.timestamp).fontSize(9).fontColor(COLORS.textHint).margin({ right: 6 })
            Text(h.friendName).fontSize(10).fontColor(COLORS.textHint)
          }
          .width('100%').justifyContent(FlexAlign.End)
          Column() {
            Text(h.message).fontSize(12).fontColor('#FFFFFF')
            Row() {
              Text('发起砍价 · 底价 ¥' + h.cutAmount).fontSize(11).fontWeight(FontWeight.Bold)
                .fontColor('#FFEE58')
            }
            .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .backgroundColor(COLORS.primary).borderRadius({ topLeft: 12, topRight: 2, bottomLeft: 12, bottomRight: 12 })
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.End).padding({ right: 8 })
        .constraintSize({ maxWidth: '75%' })
        Column() {
          Text('😊').fontSize(24)
        }.width(40).height(40).backgroundColor(COLORS.primaryLight).borderRadius(20)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      }
    }
    .width('100%').padding({ left: 12, right: 12, top: 6 })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('🤝 助力记录').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Blank()
            Text('邀请助力').fontSize(10).fontColor('#FFFFFF')
              .backgroundColor(COLORS.accent).padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .borderRadius(10).margin({ right: 14 })
              .onClick(() => { this.showInviteModal = true })
          }
          .width('100%').padding({ left: 16, top: 12, bottom: 8 })
          Row() {
            Text(getHelpRecordCount() + '位好友帮过你 · 累计助力砍价 ¥328').fontSize(10).fontColor('#FFFFFFCC')
          }
          .width('100%').padding({ left: 16, bottom: 12 })
        }
        .width('100%').backgroundColor(COLORS.primary)
        .borderRadius({ bottomLeft: 16, bottomRight: 16 })

        Scroll() {
          Column() {
            Text('—— 今天 ——').fontSize(10).fontColor(COLORS.textHint)
              .alignSelf(ItemAlign.Center).margin({ top: 10 })
            this.helpBubbleBuilder(mockHelpRecords[0])
            this.helpBubbleBuilder(mockHelpRecords[1])
            this.helpBubbleBuilder(mockHelpRecords[2])
            this.helpBubbleBuilder(mockHelpRecords[3])
            this.helpBubbleBuilder(mockHelpRecords[4])
            this.helpBubbleBuilder(mockHelpRecords[5])
            this.helpBubbleBuilder(mockHelpRecords[6])
            this.helpBubbleBuilder(mockHelpRecords[7])
            Text('—— 昨天 ——').fontSize(10).fontColor(COLORS.textHint)
              .alignSelf(ItemAlign.Center).margin({ top: 12 })
            this.helpBubbleBuilder(mockHelpRecords[8])
            this.helpBubbleBuilder(mockHelpRecords[9])
            this.helpBubbleBuilder(mockHelpRecords[10])
            this.helpBubbleBuilder(mockHelpRecords[11])
            this.helpBubbleBuilder(mockHelpRecords[12])
            this.helpBubbleBuilder(mockHelpRecords[13])
            this.helpBubbleBuilder(mockHelpRecords[14])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showInviteModal) { this.inviteModal() }
    }
    .width('100%').height('100%')
  }
}

在这里插入图片描述

聊天气泡构建器helpBubbleBuilder是该页面的核心设计。通过h.isMe字段实现条件渲染,好友助力消息显示在左侧(头像+气泡),我方发起的消息显示在右侧(气泡+头像),这种布局与主流即时通讯应用完全一致。好友气泡使用白色背景,圆角设置为{ topLeft: 2, topRight: 12, bottomLeft: 12, bottomRight: 12 },即左上角小圆角表示消息来源方向,其余三角大圆角。我方气泡使用主色红色背景,圆角设置为{ topLeft: 12, topRight: 2, bottomLeft: 12, bottomRight: 12 },右上角小圆角。

气泡内部包含两层信息:上层是好友名和时间戳的行,下层是消息正文和砍价金额行。constraintSize({ maxWidth: '75%' })约束了气泡的最大宽度为75%,防止长文本气泡占满整行。时间线分隔符"—— 今天 ——“和”—— 昨天 ——"通过alignSelf(ItemAlign.Center)实现居中对齐,为聊天列表添加了时间维度的分隔。

邀请好友弹框inviteModal设计了一个3行4列的好友选择网格,通过嵌套ForEach实现二维网格布局。每个好友卡片包含头像、昵称和"邀请"按钮。弹框底部还包含邀请文案预览区和三种分享渠道按钮(微信好友、朋友圈、复制链接),颜色分别对应绿色、蓝色和浅红色,与各平台的品牌色保持一致。

九、免费拿战绩页与省钱日报页

免费拿页面展示了用户通过砍价成功免费获得的商品,采用双列网格布局展示"战绩墙"。省钱日报页则以报纸式排版展示每日省钱数据、柱状图和达人排行榜。这两个页面共同构成了应用的"成就感反馈"闭环。

@Component
struct FreeItemsContent {
  build() {
    Column() {
      Column() {
        Row() {
          Text('🎁 免费拿战绩').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Blank()
          Text(mockFreeItems.length + '件到手').fontSize(10).fontColor('#FFFFFF')
            .backgroundColor(COLORS.accent).padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .borderRadius(10).margin({ right: 14 })
        }
        .width('100%').padding({ left: 16, top: 12, bottom: 8 })
        Row() {
          Text('累计免费获得 ¥' + getFreeItemTotal() + ' 的好物').fontSize(10).fontColor('#FFFFFFCC')
        }
        .width('100%').padding({ left: 16, bottom: 12 })
      }
      .width('100%').backgroundColor(COLORS.accent)
      .borderRadius({ bottomLeft: 16, bottomRight: 16 })

      Scroll() {
        Column() {
          Row() {
            Column() {
              Text(mockFreeItems.length.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
              Text('免费件数').fontSize(10).fontColor(COLORS.textSecondary)
            }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 14, bottom: 14 })
            .backgroundColor(COLORS.white).borderRadius(12)
            Column() {
              Text('¥' + getFreeItemTotal()).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
              Text('累计价值').fontSize(10).fontColor(COLORS.textSecondary)
            }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 14, bottom: 14 })
            .backgroundColor(COLORS.white).borderRadius(12).margin({ left: 6 })
            Column() {
              Text('38').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.success)
              Text('助力好友').fontSize(10).fontColor(COLORS.textSecondary)
            }.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 14, bottom: 14 })
            .backgroundColor(COLORS.white).borderRadius(12).margin({ left: 6 })
          }
          .width('100%').padding({ left: 12, right: 12, top: 12 })

          Row() {
            Column() {
              Stack() {
                Column() {
                  Text(BARGAIN_CATEGORY_CONFIG[mockFreeItems[0].category] ?? '🎁').fontSize(34)
                }.width('100%').height(72).backgroundColor(mockFreeItems[0].imageColor + '20')
                .borderRadius({ topLeft: 10, topRight: 10 })
                .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
                Text('✨免费').fontSize(8).fontColor('#FFFFFF').backgroundColor(COLORS.success)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(6)
                  .position({ x: 4, y: 4 })
              }.width('100%').height(72)
              Column() {
                Text(mockFreeItems[0].itemName).fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
                Row() {
                  Text('¥0').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
                  Text('原¥' + mockFreeItems[0].originalPrice).fontSize(9).fontColor(COLORS.textHint)
                    .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
                }.margin({ top: 3 })
                Row() {
                  Text('⭐'.repeat(Math.min(mockFreeItems[0].rating, 5))).fontSize(8).fontColor(COLORS.gold)
                }.margin({ top: 2 })
                Text(mockFreeItems[0].review).fontSize(9).fontColor(COLORS.textSecondary).maxLines(2).margin({ top: 3 })
              }.width('100%').padding({ left: 8, right: 8, top: 6, bottom: 8 })
            }.width('100%').backgroundColor(COLORS.white).borderRadius(10)
            // ... 第二列商品结构相同
          }
          .width('100%').padding({ left: 12, right: 12, top: 12 })
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

在这里插入图片描述

免费拿页面的头部使用COLORS.accent金色背景,与砍价大厅页的红色和我的砍价页的深红色形成色彩区分,暗示该页面是"收获"主题。头部右侧的"件到手"标签使用半透明金色背景,与页面主色调和谐统一。统计卡片区域采用三等分布局,分别展示免费件数、累计价值和助力好友数,数字使用对应主题色(金色、红色、绿色)加粗显示。

免费商品网格采用双列Row-Column结构。每个商品卡片顶部是图标区域,使用Stack容器叠加图标和"✨免费"角标,角标通过position({ x: 4, y: 4 })绝对定位在左上角。卡片下方是商品名称、价格对比(¥0免费价+原价删除线)、星级评分和用户评价。星级评分使用'⭐'.repeat(Math.min(mockFreeItems[0].rating, 5))生成对应数量的星星字符,通过Math.min确保最多5颗星。这种用Emoji字符代替图标库的方案在轻量级应用中非常实用,减少了资源依赖。

@Component
struct DailyReportContent {
  build() {
    Column() {
      Column() {
        Row() {
          Text('📰 省钱日报').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Blank()
          Text('2026-08-24').fontSize(11).fontColor('#FFFFFF')
            .backgroundColor('#FFFFFF33').padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .borderRadius(10).margin({ right: 14 })
        }
        .width('100%').padding({ left: 16, top: 12, bottom: 8 })
        Row() {
          Text('第358期 · 每天一点省钱智慧').fontSize(10).fontColor('#FFFFFFCC')
        }
        .width('100%').padding({ left: 16, bottom: 12 })
      }
      .width('100%').backgroundColor(COLORS.primaryDark)
      .borderRadius({ bottomLeft: 16, bottomRight: 16 })

      Scroll() {
        Column() {
          Column() {
            Text('今 日 头 条').fontSize(12).fontColor(COLORS.accent).fontWeight(FontWeight.Bold)
              .width('100%').textAlign(TextAlign.Center).padding({ top: 10 })
            Divider().color(COLORS.border).margin({ top: 6, left: 40, right: 40 })
            Text('本周累计省钱 ¥' + getTotalSavedAmount()).fontSize(24).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primary).margin({ top: 10 })
            Text('共发起32次砍价 · 帮助好友砍价28次').fontSize(11).fontColor(COLORS.textSecondary)
              .margin({ top: 4 })
            Text('比上周多省 ¥486,超过全国92%的用户!').fontSize(11).fontColor(COLORS.success)
              .margin({ top: 4 })
          }
          .width('100%').backgroundColor(COLORS.white).borderRadius(12)
          .margin({ left: 12, right: 12, top: 12 }).padding({ bottom: 14 })
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('📊 本周每日砍价金额').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
              .width('100%').padding({ left: 16, top: 12, bottom: 8 })
            Row() {
              ForEach([0, 1, 2, 3, 4, 5, 6], (i: number) => {
                Column() {
                  Text('¥' + mockDailyStats[i].savedAmount).fontSize(8).fontColor(COLORS.primary)
                  Column().width(18).height((mockDailyStats[i].savedAmount / 800 * 80).toFixed(0) + 'vp')
                    .backgroundColor(i === 6 ? COLORS.primary : COLORS.primaryLight).borderRadius(3)
                  Text(mockDailyStats[i].date).fontSize(8).fontColor(COLORS.textSecondary).margin({ top: 3 })
                  Text(mockDailyStats[i].bargainCount + '次').fontSize(7).fontColor(COLORS.textHint)
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center)
              })
            }
            .padding({ left: 12, right: 12, bottom: 12 })
          }
          .width('100%').backgroundColor(COLORS.white).borderRadius(12)
          .margin({ left: 12, right: 12, top: 8 })

          Column() {
            Text('🏆 本周省钱达人').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
              .width('100%').padding({ left: 16, top: 12, bottom: 8 })
            Column() {
              ForEach([0, 1, 2, 3, 4], (i: number) => {
                Row() {
                  Text((i + 1).toString()).fontSize(14).fontWeight(FontWeight.Bold)
                    .fontColor(i < 3 ? COLORS.accent : COLORS.textHint)
                    .width(28).textAlign(TextAlign.Center)
                  Column() {
                    Text('👤').fontSize(18)
                  }.width(34).height(34).backgroundColor(mockTopSavers[i].avatarColor + '30').borderRadius(17)
                  .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
                  Text(mockTopSavers[i].name).fontSize(12).fontColor(COLORS.textPrimary)
                    .layoutWeight(1).margin({ left: 8 })
                  Text('¥' + mockTopSavers[i].amount).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
                }
                .width('100%').padding({ top: 8, bottom: 8 })
                if (i < 4) {
                  Divider().color(COLORS.bg)
                }
              })
            }
            .padding({ left: 12, right: 12, bottom: 10 })
          }
          .width('100%').backgroundColor(COLORS.white).borderRadius(12)
          .margin({ left: 12, right: 12, top: 8 })

          Column() {
            Text('💡 省钱小贴士').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
              .width('100%').padding({ left: 16, top: 12, bottom: 8 })
            Column() {
              Row() { Text('🪓').fontSize(14); Text('早上9-10点发起砍价,好友在线率高,成功更快').fontSize(11).fontColor(COLORS.textSecondary).margin({ left: 8 }).layoutWeight(1) }
              .width('100%').padding({ top: 8, bottom: 8 })
              Divider().color(COLORS.bg)
              Row() { Text('👥').fontSize(14); Text('加入砍价互助群,互相助力,成功率翻倍').fontSize(11).fontColor(COLORS.textSecondary).margin({ left: 8 }).layoutWeight(1) }
              .width('100%').padding({ top: 8, bottom: 8 })
              Divider().color(COLORS.bg)
              Row() { Text('⏰').fontSize(14); Text('砍价有效期48小时,过期进度清零,抓紧时间').fontSize(11).fontColor(COLORS.textSecondary).margin({ left: 8 }).layoutWeight(1) }
              .width('100%').padding({ top: 8, bottom: 8 })
              Divider().color(COLORS.bg)
              Row() { Text('🎁').fontSize(14); Text('新用户首次砍价必成功,快邀请好友注册').fontSize(11).fontColor(COLORS.textSecondary).margin({ left: 8 }).layoutWeight(1) }
              .width('100%').padding({ top: 8, bottom: 8 })
            }
            .padding({ left: 16, right: 16 })
          }
          .width('100%').backgroundColor(COLORS.white).borderRadius(12)
          .margin({ left: 12, right: 12, top: 8, bottom: 20 })
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

在这里插入图片描述

省钱日报页采用了报纸排版风格,深红色头部带有日期和期数标签。今日头条卡片使用textAlign(TextAlign.Center)居中排版,金色"今日头条"标题配合Divider分隔线,下方是本周累计省钱金额的大号数字展示,并附有对比上周的增长数据和超越用户的百分比,这种设计在心理层面给予用户强烈的成就感。

每日砍价金额柱状图是该页面的数据可视化亮点。通过ForEach渲染7个柱子,每个柱子的颜色根据是否为当天来决定(当天用主色红色,其他天用浅红色)。柱子高度通过(mockDailyStats[i].savedAmount / 800 * 80).toFixed(0) + 'vp'动态计算,将金额按比例映射为vp单位的视觉高度。这种纯ArkTS实现的柱状图无需任何图表库,展示了声明式UI的灵活性。

省钱达人榜使用ForEach渲染5位达人,前三名排名序号使用金色COLORS.accent突出显示,后两名使用灰色提示色。每行包含排名序号、头像、昵称和省钱金额,行间用Divider分隔。省钱小贴士区域采用了图标+文字的行式布局,四条贴士分别提供发起时间、互助群、有效期和新用户策略的优化建议,既实用又贴心。

核心技术点对比总结

技术维度 实现方式 关键API/装饰器 设计特点 适用场景
页面路由 枚举+条件渲染 enum BargainTab, @State, if-else 轻量级Tab切换,无需路由框架 固定Tab数量的底部导航应用
组件复用 @Builder方法 @Builder, 参数传递 统一构建器接收数据参数生成卡片 列表项、卡片等重复UI结构
模态弹框 Stack层叠+绝对定位 Stack, position, zIndex 遮罩层+内容卡片的层叠覆盖 表单提交、详情展示、确认操作
状态管理 @State局部状态 @State, 条件渲染 布尔值控制弹框显隐,对象记录选中项 组件内交互状态管理
进度条 双层Column百分比宽度 Column.width(string), borderRadius 固定容器+动态宽度内层实现进度 砍价进度、任务完成度展示
聊天气泡 条件渲染+非对称圆角 borderRadius(四角独立设置), constraintSize isMe字段区分左右布局,圆角指示消息方向 即时通讯、社交互动记录
数据可视化 ForEach+动态高度 ForEach, height(vp), backgroundColor 纯ArkTS实现柱状图,无需图表库 数据统计、趋势展示
头像堆叠 负边距叠加 margin({ left: negative }) 多个圆形头像通过负left值叠加 社交场景的助力者展示
分类筛选 横向Scroll+ForEach Scroll, scrollable(Horizontal), onClick 标签胶囊式筛选,选中态样式切换 商品分类、标签过滤
色彩系统 设计令牌常量 const COLORS: ColorPalette 16色全局统一管理,Alpha通道拼接 全应用视觉一致性保障

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

// ============ 砍价免费拿APP ============
// 主题色:砍价红 #D32F2F + 助力金 #FFC107

interface ColorPalette {
  primary: string
  primaryLight: string
  primaryDark: string
  accent: string
  accentLight: string
  bg: string
  cardBg: string
  textPrimary: string
  textSecondary: string
  textHint: string
  border: string
  success: string
  warning: string
  danger: string
  white: string
  gold: string
}

interface BargainItem {
  id: number
  name: string
  category: string
  originalPrice: number
  targetPrice: number
  currentPrice: number
  cutAmount: number
  participants: number
  hot: boolean
  imageColor: string
  description: string
}

interface MyBargainItem {
  id: number
  itemName: string
  originalPrice: number
  targetPrice: number
  currentPrice: number
  progress: number
  remainingTime: string
  helpers: string[]
  imageColor: string
  status: string
  category: string
}

interface HelpRecord {
  id: number
  friendName: string
  avatarColor: string
  itemName: string
  cutAmount: number
  timestamp: string
  message: string
  isMe: boolean
}

interface FreeItem {
  id: number
  itemName: string
  originalPrice: number
  obtainedDate: string
  imageColor: string
  category: string
  daysUsed: number
  review: string
  rating: number
}

interface DailyStat {
  date: string
  bargainCount: number
  savedAmount: number
}

interface TopSaver {
  name: string
  amount: number
  avatarColor: string
}

// ============ 设计令牌 ============
const COLORS: ColorPalette = {
  primary: '#D32F2F',
  primaryLight: '#FFCDD2',
  primaryDark: '#B71C1C',
  accent: '#FFC107',
  accentLight: '#FFF8E1',
  bg: '#FFF8F6',
  cardBg: '#FFFFFF',
  textPrimary: '#212121',
  textSecondary: '#757575',
  textHint: '#BDBDBD',
  border: '#FFEBEE',
  success: '#43A047',
  warning: '#FB8C00',
  danger: '#D32F2F',
  white: '#FFFFFF',
  gold: '#FFD700'
}

const BARGAIN_CATEGORY_CONFIG: Record<string, string> = {
  '数码': '📱',
  '家电': '📺',
  '美妆': '💄',
  '服饰': '👔',
  '食品': '🍪',
  '家居': '🛋️',
  '母婴': '🍼',
  '运动': '⚽'
}

// ============ Mock 数据 ============
const mockBargainItems: BargainItem[] = [
  { id: 1, name: '智能扫地机器人扫拖一体', category: '家电', originalPrice: 1899, targetPrice: 0, currentPrice: 850, cutAmount: 30, participants: 12847, hot: true, imageColor: '#546E7A', description: '激光导航扫拖一体,自动集尘' },
  { id: 2, name: '苹果平板电脑10.2英寸', category: '数码', originalPrice: 2499, targetPrice: 0, currentPrice: 1200, cutAmount: 50, participants: 9823, hot: true, imageColor: '#455A64', description: '官方正品,全新未拆封' },
  { id: 3, name: '进口大牌香水50ml', category: '美妆', originalPrice: 899, targetPrice: 0, currentPrice: 380, cutAmount: 25, participants: 8432, hot: false, imageColor: '#AD1457', description: '经典款淡香水,持久留香' },
  { id: 4, name: '轻奢真皮双肩包', category: '服饰', originalPrice: 699, targetPrice: 0, currentPrice: 290, cutAmount: 20, participants: 7321, hot: false, imageColor: '#5D4037', description: '头层牛皮,大容量防盗' },
  { id: 5, name: '破壁机家用静音款', category: '家电', originalPrice: 1099, targetPrice: 0, currentPrice: 520, cutAmount: 35, participants: 6542, hot: true, imageColor: '#37474F', description: '低音降噪,八叶刀头' },
  { id: 6, name: '蓝牙降噪耳机旗舰版', category: '数码', originalPrice: 1299, targetPrice: 0, currentPrice: 640, cutAmount: 40, participants: 11205, hot: true, imageColor: '#263238', description: '主动降噪,40小时续航' },
  { id: 7, name: '燕窝礼盒70g送礼佳品', category: '食品', originalPrice: 599, targetPrice: 0, currentPrice: 250, cutAmount: 15, participants: 4521, hot: false, imageColor: '#E0E0E0', description: '印尼进口,即食燕窝' },
  { id: 8, name: '北欧实木床头柜', category: '家居', originalPrice: 399, targetPrice: 0, currentPrice: 168, cutAmount: 18, participants: 3210, hot: false, imageColor: '#8D6E63', description: '橡胶木材质,两抽屉' },
  { id: 9, name: '儿童电话手表5G版', category: '母婴', originalPrice: 899, targetPrice: 0, currentPrice: 420, cutAmount: 28, participants: 5673, hot: true, imageColor: '#F06292', description: '精准定位,视频通话' },
  { id: 10, name: '跑步机家用可折叠', category: '运动', originalPrice: 1599, targetPrice: 0, currentPrice: 780, cutAmount: 45, participants: 7856, hot: false, imageColor: '#2E7D32', description: '静音折叠,18档坡度' },
  { id: 11, name: '空气炸锅大容量6L', category: '家电', originalPrice: 599, targetPrice: 0, currentPrice: 260, cutAmount: 22, participants: 8901, hot: true, imageColor: '#FF7043', description: '360°热风循环,免翻面' },
  { id: 12, name: '电竞显示器27寸2K', category: '数码', originalPrice: 1699, targetPrice: 0, currentPrice: 890, cutAmount: 48, participants: 6432, hot: false, imageColor: '#303F9F', description: '165Hz高刷,IPS面板' },
  { id: 13, name: '羊绒围巾纯色百搭', category: '服饰', originalPrice: 399, targetPrice: 0, currentPrice: 155, cutAmount: 12, participants: 2345, hot: false, imageColor: '#BF360C', description: '100%山羊绒,加厚保暖' },
  { id: 14, name: '按摩椅全身豪华款', category: '家居', originalPrice: 4999, targetPrice: 0, currentPrice: 2800, cutAmount: 88, participants: 3456, hot: true, imageColor: '#4E342E', description: 'SL导轨,零重力太空舱' },
  { id: 15, name: '婴幼儿辅食机全功能', category: '母婴', originalPrice: 459, targetPrice: 0, currentPrice: 198, cutAmount: 16, participants: 4567, hot: false, imageColor: '#F48FB1', description: '蒸煮搅打一体,食品级材质' },
  { id: 16, name: '智能体脂秤蓝牙版', category: '运动', originalPrice: 199, targetPrice: 0, currentPrice: 78, cutAmount: 8, participants: 9876, hot: false, imageColor: '#00695C', description: '28项身体数据,App同步' }
]

const mockMyBargains: MyBargainItem[] = [
  { id: 1, itemName: '智能扫地机器人扫拖一体', originalPrice: 1899, targetPrice: 0, currentPrice: 850, progress: 55, remainingTime: '22:15:30', helpers: ['小美', '阿强', 'Lily'], imageColor: '#546E7A', status: '砍价中', category: '家电' },
  { id: 2, itemName: '蓝牙降噪耳机旗舰版', originalPrice: 1299, targetPrice: 0, currentPrice: 640, progress: 51, remainingTime: '05:42:18', helpers: ['老张', '甜甜'], imageColor: '#263238', status: '砍价中', category: '数码' },
  { id: 3, itemName: '进口大牌香水50ml', originalPrice: 899, targetPrice: 0, currentPrice: 380, progress: 58, remainingTime: '11:30:45', helpers: ['小美', '大壮', 'Amy', '小橘'], imageColor: '#AD1457', status: '砍价中', category: '美妆' },
  { id: 4, itemName: '儿童电话手表5G版', originalPrice: 899, targetPrice: 0, currentPrice: 420, progress: 53, remainingTime: '44:10:22', helpers: ['王伯'], imageColor: '#F06292', status: '砍价中', category: '母婴' },
  { id: 5, itemName: '空气炸锅大容量6L', originalPrice: 599, targetPrice: 0, currentPrice: 260, progress: 57, remainingTime: '01:15:33', helpers: ['小美', '阿强', 'Lily', '老张', '甜甜'], imageColor: '#FF7043', status: '砍价中', category: '家电' },
  { id: 6, itemName: '电竞显示器27寸2K', originalPrice: 1699, targetPrice: 0, currentPrice: 890, progress: 48, remainingTime: '已过期', helpers: ['Leo'], imageColor: '#303F9F', status: '已过期', category: '数码' },
  { id: 7, itemName: '轻奢真皮双肩包', originalPrice: 699, targetPrice: 0, currentPrice: 290, progress: 58, remainingTime: '08:45:12', helpers: ['小美', '大壮'], imageColor: '#5D4037', status: '砍价中', category: '服饰' },
  { id: 8, itemName: '智能体脂秤蓝牙版', originalPrice: 199, targetPrice: 0, currentPrice: 78, progress: 61, remainingTime: '30:20:10', helpers: ['酸酸', '小丸子', 'Amy'], imageColor: '#00695C', status: '砍价中', category: '运动' }
]

const mockHelpRecords: HelpRecord[] = [
  { id: 1, friendName: '果园达人小美', avatarColor: '#E91E63', itemName: '智能扫地机器人', cutAmount: 15, timestamp: '10:23', message: '帮你砍了15元,快去加油!', isMe: false },
  { id: 2, friendName: '种树狂人阿强', avatarColor: '#2196F3', itemName: '智能扫地机器人', cutAmount: 22, timestamp: '10:35', message: '兄弟我看行,帮你砍一刀', isMe: false },
  { id: 3, friendName: '柠檬女王Lily', avatarColor: '#FF9800', itemName: '蓝牙降噪耳机', cutAmount: 35, timestamp: '11:02', message: '这个耳机我买过,确实不错!', isMe: false },
  { id: 4, friendName: '我', avatarColor: '#D32F2F', itemName: '破壁机家用静音款', cutAmount: 30, timestamp: '11:15', message: '新发起的砍价,大家帮帮忙!', isMe: true },
  { id: 5, friendName: '西瓜哥大壮', avatarColor: '#4CAF50', itemName: '智能扫地机器人', cutAmount: 8, timestamp: '11:30', message: '砍了一刀,手气不太好', isMe: false },
  { id: 6, friendName: '草莓公主甜甜', avatarColor: '#E91E63', itemName: '进口大牌香水', cutAmount: 28, timestamp: '12:05', message: '帮砍28!香水超值哦~', isMe: false },
  { id: 7, friendName: '我', avatarColor: '#D32F2F', itemName: '儿童电话手表', cutAmount: 30, timestamp: '12:18', message: '给娃砍个电话手表,求助力!', isMe: true },
  { id: 8, friendName: '葡萄老农王伯', avatarColor: '#795548', itemName: '进口大牌香水', cutAmount: 12, timestamp: '13:01', message: '帮你砍了12块', isMe: false },
  { id: 9, friendName: '樱桃小丸子', avatarColor: '#C62828', itemName: '空气炸锅6L', cutAmount: 19, timestamp: '13:26', message: '炸锅好用!砍一刀~', isMe: false },
  { id: 10, friendName: '我', avatarColor: '#D32F2F', itemName: '智能体脂秤', cutAmount: 30, timestamp: '14:02', message: '体脂秤免费拿,感谢大家!', isMe: true },
  { id: 11, friendName: '桃子姐姐阿花', avatarColor: '#F48FB1', itemName: '蓝牙降噪耳机', cutAmount: 26, timestamp: '14:35', message: '助力成功,快到零元啦!', isMe: false },
  { id: 12, friendName: '苹果王子Leo', avatarColor: '#D32F2F', itemName: '电竞显示器27寸', cutAmount: 48, timestamp: '15:10', message: '一刀48!欧皇附体!', isMe: false },
  { id: 13, friendName: '橘子男孩小橘', avatarColor: '#FB8C00', itemName: '轻奢真皮双肩包', cutAmount: 17, timestamp: '15:44', message: '包不错,帮你砍了', isMe: false },
  { id: 14, friendName: '柠檬精酸酸', avatarColor: '#FBC02D', itemName: '智能体脂秤', cutAmount: 11, timestamp: '16:20', message: '酸酸的助力来啦', isMe: false },
  { id: 15, friendName: '丰收女神Amy', avatarColor: '#AD1457', itemName: '空气炸锅6L', cutAmount: 33, timestamp: '16:52', message: '大额刀法!帮你砍33!', isMe: false }
]

const mockFreeItems: FreeItem[] = [
  { id: 1, itemName: '保温杯316不锈钢', originalPrice: 99, obtainedDate: '2026-08-20', imageColor: '#78909C', category: '家居', daysUsed: 4, review: '质量很好,保温效果一流!', rating: 5 },
  { id: 2, itemName: '蓝牙音箱便携版', originalPrice: 159, obtainedDate: '2026-08-15', imageColor: '#546E7A', category: '数码', daysUsed: 9, review: '音质超出预期,值得砍', rating: 5 },
  { id: 3, itemName: '纸巾整箱30包', originalPrice: 69, obtainedDate: '2026-08-18', imageColor: '#E0E0E0', category: '家居', daysUsed: 6, review: '日用品免费拿太爽了', rating: 4 },
  { id: 4, itemName: '洗衣液4斤装x2', originalPrice: 79, obtainedDate: '2026-08-12', imageColor: '#4FC3F7', category: '家居', daysUsed: 12, review: '量大实用,够用半年', rating: 4 },
  { id: 5, itemName: '手机支架铝合金', originalPrice: 49, obtainedDate: '2026-08-22', imageColor: '#90A4AE', category: '数码', daysUsed: 2, review: '很稳固,桌面追剧神器', rating: 5 },
  { id: 6, itemName: '零食大礼包20包', originalPrice: 89, obtainedDate: '2026-08-08', imageColor: '#F57F17', category: '食品', daysUsed: 16, review: '已经吃完啦,很好吃', rating: 5 },
  { id: 7, itemName: '儿童绘本10册', originalPrice: 129, obtainedDate: '2026-08-05', imageColor: '#F9A825', category: '母婴', daysUsed: 19, review: '娃很喜欢,每天睡前必读', rating: 5 },
  { id: 8, itemName: '瑜伽垫TPE材质', originalPrice: 89, obtainedDate: '2026-07-30', imageColor: '#43A047', category: '运动', daysUsed: 25, review: '厚度适中,防滑效果好', rating: 4 }
]

const mockDailyStats: DailyStat[] = [
  { date: '08-18', bargainCount: 3, savedAmount: 268 },
  { date: '08-19', bargainCount: 5, savedAmount: 445 },
  { date: '08-20', bargainCount: 2, savedAmount: 156 },
  { date: '08-21', bargainCount: 6, savedAmount: 589 },
  { date: '08-22', bargainCount: 4, savedAmount: 367 },
  { date: '08-23', bargainCount: 7, savedAmount: 712 },
  { date: '08-24', bargainCount: 5, savedAmount: 483 }
]

const mockTopSavers: TopSaver[] = [
  { name: '砍价女王小丽', amount: 8650, avatarColor: '#E91E63' },
  { name: '零元购达人', amount: 7320, avatarColor: '#2196F3' },
  { name: '助力收割机', amount: 6890, avatarColor: '#4CAF50' },
  { name: '免费拿专家', amount: 5210, avatarColor: '#FF9800' },
  { name: '社群砍价王', amount: 4870, avatarColor: '#9C27B0' }
]

// ============ 辅助函数 ============
function getBargainProgress(item: BargainItem): number {
  if (item.originalPrice <= 0) { return 0 }
  return Math.floor((item.originalPrice - item.currentPrice) / item.originalPrice * 100)
}
function getActiveBargainCount(): number {
  let count: number = 0
  for (let i = 0; i < mockMyBargains.length; i++) {
    if (mockMyBargains[i].status === '砍价中') { count++ }
  }
  return count
}
function getFreeItemTotal(): number {
  let total: number = 0
  for (let i = 0; i < mockFreeItems.length; i++) { total += mockFreeItems[i].originalPrice }
  return total
}
function getTotalSavedAmount(): number {
  let total: number = 0
  for (let i = 0; i < mockDailyStats.length; i++) { total += mockDailyStats[i].savedAmount }
  return total
}
function getHelpRecordCount(): number {
  let count: number = 0
  for (let i = 0; i < mockHelpRecords.length; i++) {
    if (!mockHelpRecords[i].isMe) { count++ }
  }
  return count
}

// ============ Tab 枚举 ============
enum BargainTab {
  HALL = 0,
  MY = 1,
  HELP = 2,
  FREE = 3,
  DAILY = 4
}

// ============ 入口页面 ============
@Entry
@Component
struct BargainApp {
  @State activeTab: BargainTab = BargainTab.HALL

  @Builder contentArea() {
    Column() {
      if (this.activeTab === BargainTab.HALL) {
        BargainHallContent()
      } else if (this.activeTab === BargainTab.MY) {
        MyBargainContent()
      } else if (this.activeTab === BargainTab.HELP) {
        HelpRecordContent()
      } else if (this.activeTab === BargainTab.FREE) {
        FreeItemsContent()
      } else {
        DailyReportContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: BargainTab) {
    Column() {
      Text(icon).fontSize(20).opacity(this.activeTab === tab ? 1.0 : 0.4)
      Text(label).fontSize(9)
        .fontColor(this.activeTab === tab ? COLORS.primary : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Column() {
      this.contentArea()
      Row() {
        this.bottomTabItem('🪓', '砍价大厅', BargainTab.HALL)
        this.bottomTabItem('⏳', '我的砍价', BargainTab.MY)
        this.bottomTabItem('🤝', '助力记录', BargainTab.HELP)
        this.bottomTabItem('🎁', '免费拿', BargainTab.FREE)
        this.bottomTabItem('📰', '省钱日报', BargainTab.DAILY)
      }
      .width('100%')
      .backgroundColor(COLORS.white)
      .padding({ top: 4, bottom: 8 })
      .shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
    }
    .width('100%').height('100%')
    .backgroundColor(COLORS.bg)
  }
}

// ============ 砍价大厅页 - 大卡片列表 ============
@Component
struct BargainHallContent {
  @State showStartModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedBargain: BargainItem | null = null
  @State selectedCategory: string = '全部'
  categories: string[] = ['全部', '数码', '家电', '美妆', '服饰', '食品', '家居', '母婴', '运动']

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.5)')
      .onClick(onClose)
  }

  // 弹框1:发起砍价弹框
  @Builder startModal() {
    Column() {
      this.modalOverlay(() => { this.showStartModal = false })
      Column() {
        Text('🪓 发起砍价').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
          .margin({ top: 18 })

        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[this.selectedBargain?.category ?? '数码'] ?? '🎁').fontSize(48)
            .margin({ top: 8 })
          Text(this.selectedBargain?.name ?? '').fontSize(15).fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary).margin({ top: 6 })
          Text(this.selectedBargain?.description ?? '').fontSize(11).fontColor(COLORS.textSecondary)
            .margin({ top: 4 })
          Row() {
            Text('原价 ¥' + (this.selectedBargain?.originalPrice ?? 0)).fontSize(12).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough })
            Text('→ 砍至 ¥0').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.primary).margin({ left: 10 })
          }
          .margin({ top: 8 })
        }
        .width('100%').alignItems(HorizontalAlign.Center)

        Column() {
          Text('每人可砍金额').fontSize(11).fontColor(COLORS.textSecondary)
          Text('¥' + (this.selectedBargain?.cutAmount ?? 0) + ' / 刀').fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent).margin({ top: 4 })
          Text('已有' + (this.selectedBargain?.participants ?? 0) + '人参砍').fontSize(10)
            .fontColor(COLORS.textHint).margin({ top: 4 })
        }
        .width('100%').alignItems(HorizontalAlign.Center).padding({ top: 14 })

        Row() {
          Text('取消').fontSize(14).fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.border).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .onClick(() => { this.showStartModal = false })
          Text('🔥 立即发起').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor(COLORS.primary).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showStartModal = false })
        }
        .justifyContent(FlexAlign.Center).margin({ top: 16, bottom: 20 })
      }
      .width('85%').backgroundColor(COLORS.white).borderRadius(20)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '7.5%', y: '25%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // 弹框2:商品详情弹框
  @Builder detailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetailModal = false })
      Column() {
        Row() {
          Text('商品详情').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
          Blank()
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showDetailModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 16, bottom: 12 })
        Divider().color(COLORS.border)

        Scroll() {
          Column() {
            Column() {
              Text(BARGAIN_CATEGORY_CONFIG[this.selectedBargain?.category ?? '数码'] ?? '🎁').fontSize(56)
            }.width('100%').height(140).backgroundColor((this.selectedBargain?.imageColor ?? '#999') + '20')
            .borderRadius(12).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
            .margin({ top: 12 })

            Text(this.selectedBargain?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary).width('100%').margin({ top: 12 })
            Text(this.selectedBargain?.description ?? '').fontSize(12).fontColor(COLORS.textSecondary)
              .width('100%').margin({ top: 6 })

            Row() {
              Text('¥0').fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
              Text('砍后价').fontSize(10).fontColor(COLORS.primary).margin({ left: 4 })
              Blank()
              Text('原价 ¥' + (this.selectedBargain?.originalPrice ?? 0)).fontSize(12).fontColor(COLORS.textHint)
                .decoration({ type: TextDecorationType.LineThrough })
            }
            .width('100%').margin({ top: 10 })

            Column() {
              Row() { Text('📦 发货时效').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text('砍价成功后48小时内').fontSize(12).fontColor(COLORS.textPrimary) }.width('100%').padding({ top: 8 })
              Row() { Text('🚚 运费').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text('全国包邮').fontSize(12).fontColor(COLORS.success) }.width('100%').padding({ top: 8 })
              Row() { Text('🛡️ 质保').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text('7天无理由退换').fontSize(12).fontColor(COLORS.textPrimary) }.width('100%').padding({ top: 8 })
              Row() { Text('👥 已参砍人数').fontSize(12).fontColor(COLORS.textSecondary); Blank(); Text((this.selectedBargain?.participants ?? 0) + '人').fontSize(12).fontColor(COLORS.accent).fontWeight(FontWeight.Bold) }.width('100%').padding({ top: 8 })
            }
            .width('100%').backgroundColor(COLORS.bg).borderRadius(12)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 }).margin({ top: 12 })

            Column() {
              Text('⭐⭐⭐⭐⭐ 用户好评').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).width('100%')
              Text('"砍到了就真的免费,已经拿到好几次了!" —— 小丽').fontSize(11).fontColor(COLORS.textSecondary).margin({ top: 6 })
              Text('"叫上家人朋友一起砍,很快就成功。" —— 阿强').fontSize(11).fontColor(COLORS.textSecondary).margin({ top: 4 })
            }
            .width('100%').padding({ left: 20, right: 20, top: 14, bottom: 14 })
          }
          .padding({ left: 16, right: 16 })
        }
        .layoutWeight(1)

        Row() {
          Text('收藏').fontSize(13).fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.border).borderRadius(18)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.showDetailModal = false })
          Text('🪓 发起砍价').fontSize(13).fontColor('#FFFFFF')
            .backgroundColor(COLORS.primary).borderRadius(18)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 10 })
            .onClick(() => { this.showDetailModal = false; this.showStartModal = true })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ top: 10, bottom: 16 })
      }
      .width('92%').height('85%').backgroundColor(COLORS.white).borderRadius(16)
      .position({ x: '4%', y: '7%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder bargainCardBuilder(item: BargainItem) {
    Column() {
      Row() {
        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[item.category] ?? '🎁').fontSize(36)
        }.width(90).height(90).backgroundColor(item.imageColor + '20').borderRadius(14)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Row() {
            Text(item.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
            if (item.hot) {
              Text('🔥').fontSize(12).margin({ left: 4 })
            }
          }
          Text(item.description).fontSize(10).fontColor(COLORS.textSecondary).margin({ top: 3 }).maxLines(1)
          Row() {
            Text('¥0').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
            Text('砍后免费拿').fontSize(9).fontColor(COLORS.primary).margin({ left: 4 })
            Text('¥' + item.originalPrice).fontSize(10).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 8 })
          }
          .margin({ top: 5 })
          Row() {
            Text('每人可砍 ¥' + item.cutAmount).fontSize(9).fontColor(COLORS.accent)
            Text('· ' + item.participants + '人已参砍').fontSize(9).fontColor(COLORS.textHint).margin({ left: 6 })
          }
          .margin({ top: 4 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
      }
      .width('100%')

      Row() {
        Column() {
          Text('砍价进度').fontSize(9).fontColor(COLORS.textSecondary)
          Row() {
            Column()
              .width(getBargainProgress(item) + '%')
              .height(6).backgroundColor(COLORS.primary).borderRadius(3)
            Column().layoutWeight(1)
          }
          .width('100%').height(6).backgroundColor(COLORS.border).borderRadius(3).margin({ top: 3 })
        }.layoutWeight(1)

        Text('发起砍价').fontSize(12).fontColor('#FFFFFF')
          .backgroundColor(COLORS.primary).borderRadius(16)
          .padding({ left: 18, right: 18, top: 8, bottom: 8 })
          .margin({ left: 12 })
          .onClick(() => { this.selectedBargain = item; this.showStartModal = true })
        Text('详情').fontSize(12).fontColor(COLORS.primary)
          .backgroundColor(COLORS.primaryLight).borderRadius(16)
          .padding({ left: 14, right: 14, top: 8, bottom: 8 })
          .margin({ left: 6 })
          .onClick(() => { this.selectedBargain = item; this.showDetailModal = true })
      }
      .width('100%').margin({ top: 10 })
    }
    .width('100%').backgroundColor(COLORS.white).borderRadius(14)
    .padding(14)
    .margin({ left: 12, right: 12, top: 8 })
  }

  build() {
    Stack() {
      Column() {
        // 头部
        Column() {
          Row() {
            Text('🪓').fontSize(22).margin({ left: 14 })
            TextInput({ placeholder: '搜索想砍的商品...' })
              .placeholderColor('#FFFFFF99').fontSize(13).layoutWeight(1)
              .backgroundColor('#FFFFFF33').borderRadius(20)
              .margin({ left: 8, right: 8 })
            Text('📢').fontSize(22).margin({ right: 14 })
          }
          .width('100%').padding({ top: 10, bottom: 8 })

          Row() {
            Column() {
              Text('16,847').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFEE58')
              Text('今日发起砍价').fontSize(9).fontColor('#FFFFFFCC')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text('2,356').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFEE58')
              Text('今日砍至免费').fontSize(9).fontColor('#FFFFFFCC')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text('¥580万').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFEE58')
              Text('累计省钱').fontSize(9).fontColor('#FFFFFFCC')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').padding({ top: 4, bottom: 12 })
        }
        .width('100%').backgroundColor(COLORS.primary)

        // 分类横滚
        Scroll() {
          Row() {
            ForEach(this.categories, (cat: string) => {
              Text(cat)
                .fontSize(11).fontColor(this.selectedCategory === cat ? '#FFFFFF' : COLORS.primary)
                .backgroundColor(this.selectedCategory === cat ? COLORS.primary : COLORS.primaryLight)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(14)
                .margin({ left: 3, right: 3 })
                .onClick(() => { this.selectedCategory = cat })
            })
          }
          .padding({ left: 8, right: 8 })
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36)
        .margin({ top: 8 })

        Scroll() {
          Column() {
            this.bargainCardBuilder(mockBargainItems[0])
            this.bargainCardBuilder(mockBargainItems[1])
            this.bargainCardBuilder(mockBargainItems[2])
            this.bargainCardBuilder(mockBargainItems[3])
            this.bargainCardBuilder(mockBargainItems[4])
            this.bargainCardBuilder(mockBargainItems[5])
            this.bargainCardBuilder(mockBargainItems[6])
            this.bargainCardBuilder(mockBargainItems[7])
            this.bargainCardBuilder(mockBargainItems[8])
            this.bargainCardBuilder(mockBargainItems[9])
            this.bargainCardBuilder(mockBargainItems[10])
            this.bargainCardBuilder(mockBargainItems[11])
            this.bargainCardBuilder(mockBargainItems[12])
            this.bargainCardBuilder(mockBargainItems[13])
            this.bargainCardBuilder(mockBargainItems[14])
            this.bargainCardBuilder(mockBargainItems[15])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showStartModal) { this.startModal() }
      if (this.showDetailModal) { this.detailModal() }
    }
    .width('100%').height('100%')
  }
}

// ============ 我的砍价页 - 进度卡片 ============
@Component
struct MyBargainContent {
  @State showSuccessModal: boolean = false
  @State showGiveUpConfirm: boolean = false
  @State selectedMy: MyBargainItem | null = null

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.5)')
      .onClick(onClose)
  }

  // 弹框3:砍价成功庆祝弹框
  @Builder successModal() {
    Column() {
      this.modalOverlay(() => { this.showSuccessModal = false })
      Column() {
        Text('🎉🎊🎉').fontSize(40).margin({ top: 24 })
        Text('恭喜砍价成功!').fontSize(22).fontWeight(FontWeight.Bold)
          .fontColor(COLORS.primary).margin({ top: 10 })
        Text('商品将免费寄送到您的地址').fontSize(12).fontColor(COLORS.textSecondary)
          .margin({ top: 6 })

        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[this.selectedMy?.category ?? '数码'] ?? '🎁').fontSize(48)
          Text(this.selectedMy?.itemName ?? '').fontSize(14).fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary).margin({ top: 6 })
          Row() {
            Text('¥' + (this.selectedMy?.originalPrice ?? 0)).fontSize(12).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough })
            Text('→ 免费获得').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.primary).margin({ left: 8 })
          }
          .margin({ top: 6 })
        }
        .width('100%').alignItems(HorizontalAlign.Center).margin({ top: 12 })

        Row() {
          Text('继续砍').fontSize(14).fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.border).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .onClick(() => { this.showSuccessModal = false })
          Text('填写地址').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor(COLORS.primary).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showSuccessModal = false })
        }
        .justifyContent(FlexAlign.Center).margin({ top: 16, bottom: 24 })
      }
      .width('80%').backgroundColor(COLORS.white).borderRadius(20)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '10%', y: '25%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // 弹框4:放弃砍价确认弹框
  @Builder giveUpConfirm() {
    Column() {
      this.modalOverlay(() => { this.showGiveUpConfirm = false })
      Column() {
        Text('⚠️').fontSize(40).margin({ top: 20 })
        Text('确认放弃砍价?').fontSize(17).fontWeight(FontWeight.Bold)
          .fontColor(COLORS.danger).margin({ top: 8 })
        Text('放弃后当前砍价进度将清零,好友助力记录将无法恢复!').fontSize(12)
          .fontColor(COLORS.textSecondary).textAlign(TextAlign.Center)
          .margin({ top: 8, left: 20, right: 20 })

        Column() {
          Row() {
            Text('当前进度').fontSize(11).fontColor(COLORS.textSecondary)
            Blank()
            Text((this.selectedMy?.progress ?? 0) + '%').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
          }
          .width('100%')
          Row() {
            Text('好友助力').fontSize(11).fontColor(COLORS.textSecondary)
            Blank()
            Text((this.selectedMy?.helpers ?? []).length + '人').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.accent)
          }
          .width('100%').margin({ top: 6 })
        }
        .width('100%').backgroundColor(COLORS.bg).borderRadius(12)
        .padding({ left: 14, right: 14, top: 10, bottom: 10 }).margin({ top: 12, left: 20, right: 20 })

        Row() {
          Text('继续砍价').fontSize(14).fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.border).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .onClick(() => { this.showGiveUpConfirm = false })
          Text('确认放弃').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor(COLORS.danger).borderRadius(18)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showGiveUpConfirm = false })
        }
        .justifyContent(FlexAlign.Center).margin({ top: 16, bottom: 20 })
      }
      .width('80%').backgroundColor(COLORS.white).borderRadius(20)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '10%', y: '30%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder myBargainCardBuilder(m: MyBargainItem) {
    Column() {
      Row() {
        Column() {
          Text(BARGAIN_CATEGORY_CONFIG[m.category] ?? '🎁').fontSize(32)
        }.width(70).height(70).backgroundColor(m.imageColor + '20').borderRadius(14)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Text(m.itemName).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
          Row() {
            Text('当前 ¥' + m.currentPrice).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
            Text('原价 ¥' + m.originalPrice).fontSize(10).fontColor(COLORS.textHint)
              .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 6 })
          }
          .margin({ top: 3 })
          Row() {
            Text('⏰ ' + m.remainingTime).fontSize(10)
              .fontColor(m.status === '已过期' ? COLORS.textHint : COLORS.danger)
              .fontWeight(FontWeight.Bold)
            Text('· ' + m.helpers.length + '人助力').fontSize(10).fontColor(COLORS.textSecondary).margin({ left: 6 })
          }
          .margin({ top: 3 })
          // 助力头像
          Row() {
            ForEach([0, 1, 2], (i: number) => {
              Column() {
                Text('👤').fontSize(10)
              }.width(22).height(22).backgroundColor(COLORS.border).borderRadius(11)
              .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
              .margin({ left: i > 0 ? -6 : 0 })
            })
            if (m.helpers.length > 3) {
              Text('+' + (m.helpers.length - 3)).fontSize(8).fontColor(COLORS.textSecondary)
                .margin({ left: 4 })
            }
          }
          .margin({ top: 4 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
      }
      .width('100%')

      // 进度条
      Column() {
        Row() {
          Column()
            .width(m.progress + '%')
            .height(8).backgroundColor(m.status === '已过期' ? '#CCCCCC' : COLORS.primary).borderRadius(4)
          Column().layoutWeight(1)
        }
        .width('100%').height(8).backgroundColor(COLORS.border).borderRadius(4)
        Row() {
          Text('已砍 ' + m.progress + '%').fontSize(9).fontColor(m.status === '已过期' ? COLORS.textHint : COLORS.primary)
          Blank()
          Text('还差 ¥' + m.currentPrice + ' 到免费').fontSize(9).fontColor(COLORS.accent)
        }
        .width('100%').margin({ top: 3 })
      }
      .width('100%').margin({ top: 8 })

      Row() {
        Text('邀请助力').fontSize(11).fontColor('#FFFFFF')
          .backgroundColor(m.status === '已过期' ? '#CCCCCC' : COLORS.primary).borderRadius(14)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 })
        Text('分享').fontSize(11).fontColor(COLORS.primary)
          .backgroundColor(COLORS.primaryLight).borderRadius(14)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 }).margin({ left: 8 })
        Blank()
        if (m.status === '已过期') {
          Text('重新发起').fontSize(11).fontColor(COLORS.accent)
            .backgroundColor(COLORS.accentLight).borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
        } else {
          Text('放弃').fontSize(11).fontColor(COLORS.danger)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .onClick(() => { this.selectedMy = m; this.showGiveUpConfirm = true })
        }
      }
      .width('100%').margin({ top: 8 })
    }
    .width('100%').backgroundColor(COLORS.white).borderRadius(14)
    .padding(14)
    .margin({ left: 12, right: 12, top: 8 })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('⏳ 我的砍价').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Blank()
            Text(getActiveBargainCount() + '个进行中').fontSize(10).fontColor('#FFFFFF')
              .backgroundColor('#FFFFFF33').padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .borderRadius(10).margin({ right: 14 })
          }
          .width('100%').padding({ left: 16, top: 12, bottom: 8 })
          Row() {
            Text('叫上好友一起砍,好物免费拿到家').fontSize(10).fontColor('#FFFFFFCC')
          }
          .width('100%').padding({ left: 16, bottom: 12 })
        }
        .width('100%').backgroundColor(COLORS.primaryDark)
        .borderRadius({ bottomLeft: 16, bottomRight: 16 })

        Scroll() {
          Column() {
            this.myBargainCardBuilder(mockMyBargains[0])
            this.myBargainCardBuilder(mockMyBargains[1])
            this.myBargainCardBuilder(mockMyBargains[2])
            this.myBargainCardBuilder(mockMyBargains[3])
            this.myBargainCardBuilder(mockMyBargains[4])
            this.myBargainCardBuilder(mockMyBargains[5])
            this.myBargainCardBuilder(mockMyBargains[6])
            this.myBargainCardBuilder(mockMyBargains[7])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showSuccessModal) { this.successModal() }
      if (this.showGiveUpConfirm) { this.giveUpConfirm() }
    }
    .width('100%').height('100%')
  }
}

// ============ 助力记录页 - 聊天气泡式 ============
@Component
struct HelpRecordContent {
  @State showInviteModal: boolean = false

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.5)')
      .onClick(onClose)
  }


              .width('100%').padding({ top: 8, bottom: 8 })
              Divider().color(COLORS.bg)
              Row() { Text('⏰').fontSize(14); Text('砍价有效期48小时,过期进度清零,抓紧时间').fontSize(11).fontColor(COLORS.textSecondary).margin({ left: 8 }).layoutWeight(1) }
              .width('100%').padding({ top: 8, bottom: 8 })
              Divider().color(COLORS.bg)
              Row() { Text('🎁').fontSize(14); Text('新用户首次砍价必成功,快邀请好友注册').fontSize(11).fontColor(COLORS.textSecondary).margin({ left: 8 }).layoutWeight(1) }
              .width('100%').padding({ top: 8, bottom: 8 })
            }
            .padding({ left: 16, right: 16 })
          }
          .width('100%').backgroundColor(COLORS.white).borderRadius(12)
          .margin({ left: 12, right: 12, top: 8, bottom: 20 })
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}


在这里插入图片描述

总结

本文对基于HarmonyOS API 24的ArkTS砍价免费拿社交电商应用进行了全链路深度解析。从数据模型定义到UI组件构建,从业务纯函数到模态弹框交互,该应用完整呈现了一个社交电商场景下"发现-发起-追踪-社交-成就"的用户使用闭环。应用通过七个核心接口定义了严格的数据契约,通过ColorPalette设计令牌系统保障了视觉一致性,通过六组Mock数据支撑了完整的业务模拟,充分体现了ArkTS声明式UI在构建复杂业务应用时的架构能力。

在技术实现层面,应用展示了多种ArkTS核心特性的实战应用。@Builder装饰器被广泛应用于卡片构建、弹框构建和导航项构建,实现了UI的高度复用;@State状态变量驱动了条件渲染和弹框显隐;Stack层叠配合position绝对定位和zIndex实现了模态弹框的层叠覆盖;ForEach实现了列表、网格和柱状图的数据驱动渲染。特别是进度条的双层Column结构、聊天气泡的非对称圆角设计、头像堆叠的负边距技巧等,都是ArkTS声明式UI中值得借鉴的设计模式。

设计了砍价大厅的商品发现、我的砍价的进度追踪、助力记录的社交互动、免费拿的成就展示、省钱日报的数据反馈五个页面,形成了一个完整的用户激励循环。游戏化设计元素贯穿全应用——进度条给予即时反馈、聊天气泡营造社交氛围、柱状图和排行榜激发竞争心理、省钱小贴士提供行动指引,这些设计共同构建了一个既有实用性又有情感温度的移动应用体验。对于希望深入学习HarmonyOS ArkTS声明式UI开发的开发者而言,本应用的架构设计、组件封装和交互模式都具有重要的参考价值。

Logo

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

更多推荐