一、引言:社区团购的崛起与技术挑战

近年来,社区团购作为一种新兴的零售业态,迅速渗透到中国城乡居民的日常生活中。它以社区为基本单元,以团长为纽带,将产地直发的生鲜、食品、日用品等商品通过集中采购、统一配送的方式送到消费者手中。这种模式的核心价值在于"以销定采"——通过提前拼团锁定需求,大幅降低了仓储损耗与物流成本,同时让消费者享受到比传统电商更具价格优势的商品。

在这里插入图片描述

对于普通家庭而言,社区团购意味着更低的生活成本和更便捷的购物体验。只需在手机上轻点几下,第二天便能在小区门口的自提点取到新鲜的水果、蔬菜、肉类和米面粮油。这种"线上拼团 + 线下自提"的模式,既规避了传统电商"最后一公里"配送成本高昂的问题,又解决了社区便利店商品品类有限的痛点。

然而,要支撑这样一个完整的团购业务闭环,开发者面临诸多技术挑战。首先是商品信息的复杂度——一个团购商品包含名称、分类、价格、团购价、成团人数、当前参团人数、库存、销量、评分、标签、产地、配送时段等十余个维度;其次是状态的多样性——团购本身有"进行中、已成团、待发货、已送达、已取消"五种状态,订单又有"待付款、已付款、配送中、已签收、已退款"五种状态;最后是交互的丰富度——发布团购、编辑商品、下架商品、查看详情、筛选分类、排序、查看订单详情等操作都需要精心设计。

HarmonyOS(鸿蒙)作为华为推出的分布式操作系统,其应用开发框架 ArkTS 提供了一套声明式 UI 编程范式。它结合了 TypeScript 的类型安全与声明式 UI 的简洁表达,非常适合构建这类信息密度高、交互复杂的企业级移动应用。本文将以一个完整的社区团购管理系统为例,逐段剖析其代码实现,深入讲解 ArkTS 在实际业务场景中的应用技巧。

二、整体架构概览

在深入代码细节之前,我们先从宏观层面把握这个应用的架构设计。整个系统采用"底部多 Tab + 单页内容区"的经典移动端布局,共包含五个主功能页:今日团购、我的订单、分类浏览、数据统计、个人中心。每个 Tab 页面独立封装为一个 @Component 组件,由入口页面根据当前选中的 Tab 动态切换渲染。

在视觉设计上,应用采用了一套温暖而富有活力的配色方案。主色调为活力橙红 #E64A19,辅以暖黄 #FFB300、鲜绿 #43A047,背景使用米白 #FFF8E1,营造出亲切、新鲜的社区生活氛围。这种配色非常契合生鲜团购的业务调性——橙红代表活力与促销,暖黄暗示温暖与实惠,鲜绿象征新鲜与健康。

布局方面,商品列表采用 Grid 双列卡片布局,这是电商类应用展示商品的主流方式,能在有限屏幕空间内同时展示较多商品信息。分类筛选采用横向滚动条,排序选项采用标签式切换,统计页面则使用自绘的柱状图来直观呈现一周消费趋势。整体架构清晰、职责分明,下面我们逐段解析代码实现。

三、接口类型定义:构建类型安全的数据契约

ArkTS 作为 TypeScript 的超集,天然支持接口(interface)类型定义。在本系统中,开发者首先为各类元数据定义了清晰的接口契约,这是构建可维护、可扩展应用的第一步。

3.1 团购状态元数据接口

interface GroupStatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

在这里插入图片描述

这段代码定义了团购状态的元数据结构。每一行都有明确的职责:

  • label: string:状态的显示文本,如"进行中"、“已成团”,用于在界面上直接呈现给用户。
  • color: string:状态对应的主色值,用于文字颜色,确保不同状态在视觉上有明显区分。
  • bg: string:状态对应的背景色,用于标签的底色填充,与 color 配合形成柔和的状态徽章。
  • icon: string:状态对应的 emoji 图标,如"🔥"表示进行中,"✅"表示已成团,增加界面的趣味性与可读性。

通过将状态的所有视觉属性集中在一个接口中定义,后续新增状态时只需扩展配置,无需修改渲染逻辑,体现了"数据驱动 UI"的设计思想。

3.2 商品分类元数据接口

interface ProductCatMeta {
  label: string
  icon: string
  color: string
  bg: string
}

在这里插入图片描述

该接口定义了商品分类的元数据结构。与团购状态接口类似,它包含了分类标签、图标、主色和背景色四个字段。不同之处在于省去了 label 之外的额外语义字段,因为分类的视觉表达相对统一。例如"生鲜蔬果"对应鲜绿色 #43A047,"肉禽蛋品"对应橙红色 #E64A19,"乳品饮料"对应蓝色 #1565C0,每种分类都有专属的视觉身份。

3.3 订单状态与配送时段接口

interface OrderStatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

interface DeliverySlotMeta {
  label: string
  time: string
  icon: string
}

在这里插入图片描述

OrderStatusMeta 与团购状态接口结构完全一致,但语义不同——它描述的是订单的流转状态。"待付款"用沙漏图标"⏳"表示,"配送中"用卡车图标"🚚"表示,"已退款"用回退箭头"↩️"表示,图标选择贴合实际业务语义。

DeliverySlotMeta 则定义了配送时段的结构,包含显示标签、具体时间和图标。社区团购的核心特色之一就是灵活的自提时段,如"今日16点自提"、“明日10点自提”、"送货上门"等,这个接口将这些选项结构化管理。

3.4 柱状图数据项接口

interface BarChartItem {
  label: string
  value: number
  color: string
}

在这里插入图片描述

这个简洁的接口用于定义柱状图中每一根柱子的数据。label 是横轴标签(如"周一"),value 是数值(如 320),color 是柱子颜色。通过这个接口,柱状图的渲染逻辑可以完全通用化,只需传入不同的数据数组即可。

3.5 调色板接口

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

在这里插入图片描述

这是一个完整的调色板接口,定义了应用所有可用的颜色令牌。每一行都承载着明确的视觉职责:

  • primary / primaryLight / primaryDark:主色的三个层次,分别用于不同强调程度的场景。主色 #E64A19 用于按钮、选中态;浅色 #FFAB91 用于柔和背景;深色 #BF360C 用于渐变终点和强调文字。
  • accent / accentLight:辅助色暖黄及其浅色变体,用于次级强调,如排序选中态、评分星级。
  • success / successLight:成功色鲜绿及其浅色,用于成团状态、积极反馈。
  • bg / cardBg:页面背景色与卡片背景色,米白 #FFF8E1 与纯白 #FFFFFF 形成层次。
  • textPrimary / textSecondary / textHint:三级文字色,深棕 #3E2723 用于主标题,灰棕 #795548 用于副文本,浅灰 #BCAAA4 用于提示文字。
  • border / white:边框色与纯白色,用于分割线与通用背景。

通过接口约束调色板,确保所有颜色使用都来自统一的令牌系统,避免了散落的硬编码颜色值,便于后续主题切换与设计迭代。

四、颜色令牌系统:统一视觉语言

定义完接口后,开发者用具体的常量实现了这套调色板。

const COLORS: ColorPalette = {
  primary: '#E64A19',
  primaryLight: '#FFAB91',
  primaryDark: '#BF360C',
  accent: '#FFB300',
  accentLight: '#FFE082',
  success: '#43A047',
  successLight: '#C8E6C9',
  bg: '#FFF8E1',
  cardBg: '#FFFFFF',
  textPrimary: '#3E2723',
  textSecondary: '#795548',
  textHint: '#BCAAA4',
  border: '#FFF3E0',
  white: '#FFFFFF'
}

在这里插入图片描述

这段代码创建了一个名为 COLORS 的常量对象,严格遵循 ColorPalette 接口定义。每一个颜色值都经过精心搭配:主色 #E64A19 是一种饱和度较高的橙红色,传递活力与促销感;深色 #BF360C 是主色的深暗变体,用于渐变效果增加立体感;背景 #FFF8E1 是一种极淡的暖黄米白,让整个界面显得温暖而不刺眼。

值得注意的是,文本颜色采用了棕色系(#3E2723#795548#BCAAA4)而非常见的灰黑系,这种选择与暖色背景更加协调,营造出统一的"温暖"视觉基调。边框色 #FFF3E0 比背景色略深一点,既能起到分割作用又不至于过于突兀。

不过在实际的组件代码中,开发者为了书写简洁,较多直接使用了十六进制色值而非引用 COLORS 常量。这在小型应用中尚可接受,但在大型项目中建议统一通过令牌引用,以保证主题的一致性与可维护性。

五、配置映射:数据驱动的状态与分类管理

本系统最大的设计亮点之一,是将所有状态、分类、时段等业务配置抽离为独立的映射表。这种"配置即数据"的方式,让业务逻辑与展示逻辑彻底解耦。

5.1 团购状态配置

const GROUP_STATUS_CONFIG: Record<string, GroupStatusMeta> = {
  '进行中': { label: '进行中', color: '#E64A19', bg: '#FFF3E0', icon: '🔥' },
  '已成团': { label: '已成团', color: '#43A047', bg: '#E8F5E9', icon: '✅' },
  '待发货': { label: '待发货', color: '#FFB300', bg: '#FFF8E1', icon: '📦' },
  '已送达': { label: '已送达', color: '#1565C0', bg: '#E3F2FD', icon: '📮' },
  '已取消': { label: '已取消', color: '#9E9E9E', bg: '#F5F5F5', icon: '❌' }
}

在这里插入图片描述

这里定义了一个 Record<string, GroupStatusMeta> 类型的映射对象,键是状态的中文文本,值是对应的元数据。五条配置涵盖了团购的完整生命周期:

  • "进行中"用火焰图标和橙红色,表示火热拼团中。
  • "已成团"用勾选图标和鲜绿色,表示成功达成。
  • "待发货"用包裹图标和暖黄色,表示等待发货。
  • "已送达"用邮箱图标和蓝色,表示已送达自提点。
  • "已取消"用叉号图标和灰色,表示已取消。

使用 Record<string, GroupStatusMeta> 类型的好处是类型安全且查找高效,通过 GROUP_STATUS_CONFIG[status] 即可 O(1) 时间获取任意状态的完整视觉信息。

5.2 商品分类配置

const PRODUCT_CAT_CONFIG: Record<string, ProductCatMeta> = {
  '生鲜蔬果': { label: '生鲜蔬果', icon: '🥬', color: '#43A047', bg: '#E8F5E9' },
  '肉禽蛋品': { label: '肉禽蛋品', icon: '🥩', color: '#E64A19', bg: '#FFEBEE' },
  '米面粮油': { label: '米面粮油', icon: '🍚', color: '#FFB300', bg: '#FFF8E1' },
  '乳品饮料': { label: '乳品饮料', icon: '🥛', color: '#1565C0', bg: '#E3F2FD' },
  '零食糕点': { label: '零食糕点', icon: '🍪', color: '#7B1FA2', bg: '#F3E5F5' },
  '日用百货': { label: '日用百货', icon: '🧴', color: '#00838F', bg: '#E0F7FA' }
}

在这里插入图片描述

商品分类配置定义了六大品类的视觉身份。每个品类不仅有专属的 emoji 图标,还有独立的主题色和背景色。"生鲜蔬果"用鲜绿色暗示新鲜健康,"肉禽蛋品"用橙红暗示蛋白质,"乳品饮料"用蓝色暗示清凉,"零食糕点"用紫色增加趣味性,"日用百货"用青色表示清洁。这种色彩编码让用户在浏览时能快速识别品类,提升了信息获取效率。

5.3 订单状态配置

const ORDER_STATUS_CONFIG: Record<string, OrderStatusMeta> = {
  '待付款': { label: '待付款', color: '#E64A19', bg: '#FFF3E0', icon: '⏳' },
  '已付款': { label: '已付款', color: '#FFB300', bg: '#FFF8E1', icon: '💰' },
  '配送中': { label: '配送中', color: '#1565C0', bg: '#E3F2FD', icon: '🚚' },
  '已签收': { label: '已签收', color: '#43A047', bg: '#E8F5E9', icon: '✅' },
  '已退款': { label: '已退款', color: '#9E9E9E', bg: '#F5F5F5', icon: '↩️' }
}

在这里插入图片描述

订单状态配置覆盖了订单从下单到完结的五个阶段。图标的选择颇具匠心:"⏳"沙漏暗示等待付款的紧迫感,"💰"钱袋表示已完成支付,"🚚"卡车直观表达运输中,"✅"勾选代表已签收,"↩️"回退箭头表示退款。颜色搭配遵循"红黄蓝绿灰"的渐进逻辑,与状态流转的方向感一致。

5.4 配送时段与筛选选项

const DELIVERY_SLOTS: DeliverySlotMeta[] = [
  { label: '今日16点自提', time: '16:00', icon: '🏪' },
  { label: '今日18点自提', time: '18:00', icon: '🏪' },
  { label: '明日10点自提', time: '10:00', icon: '🏪' },
  { label: '明日16点自提', time: '16:00', icon: '🏪' },
  { label: '送货上门', time: '次日达', icon: '🚚' }
]

const CAT_FILTERS: string[] = ['全部', '生鲜蔬果', '肉禽蛋品', '米面粮油', '乳品饮料', '零食糕点', '日用百货']
const SORT_OPTIONS: string[] = ['人气优先', '价格从低到高', '价格从高到低', '成团速度']

DELIVERY_SLOTS 是一个数组而非映射,因为配送时段是有序且固定的。它提供了五个时段选项:四个自提时段覆盖了今日和明天的两个时间点,外加一个"送货上门"选项。"🏪"商店图标表示自提,"🚚"卡车表示配送,语义清晰。

CAT_FILTERS 是分类筛选的选项数组,首位增加了"全部"选项以便查看所有商品。SORT_OPTIONS 定义了四种排序方式,涵盖了用户最常见的排序需求——人气、价格、成团速度,体现了对用户购物心理的深入理解。

5.5 周消费柱状图数据

const WEEKLY_BAR: BarChartItem[] = [
  { label: '周一', value: 320, color: '#FFAB91' },
  { label: '周二', value: 450, color: '#FFAB91' },
  { label: '周三', value: 580, color: '#FF8A65' },
  { label: '周四', value: 420, color: '#FFAB91' },
  { label: '周五', value: 720, color: '#E64A19' },
  { label: '周六', value: 890, color: '#BF360C' },
  { label: '周日', value: 660, color: '#FF8A65' }
]

这组数据模拟了一周内每天的订单量。数据的分布符合社区团购的真实规律:工作日相对平稳,周五开始上升,周六达到峰值 890,周日有所回落但仍高于工作日。颜色也随数值变化——数值越高,颜色越深越饱和,从浅橙 #FFAB91 渐变到深红 #BF360C,让柱状图本身就承载了热力图的视觉信息,用户一眼就能看出周末是消费高峰。

六、数据模型设计:可观察的业务实体

ArkTS 提供了 @Observed 装饰器,用于标记可观察的数据类。被标记的类实例在属性变化时能自动触发依赖该数据的 UI 刷新,这是 ArkTS 响应式编程的核心机制之一。

6.1 团购商品数据模型

@Observed
export class GroupProductItem {
  id: number = 0
  name: string = ''
  category: string = ''
  emoji: string = '🛒'
  price: number = 0
  originalPrice: number = 0
  unit: string = ''
  groupPrice: number = 0
  minGroupSize: number = 0
  currentCount: number = 0
  status: string = '进行中'
  sold: number = 0
  stock: number = 0
  rating: number = 0
  tags: string[] = []
  origin: string = ''
  deliveryTip: string = ''
  isHot: boolean = false
  isNew: boolean = false
  constructor(id: number, name: string, category: string, emoji: string, price: number, originalPrice: number,
    unit: string, groupPrice: number, minGroupSize: number, currentCount: number, status: string,
    sold: number, stock: number, rating: number, tags: string[], origin: string,
    deliveryTip: string, isHot: boolean, isNew: boolean) {
    this.id = id; this.name = name; this.category = category; this.emoji = emoji
    this.price = price; this.originalPrice = originalPrice; this.unit = unit
    this.groupPrice = groupPrice; this.minGroupSize = minGroupSize; this.currentCount = currentCount
    this.status = status; this.sold = sold; this.stock = stock; this.rating = rating
    this.tags = tags; this.origin = origin; this.deliveryTip = deliveryTip
    this.isHot = isHot; this.isNew = isNew
  }
}

这是团购商品的核心数据模型,@Observed 装饰器确保其实例可被 UI 观察。类中定义了 19 个属性,覆盖了一个团购商品的全部信息维度:

  • 基础信息id(唯一标识)、name(商品名)、category(分类)、emoji(图标),这四个属性构成商品的识别基础。
  • 价格体系price(原价)、originalPrice(划线价)、groupPrice(团购价),形成"原价-团购价"的对比展示,突出团购优惠。
  • 规格信息unit(单位,如"5斤装"、“1kg”),让用户清楚了解商品的规格。
  • 成团机制minGroupSize(最低成团人数)、currentCount(当前参团人数),这两个属性是团购业务的核心,决定了团是否成立。
  • 状态与销量status(团购状态)、sold(已售件数)、stock(库存),用于展示商品热度与剩余情况。
  • 品质信息rating(评分)、tags(标签数组,如"产地直发"、“坏果包赔”)、origin(产地),帮助用户决策。
  • 物流信息deliveryTip(配送提示,如"今日16点自提")。
  • 营销标记isHot(是否热销)、isNew(是否新品),用于在卡片上显示角标。

构造函数接收全部 19 个参数并依次赋值。所有属性都有默认值初始化(如 id: number = 0),这是 ArkTS 的良好实践,确保即使通过空构造创建实例也不会出现 undefined 问题。tags 默认为空数组而非 null,避免了空指针异常。export 关键字使该类可被其他模块导入复用。

6.2 订单数据模型

@Observed
export class OrderItem {
  id: number = 0
  productName: string = ''
  productEmoji: string = '🛒'
  quantity: number = 0
  unitPrice: number = 0
  totalPrice: number = 0
  status: string = '待付款'
  orderDate: string = ''
  deliverySlot: string = ''
  pickupPoint: string = ''
  receiver: string = ''
  phone: string = ''
  remark: string = ''
  constructor(id: number, productName: string, productEmoji: string, quantity: number,
    unitPrice: number, totalPrice: number, status: string, orderDate: string,
    deliverySlot: string, pickupPoint: string, receiver: string, phone: string, remark: string) {
    this.id = id; this.productName = productName; this.productEmoji = productEmoji
    this.quantity = quantity; this.unitPrice = unitPrice; this.totalPrice = totalPrice
    this.status = status; this.orderDate = orderDate; this.deliverySlot = deliverySlot
    this.pickupPoint = pickupPoint; this.receiver = receiver; this.phone = phone; this.remark = remark
  }
}

OrderItem 是订单的数据模型,同样使用 @Observed 装饰。它包含 13 个属性:

  • 商品关联productNameproductEmoji,冗余存储商品名称和图标,避免订单与商品的关联查询。
  • 交易信息quantity(数量)、unitPrice(单价)、totalPrice(总价),totalPrice = quantity * unitPrice
  • 流转状态status,关联 ORDER_STATUS_CONFIG 获取视觉信息。
  • 时间信息orderDate(下单日期,如"07-15")。
  • 物流信息deliverySlot(配送时段)、pickupPoint(自提点地址)。
  • 收货信息receiver(收货人)、phone(联系电话,已脱敏如"138****8888")、remark(备注)。

电话号码采用脱敏存储(中间四位用星号替代),这是符合数据安全规范的实践。remark 默认为空字符串,在详情页会通过 if (remark !== '') 判断是否显示备注区块,避免出现空的备注区域。

七、模拟数据构造:还原真实业务场景

为了让应用在无后端的情况下也能完整运行,开发者构造了两组详实的模拟数据。

7.1 团购商品模拟数据

模拟数据包含 24 个团购商品,覆盖了六大品类,每个商品都有真实的产地、价格和标签信息。我们来看几个典型条目:

new GroupProductItem(1, '山东烟台红富士苹果', '生鲜蔬果', '🍎', 29.9, 49.9, '5斤装', 25.9, 20, 18, '进行中', 320, 500, 4.8, ['产地直发', '坏果包赔'], '山东烟台', '今日16点自提', true, false)

以第一个商品为例逐字段解析:商品 ID 为 1,名称"山东烟台红富士苹果"点明了品种和产地,分类为"生鲜蔬果",图标使用红苹果 emoji。原价 49.9 元,团购价 25.9 元,相当于半价优惠,单位是"5斤装"。最低成团人数 20 人,当前已有 18 人参团,状态"进行中"。已售 320 件,库存 500 件,评分 4.8 分。标签包括"产地直发"和"坏果包赔",产地山东烟台,配送提示"今日16点自提"。标记为热销商品。

这组数据的设计非常用心——所有商品都使用了真实的中国地理标志产地,如"内蒙古锡林郭勒"的牛腱子、"黑龙江五常"的稻花香大米、"云南丽江"的有机蔬菜、"宁夏中宁"的枸杞、"海南三亚"的贵妃芒、"新疆阿克苏"的冰糖心苹果等。这种真实感让应用在演示时就具备了产品的质感。

商品的团购价与原价之间普遍有 30%-50% 的折扣,体现了社区团购的价格优势。成团进度(currentCount / minGroupSize)各不相同,有的接近成团,有的刚开始,模拟了真实的拼团进度。部分商品标记为"已成团"状态,展示了不同阶段的商品。

7.2 订单模拟数据

new OrderItem(1, '山东烟台红富士苹果', '🍎', 2, 25.9, 51.8, '已签收', '07-15', '今日16点自提', '阳光小区自提点', '张先生', '138****8888', '请选大果')

订单模拟数据包含 15 条记录,覆盖了全部五种订单状态。第一条订单:ID 为 1,商品为红富士苹果,购买 2 件,单价 25.9 元,总价 51.8 元,状态"已签收",下单日期 07-15,配送方式"今日16点自提",自提点"阳光小区自提点",收货人"张先生",电话已脱敏,备注"请选大果"。

订单数据中出现了两位用户——“张先生"和"李女士”,以及两个自提点——“阳光小区自提点"和"社区团购驿站”,模拟了多用户、多自提点的真实场景。备注信息也各不相同,有的要求"放门口即可",有的要求"送人的包装好点",有的标注"品质问题退款",生动还原了真实订单的多样性。

八、统计函数:纯函数式的数据聚合

系统定义了一组统计函数,用于从模拟数据中聚合出关键指标。这些函数都是纯函数——不修改外部状态,只根据输入返回计算结果。

function getTotalProducts(): number { return mockProducts.length }

getTotalProducts 返回在售商品总数,直接读取数组长度,时间复杂度 O(1)。

function getActiveGroups(): number {
  let count: number = 0
  for (let i = 0; i < mockProducts.length; i++) {
    if (mockProducts[i].status === '进行中') { count++ }
  }
  return count
}

getActiveGroups 统计正在进行中的团购数量。它遍历商品数组,用计数器累加状态为"进行中"的商品。使用传统的 for 循环而非 filter + length,在 ArkTS 中这种写法更加类型友好且性能可预期。

function getTotalOrders(): number { return mockOrders.length }

function getPendingPickup(): number {
  let count: number = 0
  for (let i = 0; i < mockOrders.length; i++) {
    if (mockOrders[i].status === '配送中' || mockOrders[i].status === '已付款') { count++ }
  }
  return count
}

getTotalOrders 返回订单总数。getPendingPickup 统计待提货订单数,逻辑是状态为"配送中"或"已付款"的订单都需要用户关注,这两个状态代表订单已确认但尚未到达最终态。

function getTotalSales(): number {
  let total: number = 0
  for (let i = 0; i < mockOrders.length; i++) {
    if (mockOrders[i].status !== '已退款' && mockOrders[i].status !== '待付款') {
      total += mockOrders[i].totalPrice
    }
  }
  return Math.round(total)
}

getTotalSales 计算累计消费金额。它排除了"已退款"和"待付款"的订单——退款订单不应计入消费,待付款订单尚未形成实际消费。最后用 Math.round 取整,避免浮点数精度问题。这种业务逻辑的严谨性体现了对真实场景的考量。

function getHotProducts(): number {
  let count: number = 0
  for (let i = 0; i < mockProducts.length; i++) {
    if (mockProducts[i].isHot) { count++ }
  }
  return count
}

function getMaxBarValue(): number {
  let max: number = 0
  for (let i = 0; i < WEEKLY_BAR.length; i++) {
    if (WEEKLY_BAR[i].value > max) { max = WEEKLY_BAR[i].value }
  }
  return max
}

getHotProducts 统计热销商品数量。getMaxBarValue 获取柱状图中的最大值,这个值在渲染柱状图时用于归一化——所有柱子的高度按 value / max * 基准高度 计算,确保最高的柱子恰好填满图表区域。这种"动态归一化"的做法比硬编码最大值更加灵活,数据变化时图表会自动适配。

九、底部 Tab 枚举:类型安全的导航控制

enum GroupBuyTab {
  TODAY = 0,
  ORDERS = 1,
  CATEGORY = 2,
  STATS = 3,
  PROFILE = 4
}

这里定义了一个枚举类型 GroupBuyTab,包含五个成员,分别对应底部 Tab 的五个页面。使用枚举而非魔法数字(如直接用 0、1、2、3、4)有三大优势:

第一,可读性强。this.activeTab === GroupBuyTab.TODAYthis.activeTab === 0 更易理解意图。第二,类型安全。枚举成员是命名常量,编译器能检查拼写错误,而数字则无法。第三,可维护性好。如果未来调整 Tab 顺序,只需修改枚举定义,所有引用处自动更新。

十、入口页面:应用骨架与导航中枢

入口页面是整个应用的骨架,负责根据当前 Tab 渲染对应内容区,并渲染底部导航栏。

10.1 组件声明与状态定义

@Entry
@Component
struct GroupBuyApp {
  @State activeTab: GroupBuyTab = GroupBuyTab.TODAY

  @Builder contentArea() {
    Column() {
      if (this.activeTab === GroupBuyTab.TODAY) {
        TodayGroupContent()
      } else if (this.activeTab === GroupBuyTab.ORDERS) {
        MyOrdersContent()
      } else if (this.activeTab === GroupBuyTab.CATEGORY) {
        CategoryBrowseContent()
      } else if (this.activeTab === GroupBuyTab.STATS) {
        StatsContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

@Entry 装饰器标记这是应用的入口组件,@Component 声明这是一个自定义组件。@State activeTab 是组件的响应式状态,初始值为 TODAY,当它变化时,依赖它的 UI 会自动刷新。

contentArea 是一个 @Builder 方法,相当于一个可复用的 UI 片段构造器。它通过一系列 if-else if 判断当前 Tab,渲染对应的子组件。Column().layoutWeight(1) 让内容区占据除底部导航外的所有剩余空间。

10.2 底部 Tab 项构造器

  @Builder bottomTabItem(icon: string, label: string, tab: GroupBuyTab) {
    Column() {
      Text(icon).fontSize(22).opacity(this.activeTab === tab ? 1.0 : 0.4)
      Text(label).fontSize(10)
        .fontColor(this.activeTab === tab ? '#E64A19' : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
      if (this.activeTab === tab) {
        Row().width(20).height(3)
          .backgroundColor('#E64A19').borderRadius(2).margin({ top: 3 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 6, bottom: 6 })
    .onClick(() => { this.activeTab = tab })
  }

bottomTabItem 是一个参数化的 Builder,接收图标、标签和 Tab 枚举三个参数,动态生成一个 Tab 项。它的设计体现了"选中态"的视觉反馈:

  • 图标透明度:选中时 1.0(完全不透明),未选中时 0.4(半透明),通过透明度差异区分状态。
  • 标签颜色:选中时橙红 #E64A19,未选中时灰色 #999999
  • 标签字重:选中时加粗 FontWeight.Bold,未选中时常规 FontWeight.Normal
  • 选中指示器:仅选中时显示一个 20x3 的橙红色圆角小条,位于标签下方,作为活跃 Tab 的明确标识。

onClick 回调将 activeTab 设置为对应枚举值,触发 UI 刷新。layoutWeight(1) 让五个 Tab 项均分底部栏宽度,alignItems(HorizontalAlign.Center) 确保内容居中对齐。

10.3 主构建方法

  build() {
    Column() {
      this.contentArea()
      Row() {
        this.bottomTabItem('🔥', '今日团购', GroupBuyTab.TODAY)
        this.bottomTabItem('📦', '我的订单', GroupBuyTab.ORDERS)
        this.bottomTabItem('🗂️', '分类', GroupBuyTab.CATEGORY)
        this.bottomTabItem('📊', '统计', GroupBuyTab.STATS)
        this.bottomTabItem('👤', '我的', GroupBuyTab.PROFILE)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 8 })
      .shadow({ radius: 10, color: '#1A000000', offsetY: -3 })
    }
    .width('100%').height('100%')
    .backgroundColor('#FFF8E1')
  }
}

build 方法是组件的入口,使用 Column 将内容区和底部导航栏纵向排列。底部 Row 包含五个 bottomTabItem 调用,分别传入对应的图标、标签和枚举值。

底部栏的样式值得注意:白色背景 #FFFFFF,上下内边距不对称(上 4 下 8),底部留更多空间以适配手势区域。shadow 属性添加了向上的投影——offsetY: -3 让阴影向上偏移,配合 radius: 10 和半透明黑色 #1A000000,营造出底部栏"浮"在内容上方的层次感。最外层 Column 设置了米白背景 #FFF8E1 作为全局底色。

十一、今日团购页:核心业务功能区

今日团购页是应用的主页面,也是功能最丰富的页面。它包含顶部渐变头部、统计条、分类筛选、排序栏、商品网格列表,以及发布、编辑、下架、详情四个弹窗。

11.1 状态声明

@State selectedCat: string = '全部'
@State selectedSort: string = '人气优先'
@State showAddModal: boolean = false
@State showEditModal: boolean = false
@State showDeleteConfirm: boolean = false
@State showDetailModal: boolean = false
@State selectedProduct: GroupProductItem | null = null
@State editingProduct: GroupProductItem | null = null
@State formName: string = ''
@State formCat: string = '生鲜蔬果'
@State formPrice: string = ''
@State formGroupPrice: string = ''
@State formMinSize: string = '20'
@State formStock: string = ''
@State formOrigin: string = ''
@State formDelivery: string = '今日16点自提'
@State formTags: string = ''

这里声明了 15 个响应式状态,可分为四组:

  • 筛选状态selectedCat(当前选中的分类,默认"全部")、selectedSort(当前排序方式,默认"人气优先")。
  • 弹窗显隐状态showAddModalshowEditModalshowDeleteConfirmshowDetailModal,四个布尔值分别控制四个弹窗的显示。
  • 上下文状态selectedProduct(当前查看详情的商品)、editingProduct(当前编辑的商品),类型为 GroupProductItem | null,支持空值。
  • 表单状态formNameformCatformPrice 等 8 个字段,用于发布团购表单的数据绑定。

注意表单字段都使用 string 类型而非 number,这是因为 TextInputonChange 回调返回的是字符串,使用字符串状态避免了频繁的类型转换,在提交时再做转换即可。

11.2 遮罩层构造器

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

modalOverlay 是一个可复用的遮罩层 Builder。它接收一个 onClose 回调函数作为参数,生成一个全屏半透明黑色遮罩(rgba(0,0,0,0.5)),点击时触发回调关闭弹窗。将遮罩抽离为独立 Builder,避免了在每个弹窗中重复编写遮罩代码,符合 DRY 原则。

11.3 发布团购弹窗

发布团购弹窗是表单最复杂的弹窗,包含商品名称、分类、价格、成团人数、库存、产地、配送时段、标签等多个输入项。

@Builder addProductModal() {
  Column() {
    this.modalOverlay(() => { this.showAddModal = false })
    Column() {
      Row() {
        Text('➕ 发布新团购').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#999999')
          .onClick(() => { this.showAddModal = false })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
      Divider().color('#FFF3E0')

弹窗顶部是一个 Row,左侧标题"➕ 发布新团购",右侧关闭按钮"✕"。中间用 Row().layoutWeight(1) 占位将关闭按钮推到最右。Divider 作为标题与表单的分隔线,颜色使用浅米色 #FFF3E0 保持柔和。

      Scroll() {
        Column() {
          Text('商品名称').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
          TextInput({ placeholder: '输入商品名称...' })
            .placeholderColor('#BCAAA4').fontSize(14).width('100%')
            .backgroundColor('#FFF8E1').borderRadius(8)
            .margin({ left: 20, right: 20, top: 4 })
            .onChange((v: string) => { this.formName = v })

表单采用 Scroll 包裹,确保内容超出弹窗高度时可滚动。每个字段由标签 Text 和输入框 TextInput 组成。标签使用 12 号字、棕色 #795548,输入框背景使用浅米黄 #FFF8E1,圆角 8,placeholder 用浅灰 #BCAAA4onChange 回调将输入值同步到对应的 @State 变量。

          Text('商品分类').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
          Row() {
            ForEach(CAT_FILTERS.slice(1), (c: string) => {
              if (this.formCat === c) {
                Text(PRODUCT_CAT_CONFIG[c]?.icon + ' ' + c)
                  .fontSize(11).fontColor('#FFFFFF').backgroundColor('#E64A19')
                  .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
                  .margin({ left: 3, right: 3 })
              } else {
                Text(PRODUCT_CAT_CONFIG[c]?.icon + ' ' + c)
                  .fontSize(11).fontColor('#E64A19').backgroundColor('#FFF3E0')
                  .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
                  .margin({ left: 3, right: 3 })
                  .onClick(() => { this.formCat = c })
              }
            })
          }
          .margin({ left: 16, right: 16, top: 4 })

分类选择采用了标签式 UI 而非下拉选择器。ForEach 遍历 CAT_FILTERS.slice(1)(去掉"全部"选项),为每个分类生成一个标签。选中态为白字橙红底,未选中态为橙红字浅米底,点击切换。每个标签前拼上分类图标,如"🥬 生鲜蔬果"。这种标签式选择比下拉框更直观,操作也更便捷。

价格、成团人数、库存等字段使用 Row 并排排列两个 TextInput,通过 layoutWeight(1) 平分宽度,中间用 margin({ left: 8 }) 留出间距。配送时段同样采用标签选择,遍历 DELIVERY_SLOTS.slice(0, 3) 只显示前三个自提选项。

弹窗底部是"取消"和"发布团购"两个按钮,分别使用浅米底和橙红底,通过 justifyContent(FlexAlign.Center) 居中排列。整个弹窗用 positionzIndex(999) 定位在最上层,宽度 90%,最大高度 80%,圆角 16。

11.4 编辑与下架弹窗

编辑弹窗与发布弹窗结构类似,但字段更少——只编辑团购价、库存和配送时段。它通过 editingProduct?.name 等可选链读取当前编辑商品的值作为 placeholder,体现了对空值的防御性处理。

@Builder deleteConfirmModal() {
  Column() {
    this.modalOverlay(() => { this.showDeleteConfirm = false })
    Column() {
      Text('⚠️').fontSize(48).margin({ top: 24 })
      Text('确认下架此团购?').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
      Text('下架后已下单用户不受影响').fontSize(13).fontColor('#E64A19').margin({ top: 4 })
      Row() {
        Text(this.selectedProduct?.emoji ?? '🛒').fontSize(20)
        Text(this.selectedProduct?.name ?? '').fontSize(14).fontColor('#333333')
          .fontWeight(FontWeight.Bold).margin({ left: 8 })
      }
      .backgroundColor('#FFF3E0').borderRadius(10)
      .padding({ left: 16, right: 16, top: 10, bottom: 10 }).margin({ top: 16 })

下架确认弹窗采用了警示性设计——顶部大号警告图标"⚠️",标题"确认下架此团购?“,副标题用橙红色提示"下架后已下单用户不受影响”,缓解用户对下架影响的担忧。中间展示即将下架的商品信息(图标+名称),让用户二次确认操作对象。底部"取消"和"确认下架"按钮,确认按钮使用橙红色突出操作的重要性。

11.5 商品详情弹窗

详情弹窗是信息密度最高的弹窗,展示了商品的完整信息。

@Builder detailModal() {
  Column() {
    this.modalOverlay(() => { this.showDetailModal = false })
    Column() {
      Row() {
        Column() {
          Text(this.selectedProduct?.emoji ?? '🛒').fontSize(40)
        }.width(70).height(70)
        .backgroundColor(PRODUCT_CAT_CONFIG[this.selectedProduct?.category ?? '']?.bg ?? '#FFF3E0')
        .borderRadius(16)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
        Column() {
          Text(this.selectedProduct?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
            .fontColor('#3E2723')
          Row() {
            Text(GROUP_STATUS_CONFIG[this.selectedProduct?.status ?? '进行中']?.icon ?? '🔥')
              .fontSize(12)
            Text(GROUP_STATUS_CONFIG[this.selectedProduct?.status ?? '进行中']?.label ?? '进行中')
              .fontSize(11)
              .fontColor(GROUP_STATUS_CONFIG[this.selectedProduct?.status ?? '进行中']?.color ?? '#E64A19')
              .margin({ left: 4 })
            if (this.selectedProduct?.isHot ?? false) {
              Text('🔥 热销').fontSize(10).fontColor('#FFFFFF')
                .backgroundColor('#E64A19')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .borderRadius(8).margin({ left: 6 })
            }
          }
          .margin({ top: 4 })
        }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })

详情弹窗头部左侧是一个 70x70 的圆角方块,背景色取自商品分类配置,中间放大显示商品 emoji。右侧是商品名称和状态标签——状态图标、状态文字、热销角标横向排列。这里大量使用了 ?? '默认值' 的空值合并运算符,确保即使 selectedProduct 为 null 也不会崩溃。

        Scroll() {
          Column() {
            Row() {
              Column() {
                Text('💰 团购价').fontSize(10).fontColor('#795548')
                Text('¥' + (this.selectedProduct?.groupPrice ?? 0).toString())
                  .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E64A19').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('📦 原价').fontSize(10).fontColor('#795548')
                Text('¥' + (this.selectedProduct?.originalPrice ?? 0).toString())
                  .fontSize(16).fontColor('#BCAAA4')
                  .decoration({ type: TextDecorationType.LineThrough })
                  .margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('👥 成团').fontSize(10).fontColor('#795548')
                Text((this.selectedProduct?.currentCount ?? 0).toString() + '/' + (this.selectedProduct?.minGroupSize ?? 0).toString())
                  .fontSize(14).fontColor('#43A047').fontWeight(FontWeight.Bold).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            }
            .width('100%').margin({ top: 12 })

详情主体分三行展示数据。第一行展示团购价、原价(带删除线 TextDecorationType.LineThrough)和成团进度(如"18/20"),三个指标用 layoutWeight(1) 三等分。团购价用 20 号字加粗橙红色突出,原价用 16 号字浅灰带删除线形成对比,成团进度用鲜绿色表示积极状态。

第二行展示已售、库存、评分,第三行展示产地和配送。标签部分通过 if (this.selectedProduct != null && this.selectedProduct.tags.length > 0) 判断有标签时才渲染,每个标签以"#标签名"的形式展示,浅米底橙红字。

弹窗底部是"编辑"和"下架"两个操作按钮,分别使用暖黄和橙红背景,点击分别打开编辑弹窗和下架确认弹窗,形成了弹窗间的跳转链路。

11.6 商品卡片构造器

商品卡片是网格列表中的基本单元,需要在有限空间内展示丰富的商品信息。

@Builder productCard(p: GroupProductItem) {
  Column() {
    Row() {
      Text(p.emoji).fontSize(36)
    }
    .width('100%').height(80)
    .backgroundColor(PRODUCT_CAT_CONFIG[p.category]?.bg ?? '#FFF3E0')
    .borderRadius({ topLeft: 12, topRight: 12 })
    .justifyContent(FlexAlign.Center)

卡片顶部是一个 80 高的色块区域,背景色取自商品分类配置,居中显示 36 号的商品 emoji。borderRadius 只设置上方两个圆角,与卡片整体的 12 号圆角衔接。

    Row() {
      if (p.isHot) {
        Text('🔥 热销').fontSize(9).fontColor('#FFFFFF')
          .backgroundColor('#E64A19')
          .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
      }
      if (p.isNew) {
        Text('🆕 新品').fontSize(9).fontColor('#FFFFFF')
          .backgroundColor('#43A047')
          .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
          .margin({ left: p.isHot ? 4 : 0 })
      }
      Row().layoutWeight(1)
      Text(GROUP_STATUS_CONFIG[p.status]?.icon ?? '🔥').fontSize(12)
    }
    .width('100%').padding({ left: 8, right: 8, top: 4 })

色块下方是标签行,左侧根据 isHotisNew 条件渲染热销和新品角标。如果两个都为真,新品标签通过 margin({ left: p.isHot ? 4 : 0 }) 在热销标签右侧留出间距。右侧用 Row().layoutWeight(1) 占位,最右显示状态图标。

    Column() {
      Text(p.name).fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Medium)
        .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
        .width('100%')
      Row() {
        Text('¥' + p.groupPrice.toString()).fontSize(16).fontColor('#E64A19')
          .fontWeight(FontWeight.Bold)
        Text('¥' + p.originalPrice.toString()).fontSize(11).fontColor('#BCAAA4')
          .decoration({ type: TextDecorationType.LineThrough })
          .margin({ left: 4 })
      }
      .margin({ top: 4 })
      Row() {
        Text((p.currentCount.toString()) + '/' + (p.minGroupSize.toString()))
          .fontSize(10).fontColor('#43A047')
        Text('已售' + (p.sold.toString())).fontSize(10).fontColor('#795548')
          .margin({ left: 6 })
      }
      .margin({ top: 4 })
      Row() {
        Text(p.deliveryTip).fontSize(9).fontColor('#E64A19')
          .backgroundColor('#FFF3E0')
          .padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
      }
      .margin({ top: 4 })
    }
    .width('100%').padding({ left: 8, right: 8, top: 4, bottom: 8 })
    .alignItems(HorizontalAlign.Start)
  }
  .width('48%')
  .backgroundColor('#FFFFFF').borderRadius(12)
  .margin({ left: 4, right: 4, top: 4, bottom: 4 })
  .shadow({ radius: 4, color: '#1AE64A1910', offsetY: 2 })
  .onClick(() => { this.selectedProduct = p; this.showDetailModal = true })
}

卡片内容区从上到下依次是:商品名称(最多两行,超出省略号)、价格行(团购价大字橙红 + 原价小字带删除线)、进度行(成团进度 + 已售件数)、配送提示标签。maxLines(2)textOverflow({ overflow: TextOverflow.Ellipsis }) 确保长名称不会撑破卡片。

卡片整体宽度 48%(两列布局每列约占一半),白色背景,12 号圆角,四周 4 的外边距,配合 shadow 添加柔和投影。onClick 将点击的商品赋值给 selectedProduct 并打开详情弹窗,实现了"点击查看详情"的交互闭环。

11.7 主构建方法

build() {
  Stack() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('🏠 阳光社区').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            Text('今日 ' + getActiveGroups().toString() + ' 个团进行中')
              .fontSize(11).fontColor('#FFCC80')
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text('🔔').fontSize(18).fontColor('#FFFFFF').margin({ right: 12 })
          Text('📍').fontSize(18).fontColor('#FFFFFF')
        }
        .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })

页面顶部是渐变头部。第一行左侧显示社区名称"🏠 阳光社区"和今日进行中的团购数量,右侧是通知和定位图标。getActiveGroups() 的返回值动态拼接到文本中,让用户一眼了解当前活跃团购数。

        Row() {
          Column() {
            Text(getTotalProducts().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('在售商品').fontSize(9).fontColor('#FFCC80')
          }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          Column() {
            Text(getHotProducts().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFE082')
            Text('热销商品').fontSize(9).fontColor('#FFCC80')
          }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          Column() {
            Text('¥' + getTotalSales().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('累计消费').fontSize(9).fontColor('#FFCC80')
          }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          Column() {
            Text(getPendingPickup().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#A5D6A7')
            Text('待提货').fontSize(9).fontColor('#FFCC80')
          }.layoutWeight(1).alignItems(HorizontalAlign.Center)
        }
        .width('100%').padding({ top: 8, bottom: 12 })
      }
      .width('100%')
      .linearGradient({
        angle: 135,
        colors: [['#E64A19', 0.0], ['#BF360C', 1.0]]
      })

统计条是头部的第二部分,四列均分展示在售商品、热销商品、累计消费、待提货四个指标。数字用 18 号加粗白字,标签用 9 号浅橙字。热销数字用 #FFE082 暖黄突出,待提货数字用 #A5D6A7 浅绿表示待处理。整个头部使用 linearGradient 设置 135 度线性渐变,从 #E64A19#BF360C,营造出深邃的橙红渐变效果。

      Scroll() {
        Row() {
          ForEach(CAT_FILTERS, (cat: string) => {
            if (this.selectedCat === cat) {
              Text(cat === '全部' ? '📋 全部' : ((PRODUCT_CAT_CONFIG[cat]?.icon ?? '') + ' ' + cat))
                .fontSize(11).fontColor('#FFFFFF').backgroundColor('#E64A19')
                .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(14)
                .margin({ left: 3, right: 3 })
            } else {
              Text(cat === '全部' ? '📋 全部' : ((PRODUCT_CAT_CONFIG[cat]?.icon ?? '') + ' ' + cat))
                .fontSize(11).fontColor('#E64A19').backgroundColor('#FFF3E0')
                .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(14)
                .margin({ left: 3, right: 3 })
                .onClick(() => { this.selectedCat = cat })
            }
          })
        }
        .padding({ left: 8, right: 8 })
      }
      .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(40)

分类筛选条是一个横向 Scrollscrollable(ScrollDirection.Horizontal) 允许左右滑动,scrollBar(BarState.Off) 隐藏滚动条保持整洁。ForEach 遍历所有分类选项,“全部"特殊处理为"📋 全部”,其他拼上分类图标。选中态白字橙红底,未选中态橙红字浅米底,点击切换。

排序栏逻辑类似,遍历 SORT_OPTIONS 生成标签,选中态用暖黄底白字以区别于分类筛选的橙红。最右侧是一个橙红色圆形"➕"按钮,点击打开发布团购弹窗。

商品列表使用双 Column 实现 Grid 效果——两个等宽 Column(各 layoutWeight(1))纵向排列卡片,第一个 Column 放前 12 个商品,第二个放后 12 个,形成视觉上的双列瀑布流。外层 Scroll 支持上下滚动。整个页面用 Stack 包裹,将四个弹窗叠加在内容上方,通过 if 条件渲染控制显隐。

十二、我的订单页:订单管理与详情查看

我的订单页展示用户的全部订单,支持按状态筛选和查看订单详情。

12.1 状态声明与订单卡片

@State selectedStatus: string = '全部'
@State showDetailModal: boolean = false
@State selectedOrder: OrderItem | null = null

订单页只需三个状态:当前筛选的状态、详情弹窗显隐、当前查看的订单。

@Builder orderCard(o: OrderItem) {
  Column() {
    Row() {
      Text(o.productEmoji).fontSize(32)
      Column() {
        Text(o.productName).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#3E2723')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text('x' + o.quantity.toString() + ' · ¥' + o.unitPrice.toString() + '/件')
          .fontSize(11).fontColor('#795548').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
      Column() {
        Text('¥' + o.totalPrice.toString())
          .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
        Row() {
          Text(ORDER_STATUS_CONFIG[o.status]?.icon ?? '⏳')
            .fontSize(10)
          Text(ORDER_STATUS_CONFIG[o.status]?.label ?? '待付款')
            .fontSize(10)
            .fontColor(ORDER_STATUS_CONFIG[o.status]?.color ?? '#E64A19')
            .margin({ left: 2 })
        }
        .margin({ top: 2 })
      }.alignItems(HorizontalAlign.End)
    }
    .width('100%')
    Row() {
      Text('📅 ' + o.orderDate).fontSize(10).fontColor('#BCAAA4')
      Text('📍 ' + o.deliverySlot).fontSize(10).fontColor('#E64A19')
        .margin({ left: 8 })
      Text('🏪 ' + o.pickupPoint).fontSize(10).fontColor('#795548')
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
        .layoutWeight(1).margin({ left: 8 })
    }
    .width('100%').margin({ top: 6 })
  }
  .width('100%').backgroundColor('#FFFFFF').borderRadius(10)
  .padding({ left: 12, right: 12, top: 10, bottom: 10 })
  .margin({ left: 12, right: 12, top: 4, bottom: 4 })
  .shadow({ radius: 3, color: '#1AE64A1908', offsetY: 1 })
  .onClick(() => { this.selectedOrder = o; this.showDetailModal = true })
}

订单卡片比商品卡片更紧凑。主行左侧是商品图标,中间是商品名称(单行省略)和数量单价信息,右侧是总价(大字橙红)和状态标签(图标+文字)。底部信息行展示下单日期、配送时段和自提点,自提点用 layoutWeight(1) 占满剩余空间并单行省略。卡片整体白底圆角,配合轻投影,点击打开订单详情弹窗。

12.2 订单详情弹窗

订单详情弹窗展示了订单的完整信息,采用信息分组的方式组织。

@Builder orderDetailModal() {
  Column() {
    this.modalOverlay(() => { this.showDetailModal = false })
    Column() {
      Row() {
        Text('📋 订单详情').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Row().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#999999')
          .onClick(() => { this.showDetailModal = false })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
      Divider().color('#FFF3E0')
      Column() {
        Row() {
          Text(this.selectedOrder?.productEmoji ?? '🛒').fontSize(48)
          Column() {
            Text(this.selectedOrder?.productName ?? '').fontSize(15).fontWeight(FontWeight.Bold)
              .fontColor('#3E2723')
            Text('x' + (this.selectedOrder?.quantity ?? 0).toString())
              .fontSize(13).fontColor('#795548').margin({ top: 2 })
          }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
          Text('¥' + (this.selectedOrder?.totalPrice ?? 0).toString())
            .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E64A19')
        }
        .width('100%').padding({ left: 20, right: 20, top: 16 })

弹窗顶部标题行后是商品信息行——48 号大图标、商品名称、数量、总价。随后是多组信息行,每组用 Row 包含两个 Column(各 layoutWeight(1)),分别展示订单编号与下单日期、配送方式与自提点、收货人与联系电话。

          Text('#' + (this.selectedOrder?.id ?? 0).toString().padStart(6, '0'))

订单编号通过 padStart(6, '0') 补零到 6 位,如 ID 为 1 显示为"#000001",更符合订单编号的视觉习惯。备注区块通过 if (this.selectedOrder != null && this.selectedOrder.remark !== '') 条件渲染,只有有备注时才显示。

弹窗底部居中显示一个状态胶囊标签,背景色和文字色都来自 ORDER_STATUS_CONFIG,用图标+文字的形式明确告知当前订单状态。

12.3 订单页主构建

build() {
  Stack() {
    Column() {
      Text('📦 我的订单').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      Row() {
        ForEach(['全部', '待付款', '已付款', '配送中', '已签收', '已退款'], (s: string) => {
          if (this.selectedStatus === s) {
            Text(s).fontSize(11).fontColor('#FFFFFF').backgroundColor('#E64A19')
              .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
              .margin({ left: 3, right: 3 })
          } else {
            Text(s).fontSize(11).fontColor('#795548').backgroundColor('#FFF3E0')
              .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
              .margin({ left: 3, right: 3 })
              .onClick(() => { this.selectedStatus = s })
          }
        })
      }
      .width('100%').padding({ left: 12, bottom: 8 })
      Scroll() {
        Column() {
          this.orderCard(mockOrders[0])
          this.orderCard(mockOrders[1])
          // ... 共15条订单卡片
        }
        .padding({ bottom: 12 })
      }
      .layoutWeight(1)
    }
    if (this.showDetailModal) { this.orderDetailModal() }
  }
  .width('100%').height('100%').backgroundColor('#FFF8E1')
}

订单页顶部是标题"📦 我的订单",下方是状态筛选标签条——包含"全部"和五种状态的六个选项,选中态白字橙红底,点击切换。下方是可滚动的订单列表,15 条订单卡片依次排列。整个页面用 Stack 包裹,详情弹窗叠加在上层。

十三、分类浏览页:品类入口与概览

分类浏览页以列表形式展示六大品类,每个品类卡片展示该品类的概览数据。

@Component
struct CategoryBrowseContent {
  build() {
    Column() {
      Text('🗂️ 商品分类').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      Scroll() {
        Column() {
          ForEach(['生鲜蔬果', '肉禽蛋品', '米面粮油', '乳品饮料', '零食糕点', '日用百货'], (cat: string) => {
            Column() {
              Row() {
                Text(PRODUCT_CAT_CONFIG[cat]?.icon ?? '📂').fontSize(32)
                Text(PRODUCT_CAT_CONFIG[cat]?.label ?? cat)
                  .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3E2723')
                  .margin({ left: 10 })
                Row().layoutWeight(1)
                Text('>').fontSize(16).fontColor('#BCAAA4')
              }
              .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
              Row() {
                Column() {
                  Text('5').fontSize(18).fontWeight(FontWeight.Bold).fontColor(PRODUCT_CAT_CONFIG[cat]?.color)
                  Text('在售').fontSize(9).fontColor('#795548')
                }.layoutWeight(1).alignItems(HorizontalAlign.Center)
                Column() {
                  Text('128').fontSize(18).fontWeight(FontWeight.Bold).fontColor(PRODUCT_CAT_CONFIG[cat]?.color)
                  Text('已售').fontSize(9).fontColor('#795548')
                }.layoutWeight(1).alignItems(HorizontalAlign.Center)
                Column() {
                  Text('4.7').fontSize(18).fontWeight(FontWeight.Bold).fontColor(PRODUCT_CAT_CONFIG[cat]?.color)
                  Text('评分').fontSize(9).fontColor('#795548')
                }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              }
              .width('100%').padding({ top: 8, bottom: 8 })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 6, bottom: 6 })
            .shadow({ radius: 3, color: '#1AE64A1908', offsetY: 1 })
          })
        }
        .padding({ bottom: 12 })
      }
      .layoutWeight(1)
    }
    .width('100%').height('100%').backgroundColor('#FFF8E1')
  }
}

这个页面结构相对简洁。顶部标题后是可滚动的分类列表,ForEach 遍历六大品类,每个品类生成一个白色圆角卡片。卡片上半部分是品类图标、名称和右箭头">",下半部分是三列数据——在售数、已售数、评分,数字使用该品类的主题色,与品类形成视觉关联。虽然这里的数据是静态的(“5”、“128”、“4.7”),但在真实场景中这些数据应该来自后端聚合查询。

十四、数据统计页:可视化数据呈现

统计页是应用的数据可视化中心,包含 KPI 四宫格、周消费柱状图和配送时段偏好三个模块。

14.1 KPI 四宫格

Row() {
  Column() {
    Text(getTotalProducts().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#E64A19')
    Text('在售商品').fontSize(10).fontColor('#795548')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
  Column() {
    Text(getTotalOrders().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFB300')
    Text('累计订单').fontSize(10).fontColor('#795548')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 14, bottom: 14 }).margin({ left: 12, right: 12 })
Row() {
  Column() {
    Text('¥' + getTotalSales().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#43A047')
    Text('累计消费').fontSize(10).fontColor('#795548')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
  Column() {
    Text(getPendingPickup().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1565C0')
    Text('待提货').fontSize(10).fontColor('#795548')
  }.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 14, bottom: 14 }).margin({ left: 12, right: 12, top: 8 })

四宫格分为两行,每行两个白色卡片。四个 KPI 分别是:在售商品(橙红)、累计订单(暖黄)、累计消费(鲜绿)、待提货(蓝色),每个数字使用不同主题色,既美观又能通过颜色快速区分指标类型。数据全部来自前面定义的统计函数,实现了数据的实时聚合。

14.2 周消费柱状图

Column() {
  Text('📈 本周消费趋势').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
    .margin({ top: 12, left: 16, bottom: 8 })
  Row() {
    ForEach(WEEKLY_BAR, (item: BarChartItem) => {
      Column() {
        Column() {
          Text(item.value.toString()).fontSize(9).fontColor('#FFFFFF')
        }
        .width(20).height(item.value / getMaxBarValue() * 120)
        .backgroundColor(item.color).borderRadius({ topLeft: 4, topRight: 4 })
        .justifyContent(FlexAlign.End)
        Text(item.label).fontSize(9).fontColor('#795548').margin({ top: 4 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Center)
    })
  }
  .width('100%').height(160)
  .padding({ left: 12, right: 12, bottom: 12 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8 })
.layoutWeight(1)

这是整个应用最巧妙的部分——不依赖任何图表库,纯用 ArkTS 原生组件自绘柱状图。ForEach 遍历 WEEKLY_BAR 数组,每根柱子是一个 Column:内部嵌套一个固定宽度 20 的 Column 作为柱体,高度通过 item.value / getMaxBarValue() * 120 计算——用当前值除以最大值得到比例,再乘以基准高度 120,实现归一化。柱体颜色取自数据项的 color 字段,上方圆角,内部居底显示数值文字(白字)。柱子下方是日期标签。

整个图表区域高度 160,Row 让七根柱子通过 layoutWeight(1) 均分宽度。这种自绘方式的优势是完全可控、无额外依赖,劣势是不支持交互(如点击查看详情)和动画。对于简单的趋势展示场景,这种方案足够实用。

14.3 配送时段偏好

Column() {
  Text('🚚 配送时段偏好').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
    .margin({ top: 12, left: 16, bottom: 8 })
  ForEach(DELIVERY_SLOTS, (slot: DeliverySlotMeta) => {
    Row() {
      Text(slot.icon + ' ' + slot.label).fontSize(12).fontColor('#3E2723')
        .layoutWeight(1)
      Text(slot.time).fontSize(12).fontColor('#E64A19')
    }
    .width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
    Divider().color('#FFF3E0')
  })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 8, bottom: 12 })

配送时段偏好以列表形式展示五个配送选项,每行左侧是图标+标签,右侧是具体时间,行间用浅米色分割线分隔。这个模块复用了 DELIVERY_SLOTS 配置数据,与发布团购弹窗中的时段选择保持一致,体现了数据的统一管理。

十五、个人中心页:用户信息与功能入口

个人中心页展示用户信息和功能入口列表。

@Component
struct ProfileContent {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('🧑').fontSize(56)
          Text('张先生').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('138****8888').fontSize(12).fontColor('#FFCC80').margin({ top: 2 })
        }
        .width('100%').padding({ top: 30, bottom: 30 })
        .linearGradient({
          angle: 135,
          colors: [['#E64A19', 0.0], ['#BF360C', 1.0]]
        })
        .alignItems(HorizontalAlign.Center)

页面顶部是用户头部区域,使用与今日团购页相同的橙红渐变背景。居中展示 56 号的用户头像 emoji、用户名"张先生"和脱敏手机号。linearGradient 的参数与头部一致,保证视觉统一。

        Row() {
          Column() {
            Text('🏅 团长会员').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
            Text('累计消费 ¥' + getTotalSales().toString() + ' · 省了 ¥' + (getTotalSales() * 15 / 100).toString().split('.')[0])
              .fontSize(11).fontColor('#795548').margin({ top: 2 })
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text('查看权益 >').fontSize(11).fontColor('#E64A19')
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .margin({ left: 12, right: 12, top: -16 })
        .shadow({ radius: 6, color: '#1A000000', offsetY: 2 })

会员卡是一个白色卡片,通过 margin({ top: -16 }) 向上偏移,与渐变头部产生重叠效果,营造出卡片"浮"在头部下方的层次感。卡片左侧展示会员等级和消费统计——累计消费金额来自 getTotalSales(),省钱金额按消费的 15% 估算,用 split('.')[0] 取整。右侧是"查看权益 >"入口。

        Column() {
          Row() {
            Text('📍').fontSize(22)
            Text('收货地址管理').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('3个地址').fontSize(12).fontColor('#BCAAA4')
            Text('>').fontSize(16).fontColor('#BCAAA4').margin({ left: 8 })
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
          Divider().color('#FFF3E0')
          // ... 更多功能项
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .margin({ left: 12, right: 12, top: 12, bottom: 12 })

功能列表包含六个入口项:收货地址管理、自提点管理、支付方式、我的优惠券、联系客服、设置。每项左侧是 emoji 图标,中间是功能名称(layoutWeight(1) 占满空间),右侧是辅助信息(如"3个地址"、“5张可用”)和箭头">"。行间用 Divider 分隔。优惠券的辅助信息"5张可用"使用橙红色突出,暗示有待使用的优惠,吸引用户点击。

整个页面用 Scroll 包裹,确保内容超出屏幕时可滚动查看。layoutWeight(1)Scroll 占满剩余空间。

十六、关键特性对比总结

下面通过表格对系统的五大功能模块进行横向对比,帮助快速理解各模块的定位与特点。

功能模块核心职责主要状态变量交互弹窗数数据来源视觉特色
今日团购商品展示与团购管理15 个(筛选、弹窗、表单)4 个(发布、编辑、下架、详情)mockProducts渐变头部 + Grid 双列卡片
我的订单订单查看与筛选3 个(筛选、弹窗、选中订单)1 个(订单详情)mockOrders状态标签筛选 + 紧凑卡片
分类浏览品类入口与概览0 个0 个PRODUCT_CAT_CONFIG品类主题色 + 数据三栏
数据统计KPI 与趋势可视化0 个0 个统计函数 + WEEKLY_BAR四宫格 KPI + 自绘柱状图
个人中心用户信息与功能入口0 个0 个统计函数 + 静态数据渐变头部 + 会员卡悬浮

从表格可以看出,今日团购页是功能最复杂的模块,承载了全部的 CRUD 交互;数据统计页虽然无状态,但通过自绘柱状图实现了数据可视化;分类浏览和个人中心则更多承担信息展示的职责。这种职责划分让每个模块都保持单一关注点,避免了"上帝组件"的反模式。

十七、设计模式与技术亮点总结

回顾整个系统的实现,我们可以总结出以下几个值得关注的技术亮点与设计模式。

第一,配置驱动的 UI 渲染。 系统将团购状态、商品分类、订单状态、配送时段等业务配置全部抽离为独立的映射表或数组,UI 渲染时通过键查找获取完整的视觉信息(图标、颜色、背景色)。这种模式的好处是显而易见的——新增一个状态只需在配置表中添加一条记录,无需修改任何渲染代码;修改某个状态的颜色只需改一处配置,所有引用处自动生效。这是"数据驱动 UI"思想的典型实践。

第二,响应式状态管理。 通过 @State@Observed@Component 等 ArkTS 装饰器,系统实现了完整的响应式数据流。状态变化自动触发 UI 刷新,开发者无需手动操作 DOM 或调用 setState@Observed 标记的数据类在属性变化时能通知所有依赖它的组件,实现了细粒度的更新。@Builder 方法将可复用的 UI 片段封装为参数化构造器,既减少了代码重复,又保持了类型安全。

第三,防御性编程。 代码中大量使用了空值合并运算符 ??,如 this.selectedProduct?.name ?? ''GROUP_STATUS_CONFIG[p.status]?.icon ?? '🔥',确保即使数据为空也不会导致渲染崩溃。@State 变量都初始化了默认值,tags 数组默认为空而非 null。这些细节体现了对运行时稳定性的重视。

第四,自绘图表能力。 统计页的柱状图完全使用 ArkTS 原生组件(ColumnRowText)自绘,通过数学计算 value / max * 基准高度 实现高度归一化,通过数据项携带的 color 字段实现颜色编码。这种方案无需引入第三方图表库,减小了包体积,同时也展示了 ArkTS 灵活的布局能力。

第五,统一的视觉语言。 整个应用遵循一套完整的调色板,从主色橙红到文字棕色系,从背景米白到边框浅米,所有颜色都经过精心搭配。渐变头部、圆角卡片、柔和投影、emoji 图标等视觉元素在各页面间保持一致,营造出统一的品牌质感。状态、分类等业务概念都有专属的视觉身份(图标+配色),帮助用户快速识别。

第六,组件化与复用。 系统将每个 Tab 页封装为独立的 @Component,将弹窗、卡片、遮罩等可复用 UI 封装为 @Builder 方法。modalOverlay 遮罩在四个弹窗中复用,productCard 在商品列表中复用 24 次,bottomTabItem 在底部导航中复用 5 次。这种组件化策略大幅减少了代码冗余,提高了可维护性。


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 社区团购管理 - Community Group Buying Manager
// 344.ets · HarmonyOS ArkTS
// 配色:活力橙红 #E64A19 + 暖黄 #FFB300 + 鲜绿 #43A047 + 米白 #FFF8E1
// 布局:Grid双列卡片 + 横向分类滚动 + 柱状图统计
// ============================================================

// ==================== 接口类型定义 ====================
interface GroupStatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

interface ProductCatMeta {
  label: string
  icon: string
  color: string
  bg: string
}

interface OrderStatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

interface DeliverySlotMeta {
  label: string
  time: string
  icon: string
}

interface BarChartItem {
  label: string
  value: number
  color: string
}

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

// ==================== 颜色令牌 ====================
const COLORS: ColorPalette = {
  primary: '#E64A19',
  primaryLight: '#FFAB91',
  primaryDark: '#BF360C',
  accent: '#FFB300',
  accentLight: '#FFE082',
  success: '#43A047',
  successLight: '#C8E6C9',
  bg: '#FFF8E1',
  cardBg: '#FFFFFF',
  textPrimary: '#3E2723',
  textSecondary: '#795548',
  textHint: '#BCAAA4',
  border: '#FFF3E0',
  white: '#FFFFFF'
}

// ==================== 配置映射 ====================
const GROUP_STATUS_CONFIG: Record<string, GroupStatusMeta> = {
  '进行中': { label: '进行中', color: '#E64A19', bg: '#FFF3E0', icon: '🔥' },
  '已成团': { label: '已成团', color: '#43A047', bg: '#E8F5E9', icon: '✅' },
  '待发货': { label: '待发货', color: '#FFB300', bg: '#FFF8E1', icon: '📦' },
  '已送达': { label: '已送达', color: '#1565C0', bg: '#E3F2FD', icon: '📮' },
  '已取消': { label: '已取消', color: '#9E9E9E', bg: '#F5F5F5', icon: '❌' }
}

const PRODUCT_CAT_CONFIG: Record<string, ProductCatMeta> = {
  '生鲜蔬果': { label: '生鲜蔬果', icon: '🥬', color: '#43A047', bg: '#E8F5E9' },
  '肉禽蛋品': { label: '肉禽蛋品', icon: '🥩', color: '#E64A19', bg: '#FFEBEE' },
  '米面粮油': { label: '米面粮油', icon: '🍚', color: '#FFB300', bg: '#FFF8E1' },
  '乳品饮料': { label: '乳品饮料', icon: '🥛', color: '#1565C0', bg: '#E3F2FD' },
  '零食糕点': { label: '零食糕点', icon: '🍪', color: '#7B1FA2', bg: '#F3E5F5' },
  '日用百货': { label: '日用百货', icon: '🧴', color: '#00838F', bg: '#E0F7FA' }
}

const ORDER_STATUS_CONFIG: Record<string, OrderStatusMeta> = {
  '待付款': { label: '待付款', color: '#E64A19', bg: '#FFF3E0', icon: '⏳' },
  '已付款': { label: '已付款', color: '#FFB300', bg: '#FFF8E1', icon: '💰' },
  '配送中': { label: '配送中', color: '#1565C0', bg: '#E3F2FD', icon: '🚚' },
  '已签收': { label: '已签收', color: '#43A047', bg: '#E8F5E9', icon: '✅' },
  '已退款': { label: '已退款', color: '#9E9E9E', bg: '#F5F5F5', icon: '↩️' }
}

const DELIVERY_SLOTS: DeliverySlotMeta[] = [
  { label: '今日16点自提', time: '16:00', icon: '🏪' },
  { label: '今日18点自提', time: '18:00', icon: '🏪' },
  { label: '明日10点自提', time: '10:00', icon: '🏪' },
  { label: '明日16点自提', time: '16:00', icon: '🏪' },
  { label: '送货上门', time: '次日达', icon: '🚚' }
]

const CAT_FILTERS: string[] = ['全部', '生鲜蔬果', '肉禽蛋品', '米面粮油', '乳品饮料', '零食糕点', '日用百货']
const SORT_OPTIONS: string[] = ['人气优先', '价格从低到高', '价格从高到低', '成团速度']

const WEEKLY_BAR: BarChartItem[] = [
  { label: '周一', value: 320, color: '#FFAB91' },
  { label: '周二', value: 450, color: '#FFAB91' },
  { label: '周三', value: 580, color: '#FF8A65' },
  { label: '周四', value: 420, color: '#FFAB91' },
  { label: '周五', value: 720, color: '#E64A19' },
  { label: '周六', value: 890, color: '#BF360C' },
  { label: '周日', value: 660, color: '#FF8A65' }
]

// ==================== 团购商品数据模型 ====================
@Observed
export class GroupProductItem {
  id: number = 0
  name: string = ''
  category: string = ''
  emoji: string = '🛒'
  price: number = 0
  originalPrice: number = 0
  unit: string = ''
  groupPrice: number = 0
  minGroupSize: number = 0
  currentCount: number = 0
  status: string = '进行中'
  sold: number = 0
  stock: number = 0
  rating: number = 0
  tags: string[] = []
  origin: string = ''
  deliveryTip: string = ''
  isHot: boolean = false
  isNew: boolean = false
  constructor(id: number, name: string, category: string, emoji: string, price: number, originalPrice: number,
    unit: string, groupPrice: number, minGroupSize: number, currentCount: number, status: string,
    sold: number, stock: number, rating: number, tags: string[], origin: string,
    deliveryTip: string, isHot: boolean, isNew: boolean) {
    this.id = id; this.name = name; this.category = category; this.emoji = emoji
    this.price = price; this.originalPrice = originalPrice; this.unit = unit
    this.groupPrice = groupPrice; this.minGroupSize = minGroupSize; this.currentCount = currentCount
    this.status = status; this.sold = sold; this.stock = stock; this.rating = rating
    this.tags = tags; this.origin = origin; this.deliveryTip = deliveryTip
    this.isHot = isHot; this.isNew = isNew
  }
}

// ==================== 订单数据模型 ====================
@Observed
export class OrderItem {
  id: number = 0
  productName: string = ''
  productEmoji: string = '🛒'
  quantity: number = 0
  unitPrice: number = 0
  totalPrice: number = 0
  status: string = '待付款'
  orderDate: string = ''
  deliverySlot: string = ''
  pickupPoint: string = ''
  receiver: string = ''
  phone: string = ''
  remark: string = ''
  constructor(id: number, productName: string, productEmoji: string, quantity: number,
    unitPrice: number, totalPrice: number, status: string, orderDate: string,
    deliverySlot: string, pickupPoint: string, receiver: string, phone: string, remark: string) {
    this.id = id; this.productName = productName; this.productEmoji = productEmoji
    this.quantity = quantity; this.unitPrice = unitPrice; this.totalPrice = totalPrice
    this.status = status; this.orderDate = orderDate; this.deliverySlot = deliverySlot
    this.pickupPoint = pickupPoint; this.receiver = receiver; this.phone = phone; this.remark = remark
  }
}

// ==================== 模拟数据 ====================
const mockProducts: GroupProductItem[] = [
  new GroupProductItem(1, '山东烟台红富士苹果', '生鲜蔬果', '🍎', 29.9, 49.9, '5斤装', 25.9, 20, 18, '进行中', 320, 500, 4.8, ['产地直发', '坏果包赔'], '山东烟台', '今日16点自提', true, false),
  new GroupProductItem(2, '内蒙古草原牛腱子肉', '肉禽蛋品', '🥩', 68.8, 98.0, '1kg', 59.9, 15, 12, '进行中', 210, 300, 4.9, ['草饲牛', '冷链配送'], '内蒙古锡林郭勒', '明日10点自提', true, false),
  new GroupProductItem(3, '东北五常稻花香大米', '米面粮油', '🍚', 45.0, 65.0, '10斤装', 39.9, 30, 28, '进行中', 450, 600, 4.7, ['新米上市', '真空包装'], '黑龙江五常', '今日18点自提', false, true),
  new GroupProductItem(4, '云南雪山有机蔬菜礼盒', '生鲜蔬果', '🥬', 39.9, 59.9, '6种装', 33.9, 25, 22, '进行中', 180, 400, 4.6, ['有机认证', '当日采摘'], '云南丽江', '明日16点自提', false, true),
  new GroupProductItem(5, '宁夏中宁枸杞', '日用百货', '🔴', 35.0, 58.0, '250g', 29.9, 20, 20, '已成团', 600, 800, 4.9, ['特级', '无硫熏'], '宁夏中宁', '今日16点自提', true, false),
  new GroupProductItem(6, '海南贵妃芒果', '生鲜蔬果', '🥭', 32.0, 52.0, '5斤装', 27.9, 20, 15, '进行中', 150, 350, 4.5, ['树上熟', '甜度18°'], '海南三亚', '明日10点自提', true, false),
  new GroupProductItem(7, '新疆阿克苏冰糖心苹果', '生鲜蔬果', '🍏', 34.9, 55.0, '5斤装', 29.9, 25, 23, '进行中', 280, 500, 4.8, ['冰糖心', '日照充足'], '新疆阿克苏', '今日18点自提', false, false),
  new GroupProductItem(8, '内蒙古散养土鸡蛋', '肉禽蛋品', '🥚', 28.0, 45.0, '30枚装', 23.9, 30, 30, '已成团', 720, 1000, 4.9, ['散养土鸡', '新鲜直发'], '内蒙古赤峰', '明日10点自提', true, false),
  new GroupProductItem(9, '陕西徐香猕猴桃', '生鲜蔬果', '🥝', 25.9, 42.0, '3斤装', 21.9, 20, 10, '进行中', 90, 300, 4.6, ['软糯香甜', '维C之王'], '陕西周至', '今日16点自提', false, true),
  new GroupProductItem(10, '黑龙江非转基因大豆油', '米面粮油', '🫒', 55.0, 78.0, '5L装', 46.9, 20, 18, '进行中', 200, 350, 4.7, ['非转基因', '压榨工艺'], '黑龙江佳木斯', '明日16点自提', false, false),
  new GroupProductItem(11, '青海高原纯牛奶', '乳品饮料', '🥛', 59.0, 89.0, '12盒装', 49.9, 25, 25, '已成团', 580, 800, 4.8, ['高原牧场', '零添加'], '青海西宁', '今日18点自提', true, false),
  new GroupProductItem(12, '湖北洪湖莲藕', '生鲜蔬果', '🪷', 22.0, 35.0, '5斤装', 18.9, 15, 8, '进行中', 60, 200, 4.5, ['湖藕', '粉糯'], '湖北洪湖', '明日10点自提', false, false),
  new GroupProductItem(13, '云南鲜花饼礼盒', '零食糕点', '🌹', 38.0, 58.0, '20枚装', 32.9, 20, 20, '已成团', 450, 600, 4.7, ['现烤直发', '玫瑰花瓣'], '云南昆明', '今日16点自提', false, true),
  new GroupProductItem(14, '山东大葱', '生鲜蔬果', '🧅', 18.0, 28.0, '5斤装', 14.9, 20, 5, '进行中', 40, 150, 4.3, ['章丘大葱', '葱白长'], '山东济南', '明日10点自提', false, false),
  new GroupProductItem(15, '福建平和琯溪蜜柚', '生鲜蔬果', '🍊', 26.0, 42.0, '2个装', 21.9, 20, 16, '进行中', 170, 400, 4.6, ['红肉蜜柚', '清甜多汁'], '福建平和', '今日18点自提', true, false),
  new GroupProductItem(16, '四川不知火丑橘', '生鲜蔬果', '🍊', 32.0, 50.0, '5斤装', 27.9, 20, 19, '进行中', 230, 400, 4.7, ['皮薄肉厚', '清甜不上火'], '四川蒲江', '明日16点自提', false, true),
  new GroupProductItem(17, '广西百香果', '生鲜蔬果', '🟣', 25.0, 40.0, '3斤装', 20.9, 15, 12, '进行中', 140, 300, 4.5, ['钦蜜9号', '纯甜不酸'], '广西钦州', '今日16点自提', false, false),
  new GroupProductItem(18, '浙江舟山小黄鱼', '肉禽蛋品', '🐟', 42.0, 68.0, '1kg装', 35.9, 20, 8, '进行中', 85, 200, 4.6, ['东海捕捞', '冷链直发'], '浙江舟山', '明日10点自提', true, false),
  new GroupProductItem(19, '贵州老干妈辣椒酱组合', '日用百货', '🌶️', 33.0, 50.0, '3瓶装', 27.9, 20, 20, '已成团', 380, 500, 4.8, ['经典组合', '百搭调味'], '贵州贵阳', '今日18点自提', false, false),
  new GroupProductItem(20, '广东荔枝干', '零食糕点', '🍬', 29.0, 45.0, '500g装', 24.9, 15, 15, '已成团', 300, 400, 4.6, ['岭南特产', '肉厚核小'], '广东茂名', '明日16点自提', false, false),
  new GroupProductItem(21, '山东章丘铁锅', '日用百货', '🍳', 158.0, 298.0, '32cm', 128.0, 10, 6, '进行中', 55, 100, 4.9, ['手工锻造', '无涂层'], '山东章丘', '送货上门', true, false),
  new GroupProductItem(22, '湖北恩施富硒土豆', '生鲜蔬果', '🥔', 19.9, 32.0, '5斤装', 16.9, 20, 14, '进行中', 120, 300, 4.4, ['富硒土壤', '软糯香甜'], '湖北恩施', '今日16点自提', false, true),
  new GroupProductItem(23, '云南普洱熟茶饼', '乳品饮料', '🍵', 68.0, 128.0, '357g', 55.9, 15, 10, '进行中', 95, 200, 4.8, ['古树料', '醇厚回甘'], '云南普洱', '送货上门', false, false),
  new GroupProductItem(24, '河南铁棍山药粉', '米面粮油', '🥣', 45.0, 75.0, '500g装', 38.9, 20, 12, '进行中', 160, 300, 4.6, ['温县正宗', '破壁技术'], '河南温县', '明日10点自提', false, false)
]

const mockOrders: OrderItem[] = [
  new OrderItem(1, '山东烟台红富士苹果', '🍎', 2, 25.9, 51.8, '已签收', '07-15', '今日16点自提', '阳光小区自提点', '张先生', '138****8888', '请选大果'),
  new OrderItem(2, '内蒙古草原牛腱子肉', '🥩', 1, 59.9, 59.9, '已签收', '07-14', '今日18点自提', '阳光小区自提点', '张先生', '138****8888', ''),
  new OrderItem(3, '东北五常稻花香大米', '🍚', 3, 39.9, 119.7, '配送中', '07-16', '送货上门', '阳光小区3栋502', '张先生', '138****8888', '放门口即可'),
  new OrderItem(4, '青海高原纯牛奶', '🥛', 2, 49.9, 99.8, '已付款', '07-16', '今日18点自提', '社区团购驿站', '李女士', '139****6666', ''),
  new OrderItem(5, '云南鲜花饼礼盒', '🌹', 1, 32.9, 32.9, '已签收', '07-13', '今日16点自提', '阳光小区自提点', '张先生', '138****8888', '送人的包装好点'),
  new OrderItem(6, '宁夏中宁枸杞', '🔴', 2, 29.9, 59.8, '已签收', '07-12', '今日16点自提', '社区团购驿站', '李女士', '139****6666', ''),
  new OrderItem(7, '福建平和琯溪蜜柚', '🍊', 2, 21.9, 43.8, '配送中', '07-16', '明日16点自提', '阳光小区自提点', '张先生', '138****8888', ''),
  new OrderItem(8, '山东章丘铁锅', '🍳', 1, 128.0, 128.0, '待付款', '07-16', '送货上门', '阳光小区3栋502', '张先生', '138****8888', '尽快发货'),
  new OrderItem(9, '内蒙古散养土鸡蛋', '🥚', 2, 23.9, 47.8, '已签收', '07-11', '今日18点自提', '社区团购驿站', '李女士', '139****6666', ''),
  new OrderItem(10, '四川不知火丑橘', '🍊', 1, 27.9, 27.9, '已退款', '07-10', '今日16点自提', '阳光小区自提点', '张先生', '138****8888', '品质问题退款'),
  new OrderItem(11, '贵州老干妈辣椒酱组合', '🌶️', 1, 27.9, 27.9, '已签收', '07-09', '今日18点自提', '社区团购驿站', '李女士', '139****6666', ''),
  new OrderItem(12, '浙江舟山小黄鱼', '🐟', 2, 35.9, 71.8, '已付款', '07-16', '明日10点自提', '阳光小区自提点', '张先生', '138****8888', ''),
  new OrderItem(13, '河南铁棍山药粉', '🥣', 1, 38.9, 38.9, '配送中', '07-15', '送货上门', '阳光小区3栋502', '张先生', '138****8888', ''),
  new OrderItem(14, '广西百香果', '🟣', 2, 20.9, 41.8, '已签收', '07-08', '今日16点自提', '社区团购驿站', '李女士', '139****6666', ''),
  new OrderItem(15, '云南普洱熟茶饼', '🍵', 1, 55.9, 55.9, '已签收', '07-07', '送货上门', '阳光小区3栋502', '张先生', '138****8888', '包装精美')
]

// ==================== 统计函数 ====================
function getTotalProducts(): number { return mockProducts.length }
function getActiveGroups(): number {
  let count: number = 0
  for (let i = 0; i < mockProducts.length; i++) {
    if (mockProducts[i].status === '进行中') { count++ }
  }
  return count
}
function getTotalOrders(): number { return mockOrders.length }
function getPendingPickup(): number {
  let count: number = 0
  for (let i = 0; i < mockOrders.length; i++) {
    if (mockOrders[i].status === '配送中' || mockOrders[i].status === '已付款') { count++ }
  }
  return count
}
function getTotalSales(): number {
  let total: number = 0
  for (let i = 0; i < mockOrders.length; i++) {
    if (mockOrders[i].status !== '已退款' && mockOrders[i].status !== '待付款') {
      total += mockOrders[i].totalPrice
    }
  }
  return Math.round(total)
}
function getHotProducts(): number {
  let count: number = 0
  for (let i = 0; i < mockProducts.length; i++) {
    if (mockProducts[i].isHot) { count++ }
  }
  return count
}
function getMaxBarValue(): number {
  let max: number = 0
  for (let i = 0; i < WEEKLY_BAR.length; i++) {
    if (WEEKLY_BAR[i].value > max) { max = WEEKLY_BAR[i].value }
  }
  return max
}

// ==================== 底部Tab枚举 ====================
enum GroupBuyTab {
  TODAY = 0,
  ORDERS = 1,
  CATEGORY = 2,
  STATS = 3,
  PROFILE = 4
}

// ==================== 入口页面 ====================
@Entry
@Component
struct GroupBuyApp {
  @State activeTab: GroupBuyTab = GroupBuyTab.TODAY

  @Builder contentArea() {
    Column() {
      if (this.activeTab === GroupBuyTab.TODAY) {
        TodayGroupContent()
      } else if (this.activeTab === GroupBuyTab.ORDERS) {
        MyOrdersContent()
      } else if (this.activeTab === GroupBuyTab.CATEGORY) {
        CategoryBrowseContent()
      } else if (this.activeTab === GroupBuyTab.STATS) {
        StatsContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: GroupBuyTab) {
    Column() {
      Text(icon).fontSize(22).opacity(this.activeTab === tab ? 1.0 : 0.4)
      Text(label).fontSize(10)
        .fontColor(this.activeTab === tab ? '#E64A19' : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 2 })
      if (this.activeTab === tab) {
        Row().width(20).height(3)
          .backgroundColor('#E64A19').borderRadius(2).margin({ top: 3 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 6, bottom: 6 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Column() {
      this.contentArea()
      Row() {
        this.bottomTabItem('🔥', '今日团购', GroupBuyTab.TODAY)
        this.bottomTabItem('📦', '我的订单', GroupBuyTab.ORDERS)
        this.bottomTabItem('🗂️', '分类', GroupBuyTab.CATEGORY)
        this.bottomTabItem('📊', '统计', GroupBuyTab.STATS)
        this.bottomTabItem('👤', '我的', GroupBuyTab.PROFILE)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 8 })
      .shadow({ radius: 10, color: '#1A000000', offsetY: -3 })
    }
    .width('100%').height('100%')
    .backgroundColor('#FFF8E1')
  }
}

// ==================== Tab1:今日团购(Grid双列卡片) ====================
@Component
struct TodayGroupContent {
  @State selectedCat: string = '全部'
  @State selectedSort: string = '人气优先'
  @State showAddModal: boolean = false
  @State showEditModal: boolean = false
  @State showDeleteConfirm: boolean = false
  @State showDetailModal: boolean = false
  @State selectedProduct: GroupProductItem | null = null
  @State editingProduct: GroupProductItem | null = null
  @State formName: string = ''
  @State formCat: string = '生鲜蔬果'
  @State formPrice: string = ''
  @State formGroupPrice: string = ''
  @State formMinSize: string = '20'
  @State formStock: string = ''
  @State formOrigin: string = ''
  @State formDelivery: string = '今日16点自提'
  @State formTags: string = ''

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

  @Builder addProductModal() {
    Column() {
      this.modalOverlay(() => { this.showAddModal = false })
      Column() {
        Row() {
          Text('➕ 发布新团购').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          Row().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showAddModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
        Divider().color('#FFF3E0')
        Scroll() {
          Column() {
            Text('商品名称').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
            TextInput({ placeholder: '输入商品名称...' })
              .placeholderColor('#BCAAA4').fontSize(14).width('100%')
              .backgroundColor('#FFF8E1').borderRadius(8)
              .margin({ left: 20, right: 20, top: 4 })
              .onChange((v: string) => { this.formName = v })
            Text('商品分类').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
            Row() {
              ForEach(CAT_FILTERS.slice(1), (c: string) => {
                if (this.formCat === c) {
                  Text(PRODUCT_CAT_CONFIG[c]?.icon + ' ' + c)
                    .fontSize(11).fontColor('#FFFFFF').backgroundColor('#E64A19')
                    .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                } else {
                  Text(PRODUCT_CAT_CONFIG[c]?.icon + ' ' + c)
                    .fontSize(11).fontColor('#E64A19').backgroundColor('#FFF3E0')
                    .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
                    .margin({ left: 3, right: 3 })
                    .onClick(() => { this.formCat = c })
                }
              })
            }
            .margin({ left: 16, right: 16, top: 4 })
            Text('价格信息').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
            Row() {
              TextInput({ placeholder: '原价 ¥49.9' })
                .placeholderColor('#BCAAA4').fontSize(12).layoutWeight(1)
                .backgroundColor('#FFF8E1').borderRadius(8)
                .onChange((v: string) => { this.formPrice = v })
              TextInput({ placeholder: '团购价 ¥39.9' })
                .placeholderColor('#BCAAA4').fontSize(12).layoutWeight(1)
                .backgroundColor('#FFF8E1').borderRadius(8).margin({ left: 8 })
                .onChange((v: string) => { this.formGroupPrice = v })
            }
            .margin({ left: 20, right: 20, top: 4 })
            Row() {
              TextInput({ placeholder: '成团人数 20' })
                .placeholderColor('#BCAAA4').fontSize(12).layoutWeight(1)
                .backgroundColor('#FFF8E1').borderRadius(8)
                .onChange((v: string) => { this.formMinSize = v })
              TextInput({ placeholder: '库存 500' })
                .placeholderColor('#BCAAA4').fontSize(12).layoutWeight(1)
                .backgroundColor('#FFF8E1').borderRadius(8).margin({ left: 8 })
                .onChange((v: string) => { this.formStock = v })
            }
            .margin({ left: 20, right: 20, top: 6 })
            Text('产地与配送').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
            TextInput({ placeholder: '产地(如:山东烟台)' })
              .placeholderColor('#BCAAA4').fontSize(12).width('100%')
              .backgroundColor('#FFF8E1').borderRadius(8)
              .margin({ left: 20, right: 20, top: 4 })
              .onChange((v: string) => { this.formOrigin = v })
            Row() {
              ForEach(DELIVERY_SLOTS.slice(0, 3), (s: DeliverySlotMeta) => {
                if (this.formDelivery === s.label) {
                  Text(s.icon + ' ' + s.label)
                    .fontSize(10).fontColor('#FFFFFF').backgroundColor('#E64A19')
                    .padding({ left: 6, right: 6, top: 4, bottom: 4 }).borderRadius(10)
                    .margin({ left: 2, right: 2 })
                } else {
                  Text(s.icon + ' ' + s.label)
                    .fontSize(10).fontColor('#E64A19').backgroundColor('#FFF3E0')
                    .padding({ left: 6, right: 6, top: 4, bottom: 4 }).borderRadius(10)
                    .margin({ left: 2, right: 2 })
                    .onClick(() => { this.formDelivery = s.label })
                }
              })
            }
            .margin({ left: 16, right: 16, top: 4 })
            Text('标签(逗号分隔)').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
            TextInput({ placeholder: '产地直发, 坏果包赔' })
              .placeholderColor('#BCAAA4').fontSize(12).width('100%')
              .backgroundColor('#FFF8E1').borderRadius(8)
              .margin({ left: 20, right: 20, top: 4 })
              .onChange((v: string) => { this.formTags = v })
          }
        }
        .layoutWeight(1)
        Row() {
          Text('取消').fontSize(14).fontColor('#795548')
            .backgroundColor('#FFF3E0').borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.showAddModal = false })
          Text('发布团购').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor('#E64A19').borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showAddModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 16, bottom: 16 })
      }
      .width('90%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '5%', y: '10%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder editProductModal() {
    Column() {
      this.modalOverlay(() => { this.showEditModal = false })
      Column() {
        Row() {
          Text('✏️ 编辑团购').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          Row().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showEditModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
        Divider().color('#FFF3E0')
        Column() {
          Text('商品名称').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
          TextInput({ placeholder: this.editingProduct?.name ?? '' })
            .placeholderColor('#BCAAA4').fontSize(14).width('100%')
            .backgroundColor('#FFF8E1').borderRadius(8)
            .margin({ left: 20, right: 20, top: 4 })
          Text('团购价').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
          TextInput({ placeholder: '¥' + (this.editingProduct?.groupPrice ?? 0).toString() })
            .placeholderColor('#BCAAA4').fontSize(14).width('100%')
            .backgroundColor('#FFF8E1').borderRadius(8)
            .margin({ left: 20, right: 20, top: 4 })
          Text('库存数量').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
          TextInput({ placeholder: (this.editingProduct?.stock ?? 0).toString() })
            .placeholderColor('#BCAAA4').fontSize(14).width('100%')
            .backgroundColor('#FFF8E1').borderRadius(8)
            .margin({ left: 20, right: 20, top: 4 })
          Text('配送时段').fontSize(12).fontColor('#795548').margin({ top: 12, left: 20 })
          TextInput({ placeholder: this.editingProduct?.deliveryTip ?? '' })
            .placeholderColor('#BCAAA4').fontSize(14).width('100%')
            .backgroundColor('#FFF8E1').borderRadius(8)
            .margin({ left: 20, right: 20, top: 4 })
        }
        .layoutWeight(1)
        Row() {
          Text('取消').fontSize(14).fontColor('#795548')
            .backgroundColor('#FFF3E0').borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.showEditModal = false })
          Text('保存修改').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor('#FFB300').borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showEditModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 16, bottom: 16 })
      }
      .width('88%')
      .constraintSize({ maxHeight: '65%' })
      .backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '6%', y: '15%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder deleteConfirmModal() {
    Column() {
      this.modalOverlay(() => { this.showDeleteConfirm = false })
      Column() {
        Text('⚠️').fontSize(48).margin({ top: 24 })
        Text('确认下架此团购?').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        Text('下架后已下单用户不受影响').fontSize(13).fontColor('#E64A19').margin({ top: 4 })
        Row() {
          Text(this.selectedProduct?.emoji ?? '🛒').fontSize(20)
          Text(this.selectedProduct?.name ?? '').fontSize(14).fontColor('#333333')
            .fontWeight(FontWeight.Bold).margin({ left: 8 })
        }
        .backgroundColor('#FFF3E0').borderRadius(10)
        .padding({ left: 16, right: 16, top: 10, bottom: 10 }).margin({ top: 16 })
        Row() {
          Text('取消').fontSize(14).fontColor('#795548')
            .backgroundColor('#FFF3E0').borderRadius(20)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .onClick(() => { this.showDeleteConfirm = false })
          Text('确认下架').fontSize(14).fontColor('#FFFFFF')
            .backgroundColor('#E64A19').borderRadius(20)
            .padding({ left: 28, right: 28, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.showDeleteConfirm = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 20, bottom: 20 })
      }
      .width('80%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '10%', y: '38%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder detailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetailModal = false })
      Column() {
        Row() {
          Column() {
            Text(this.selectedProduct?.emoji ?? '🛒').fontSize(40)
          }.width(70).height(70)
          .backgroundColor(PRODUCT_CAT_CONFIG[this.selectedProduct?.category ?? '']?.bg ?? '#FFF3E0')
          .borderRadius(16)
          .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
          Column() {
            Text(this.selectedProduct?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold)
              .fontColor('#3E2723')
            Row() {
              Text(GROUP_STATUS_CONFIG[this.selectedProduct?.status ?? '进行中']?.icon ?? '🔥')
                .fontSize(12)
              Text(GROUP_STATUS_CONFIG[this.selectedProduct?.status ?? '进行中']?.label ?? '进行中')
                .fontSize(11)
                .fontColor(GROUP_STATUS_CONFIG[this.selectedProduct?.status ?? '进行中']?.color ?? '#E64A19')
                .margin({ left: 4 })
              if (this.selectedProduct?.isHot ?? false) {
                Text('🔥 热销').fontSize(10).fontColor('#FFFFFF')
                  .backgroundColor('#E64A19')
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .borderRadius(8).margin({ left: 6 })
              }
            }
            .margin({ top: 4 })
          }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showDetailModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18 })
        Divider().color('#FFF3E0').margin({ top: 12 })
        Scroll() {
          Column() {
            Row() {
              Column() {
                Text('💰 团购价').fontSize(10).fontColor('#795548')
                Text('¥' + (this.selectedProduct?.groupPrice ?? 0).toString())
                  .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E64A19').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('📦 原价').fontSize(10).fontColor('#795548')
                Text('¥' + (this.selectedProduct?.originalPrice ?? 0).toString())
                  .fontSize(16).fontColor('#BCAAA4')
                  .decoration({ type: TextDecorationType.LineThrough })
                  .margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('👥 成团').fontSize(10).fontColor('#795548')
                Text((this.selectedProduct?.currentCount ?? 0).toString() + '/' + (this.selectedProduct?.minGroupSize ?? 0).toString())
                  .fontSize(14).fontColor('#43A047').fontWeight(FontWeight.Bold).margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            }
            .width('100%').margin({ top: 12 })
            Row() {
              Column() {
                Text('📈 已售').fontSize(10).fontColor('#795548')
                Text((this.selectedProduct?.sold ?? 0).toString() + '件')
                  .fontSize(13).fontColor('#3E2723').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('📦 库存').fontSize(10).fontColor('#795548')
                Text((this.selectedProduct?.stock ?? 0).toString() + '件')
                  .fontSize(13).fontColor('#3E2723').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('⭐ 评分').fontSize(10).fontColor('#795548')
                Text((this.selectedProduct?.rating ?? 0).toString())
                  .fontSize(13).fontColor('#FFB300').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            }
            .width('100%').margin({ top: 10 })
            Row() {
              Column() {
                Text('🌍 产地').fontSize(10).fontColor('#795548')
                Text(this.selectedProduct?.origin ?? '').fontSize(13).fontColor('#3E2723').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
              Column() {
                Text('🚚 配送').fontSize(10).fontColor('#795548')
                Text(this.selectedProduct?.deliveryTip ?? '').fontSize(13).fontColor('#E64A19').margin({ top: 2 })
              }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            }
            .width('100%').margin({ top: 10 })
            if (this.selectedProduct != null && this.selectedProduct.tags.length > 0) {
              Text('🏷️ 商品标签').fontSize(12).fontColor('#795548').margin({ top: 14, left: 4 })
              Row() {
                ForEach(this.selectedProduct.tags, (tag: string) => {
                  Text('#' + tag).fontSize(11).fontColor('#E64A19')
                    .backgroundColor('#FFF3E0').padding({ left: 7, right: 7, top: 3, bottom: 3 })
                    .borderRadius(10).margin({ left: 3, right: 3 })
                })
              }
              .width('100%').margin({ top: 6 })
            }
          }
          .padding({ left: 20, right: 20, bottom: 16 })
        }
        .layoutWeight(1)
        Row() {
          Text('✏️ 编辑').fontSize(13).fontColor('#FFFFFF')
            .backgroundColor('#FFB300').borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .onClick(() => { this.showEditModal = true })
          Text('🗑️ 下架').fontSize(13).fontColor('#FFFFFF')
            .backgroundColor('#E64A19').borderRadius(16)
            .padding({ left: 20, right: 20, top: 8, bottom: 8 })
            .margin({ left: 8 })
            .onClick(() => { this.showDeleteConfirm = true })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 12, bottom: 14 })
      }
      .width('92%')
      .constraintSize({ maxHeight: '78%' })
      .backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '4%', y: '8%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder productCard(p: GroupProductItem) {
    Column() {
      Row() {
        Text(p.emoji).fontSize(36)
      }
      .width('100%').height(80)
      .backgroundColor(PRODUCT_CAT_CONFIG[p.category]?.bg ?? '#FFF3E0')
      .borderRadius({ topLeft: 12, topRight: 12 })
      .justifyContent(FlexAlign.Center)
      Row() {
        if (p.isHot) {
          Text('🔥 热销').fontSize(9).fontColor('#FFFFFF')
            .backgroundColor('#E64A19')
            .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
        }
        if (p.isNew) {
          Text('🆕 新品').fontSize(9).fontColor('#FFFFFF')
            .backgroundColor('#43A047')
            .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
            .margin({ left: p.isHot ? 4 : 0 })
        }
        Row().layoutWeight(1)
        Text(GROUP_STATUS_CONFIG[p.status]?.icon ?? '🔥').fontSize(12)
      }
      .width('100%').padding({ left: 8, right: 8, top: 4 })
      Column() {
        Text(p.name).fontSize(12).fontColor('#3E2723').fontWeight(FontWeight.Medium)
          .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')
        Row() {
          Text('¥' + p.groupPrice.toString()).fontSize(16).fontColor('#E64A19')
            .fontWeight(FontWeight.Bold)
          Text('¥' + p.originalPrice.toString()).fontSize(11).fontColor('#BCAAA4')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 4 })
        }
        .margin({ top: 4 })
        Row() {
          Text((p.currentCount.toString()) + '/' + (p.minGroupSize.toString()))
            .fontSize(10).fontColor('#43A047')
          Text('已售' + (p.sold.toString())).fontSize(10).fontColor('#795548')
            .margin({ left: 6 })
        }
        .margin({ top: 4 })
        Row() {
          Text(p.deliveryTip).fontSize(9).fontColor('#E64A19')
            .backgroundColor('#FFF3E0')
            .padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
        }
        .margin({ top: 4 })
      }
      .width('100%').padding({ left: 8, right: 8, top: 4, bottom: 8 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('48%')
    .backgroundColor('#FFFFFF').borderRadius(12)
    .margin({ left: 4, right: 4, top: 4, bottom: 4 })
    .shadow({ radius: 4, color: '#1AE64A1910', offsetY: 2 })
    .onClick(() => { this.selectedProduct = p; this.showDetailModal = true })
  }

  build() {
    Stack() {
      Column() {
        // 顶部渐变头部
        Column() {
          Row() {
            Column() {
              Text('🏠 阳光社区').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
              Text('今日 ' + getActiveGroups().toString() + ' 个团进行中')
                .fontSize(11).fontColor('#FFCC80')
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            Text('🔔').fontSize(18).fontColor('#FFFFFF').margin({ right: 12 })
            Text('📍').fontSize(18).fontColor('#FFFFFF')
          }
          .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 8 })
          // 统计条
          Row() {
            Column() {
              Text(getTotalProducts().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('在售商品').fontSize(9).fontColor('#FFCC80')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text(getHotProducts().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFE082')
              Text('热销商品').fontSize(9).fontColor('#FFCC80')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text('¥' + getTotalSales().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('累计消费').fontSize(9).fontColor('#FFCC80')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
            Column() {
              Text(getPendingPickup().toString()).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#A5D6A7')
              Text('待提货').fontSize(9).fontColor('#FFCC80')
            }.layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('100%').padding({ top: 8, bottom: 12 })
        }
        .width('100%')
        .linearGradient({
          angle: 135,
          colors: [['#E64A19', 0.0], ['#BF360C', 1.0]]
        })

        // 分类滚动筛选
        Scroll() {
          Row() {
            ForEach(CAT_FILTERS, (cat: string) => {
              if (this.selectedCat === cat) {
                Text(cat === '全部' ? '📋 全部' : ((PRODUCT_CAT_CONFIG[cat]?.icon ?? '') + ' ' + cat))
                  .fontSize(11).fontColor('#FFFFFF').backgroundColor('#E64A19')
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(14)
                  .margin({ left: 3, right: 3 })
              } else {
                Text(cat === '全部' ? '📋 全部' : ((PRODUCT_CAT_CONFIG[cat]?.icon ?? '') + ' ' + cat))
                  .fontSize(11).fontColor('#E64A19').backgroundColor('#FFF3E0')
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(14)
                  .margin({ left: 3, right: 3 })
                  .onClick(() => { this.selectedCat = cat })
              }
            })
          }
          .padding({ left: 8, right: 8 })
        }
        .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(40)

        // 排序栏
        Row() {
          Text('📝 排序:').fontSize(11).fontColor('#795548').margin({ left: 12 })
          ForEach(SORT_OPTIONS, (s: string) => {
            if (this.selectedSort === s) {
              Text(s).fontSize(10).fontColor('#FFFFFF').backgroundColor('#FFB300')
                .padding({ left: 6, right: 6, top: 3, bottom: 3 }).borderRadius(10)
                .margin({ left: 3, right: 3 })
            } else {
              Text(s).fontSize(10).fontColor('#795548')
                .padding({ left: 6, right: 6, top: 3, bottom: 3 })
                .margin({ left: 3, right: 3 })
                .onClick(() => { this.selectedSort = s })
            }
          })
          Row().layoutWeight(1)
          Text('➕').fontSize(20).fontColor('#FFFFFF')
            .backgroundColor('#E64A19').width(28).height(28).borderRadius(14)
            .textAlign(TextAlign.Center).margin({ right: 12 })
            .onClick(() => { this.showAddModal = true })
        }
        .width('100%').padding({ top: 6, bottom: 6 })
        .backgroundColor('#FFF8E1')

        // Grid双列商品列表
        Scroll() {
          Column() {
            Row() {
              Column() {
                this.productCard(mockProducts[0])
                this.productCard(mockProducts[1])
                this.productCard(mockProducts[2])
                this.productCard(mockProducts[3])
                this.productCard(mockProducts[4])
                this.productCard(mockProducts[5])
                this.productCard(mockProducts[6])
                this.productCard(mockProducts[7])
                this.productCard(mockProducts[8])
                this.productCard(mockProducts[9])
                this.productCard(mockProducts[10])
                this.productCard(mockProducts[11])
              }
              .layoutWeight(1)
              Column() {
                this.productCard(mockProducts[12])
                this.productCard(mockProducts[13])
                this.productCard(mockProducts[14])
                this.productCard(mockProducts[15])
                this.productCard(mockProducts[16])
                this.productCard(mockProducts[17])
                this.productCard(mockProducts[18])
                this.productCard(mockProducts[19])
                this.productCard(mockProducts[20])
                this.productCard(mockProducts[21])
                this.productCard(mockProducts[22])
                this.productCard(mockProducts[23])
              }
              .layoutWeight(1)
            }
            .width('100%').padding({ left: 4, right: 4 })
          }
        }
        .layoutWeight(1)
      }
      if (this.showAddModal) { this.addProductModal() }
      if (this.showEditModal) { this.editProductModal() }
      if (this.showDeleteConfirm) { this.deleteConfirmModal() }
      if (this.showDetailModal) { this.detailModal() }
    }
    .width('100%').height('100%')
  }
}

// ==================== Tab2:我的订单 ====================
@Component
struct MyOrdersContent {
  @State selectedStatus: string = '全部'
  @State showDetailModal: boolean = false
  @State selectedOrder: OrderItem | null = null

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

  @Builder orderDetailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetailModal = false })
      Column() {
        Row() {
          Text('📋 订单详情').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          Row().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showDetailModal = false })
        }
        .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 12 })
        Divider().color('#FFF3E0')
        Column() {
          Row() {
            Text(this.selectedOrder?.productEmoji ?? '🛒').fontSize(48)
            Column() {
              Text(this.selectedOrder?.productName ?? '').fontSize(15).fontWeight(FontWeight.Bold)
                .fontColor('#3E2723')
              Text('x' + (this.selectedOrder?.quantity ?? 0).toString())
                .fontSize(13).fontColor('#795548').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
            Text('¥' + (this.selectedOrder?.totalPrice ?? 0).toString())
              .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          }
          .width('100%').padding({ left: 20, right: 20, top: 16 })
          Row() {
            Column() {
              Text('订单编号').fontSize(10).fontColor('#BCAAA4')
              Text('#' + (this.selectedOrder?.id ?? 0).toString().padStart(6, '0'))
                .fontSize(13).fontColor('#3E2723').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            Column() {
              Text('下单日期').fontSize(10).fontColor('#BCAAA4')
              Text(this.selectedOrder?.orderDate ?? '').fontSize(13).fontColor('#3E2723').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          }
          .width('100%').padding({ left: 20, right: 20, top: 12 })
          Row() {
            Column() {
              Text('配送方式').fontSize(10).fontColor('#BCAAA4')
              Text(this.selectedOrder?.deliverySlot ?? '').fontSize(13).fontColor('#E64A19').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            Column() {
              Text('自提点').fontSize(10).fontColor('#BCAAA4')
              Text(this.selectedOrder?.pickupPoint ?? '').fontSize(12).fontColor('#3E2723').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          }
          .width('100%').padding({ left: 20, right: 20, top: 10 })
          Row() {
            Column() {
              Text('收货人').fontSize(10).fontColor('#BCAAA4')
              Text(this.selectedOrder?.receiver ?? '').fontSize(13).fontColor('#3E2723').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
            Column() {
              Text('联系电话').fontSize(10).fontColor('#BCAAA4')
              Text(this.selectedOrder?.phone ?? '').fontSize(13).fontColor('#3E2723').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          }
          .width('100%').padding({ left: 20, right: 20, top: 10 })
          if (this.selectedOrder != null && this.selectedOrder.remark !== '') {
            Column() {
              Text('备注').fontSize(10).fontColor('#BCAAA4')
              Text(this.selectedOrder?.remark ?? '').fontSize(13).fontColor('#795548').margin({ top: 4 })
            }
            .width('100%').padding({ left: 20, right: 20, top: 10 }).alignItems(HorizontalAlign.Start)
          }
        }
        .layoutWeight(1)
        Row() {
          Text(ORDER_STATUS_CONFIG[this.selectedOrder?.status ?? '待付款']?.icon + ' ' +
            ORDER_STATUS_CONFIG[this.selectedOrder?.status ?? '待付款']?.label)
            .fontSize(14).fontColor(ORDER_STATUS_CONFIG[this.selectedOrder?.status ?? '待付款']?.color)
            .backgroundColor(ORDER_STATUS_CONFIG[this.selectedOrder?.status ?? '待付款']?.bg)
            .padding({ left: 16, right: 16, top: 8, bottom: 8 }).borderRadius(20)
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 12, bottom: 16 })
      }
      .width('88%')
      .constraintSize({ maxHeight: '70%' })
      .backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '6%', y: '12%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder orderCard(o: OrderItem) {
    Column() {
      Row() {
        Text(o.productEmoji).fontSize(32)
        Column() {
          Text(o.productName).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#3E2723')
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          Text('x' + o.quantity.toString() + ' · ¥' + o.unitPrice.toString() + '/件')
            .fontSize(11).fontColor('#795548').margin({ top: 2 })
        }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
        Column() {
          Text('¥' + o.totalPrice.toString())
            .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          Row() {
            Text(ORDER_STATUS_CONFIG[o.status]?.icon ?? '⏳')
              .fontSize(10)
            Text(ORDER_STATUS_CONFIG[o.status]?.label ?? '待付款')
              .fontSize(10)
              .fontColor(ORDER_STATUS_CONFIG[o.status]?.color ?? '#E64A19')
              .margin({ left: 2 })
          }
          .margin({ top: 2 })
        }.alignItems(HorizontalAlign.End)
      }
      .width('100%')
      Row() {
        Text('📅 ' + o.orderDate).fontSize(10).fontColor('#BCAAA4')
        Text('📍 ' + o.deliverySlot).fontSize(10).fontColor('#E64A19')
          .margin({ left: 8 })
        Text('🏪 ' + o.pickupPoint).fontSize(10).fontColor('#795548')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
          .layoutWeight(1).margin({ left: 8 })
      }
      .width('100%').margin({ top: 6 })
    }
    .width('100%').backgroundColor('#FFFFFF').borderRadius(10)
    .padding({ left: 12, right: 12, top: 10, bottom: 10 })
    .margin({ left: 12, right: 12, top: 4, bottom: 4 })
    .shadow({ radius: 3, color: '#1AE64A1908', offsetY: 1 })
    .onClick(() => { this.selectedOrder = o; this.showDetailModal = true })
  }

  build() {
    Stack() {
      Column() {
        Text('📦 我的订单').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          .width('100%').padding({ left: 16, top: 14, bottom: 8 })
        Row() {
          ForEach(['全部', '待付款', '已付款', '配送中', '已签收', '已退款'], (s: string) => {
            if (this.selectedStatus === s) {
              Text(s).fontSize(11).fontColor('#FFFFFF').backgroundColor('#E64A19')
                .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
                .margin({ left: 3, right: 3 })
            } else {
              Text(s).fontSize(11).fontColor('#795548').backgroundColor('#FFF3E0')
                .padding({ left: 8, right: 8, top: 5, bottom: 5 }).borderRadius(12)
                .margin({ left: 3, right: 3 })
                .onClick(() => { this.selectedStatus = s })
            }
          })
        }
        .width('100%').padding({ left: 12, bottom: 8 })
        Scroll() {
          Column() {
            this.orderCard(mockOrders[0])
            this.orderCard(mockOrders[1])
            this.orderCard(mockOrders[2])
            this.orderCard(mockOrders[3])
            this.orderCard(mockOrders[4])
            this.orderCard(mockOrders[5])
            this.orderCard(mockOrders[6])
            this.orderCard(mockOrders[7])
            this.orderCard(mockOrders[8])
            this.orderCard(mockOrders[9])
            this.orderCard(mockOrders[10])
            this.orderCard(mockOrders[11])
            this.orderCard(mockOrders[12])
            this.orderCard(mockOrders[13])
            this.orderCard(mockOrders[14])
          }
          .padding({ bottom: 12 })
        }
        .layoutWeight(1)
      }
      if (this.showDetailModal) { this.orderDetailModal() }
    }
    .width('100%').height('100%').backgroundColor('#FFF8E1')
  }
}

// ==================== Tab3:分类浏览 ====================
@Component
struct CategoryBrowseContent {
  build() {
    Column() {
      Text('🗂️ 商品分类').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      Scroll() {
        Column() {
          ForEach(['生鲜蔬果', '肉禽蛋品', '米面粮油', '乳品饮料', '零食糕点', '日用百货'], (cat: string) => {
            Column() {
              Row() {
                Text(PRODUCT_CAT_CONFIG[cat]?.icon ?? '📂').fontSize(32)
                Text(PRODUCT_CAT_CONFIG[cat]?.label ?? cat)
                  .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#3E2723')
                  .margin({ left: 10 })
                Row().layoutWeight(1)
                Text('>').fontSize(16).fontColor('#BCAAA4')
              }
              .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
              Row() {
                Column() {
                  Text('5').fontSize(18).fontWeight(FontWeight.Bold).fontColor(PRODUCT_CAT_CONFIG[cat]?.color)
                  Text('在售').fontSize(9).fontColor('#795548')
                }.layoutWeight(1).alignItems(HorizontalAlign.Center)
                Column() {
                  Text('128').fontSize(18).fontWeight(FontWeight.Bold).fontColor(PRODUCT_CAT_CONFIG[cat]?.color)
                  Text('已售').fontSize(9).fontColor('#795548')
                }.layoutWeight(1).alignItems(HorizontalAlign.Center)
                Column() {
                  Text('4.7').fontSize(18).fontWeight(FontWeight.Bold).fontColor(PRODUCT_CAT_CONFIG[cat]?.color)
                  Text('评分').fontSize(9).fontColor('#795548')
                }.layoutWeight(1).alignItems(HorizontalAlign.Center)
              }
              .width('100%').padding({ top: 8, bottom: 8 })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 6, bottom: 6 })
            .shadow({ radius: 3, color: '#1AE64A1908', offsetY: 1 })
          })
        }
        .padding({ bottom: 12 })
      }
      .layoutWeight(1)
    }
    .width('100%').height('100%').backgroundColor('#FFF8E1')
  }
}

// ==================== Tab4:数据统计(简单柱状图) ====================
@Component
struct StatsContent {
  build() {
    Column() {
      Text('📊 数据统计').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#3E2723')
        .width('100%').padding({ left: 16, top: 14, bottom: 8 })
      // 四宫格KPI
      Row() {
        Column() {
          Text(getTotalProducts().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#E64A19')
          Text('在售商品').fontSize(10).fontColor('#795548')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column() {
          Text(getTotalOrders().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFB300')
          Text('累计订单').fontSize(10).fontColor('#795548')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
      .padding({ top: 14, bottom: 14 }).margin({ left: 12, right: 12 })
      Row() {
        Column() {
          Text('¥' + getTotalSales().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#43A047')
          Text('累计消费').fontSize(10).fontColor('#795548')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
        Column() {
          Text(getPendingPickup().toString()).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1565C0')
          Text('待提货').fontSize(10).fontColor('#795548')
        }.layoutWeight(1).alignItems(HorizontalAlign.Center)
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
      .padding({ top: 14, bottom: 14 }).margin({ left: 12, right: 12, top: 8 })

      // 柱状图区域
      Column() {
        Text('📈 本周消费趋势').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          .margin({ top: 12, left: 16, bottom: 8 })
        Row() {
          ForEach(WEEKLY_BAR, (item: BarChartItem) => {
            Column() {
              Column() {
                Text(item.value.toString()).fontSize(9).fontColor('#FFFFFF')
              }
              .width(20).height(item.value / getMaxBarValue() * 120)
              .backgroundColor(item.color).borderRadius({ topLeft: 4, topRight: 4 })
              .justifyContent(FlexAlign.End)
              Text(item.label).fontSize(9).fontColor('#795548').margin({ top: 4 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%').height(160)
        .padding({ left: 12, right: 12, bottom: 12 })
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
      .margin({ left: 12, right: 12, top: 8 })
      .layoutWeight(1)

      // 配送时段分布
      Column() {
        Text('🚚 配送时段偏好').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
          .margin({ top: 12, left: 16, bottom: 8 })
        ForEach(DELIVERY_SLOTS, (slot: DeliverySlotMeta) => {
          Row() {
            Text(slot.icon + ' ' + slot.label).fontSize(12).fontColor('#3E2723')
              .layoutWeight(1)
            Text(slot.time).fontSize(12).fontColor('#E64A19')
          }
          .width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
          Divider().color('#FFF3E0')
        })
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
      .margin({ left: 12, right: 12, top: 8, bottom: 12 })
    }
    .width('100%').height('100%').backgroundColor('#FFF8E1')
  }
}

// ==================== Tab5:个人中心 ====================
@Component
struct ProfileContent {
  build() {
    Scroll() {
      Column() {
        // 用户头部
        Column() {
          Text('🧑').fontSize(56)
          Text('张先生').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('138****8888').fontSize(12).fontColor('#FFCC80').margin({ top: 2 })
        }
        .width('100%').padding({ top: 30, bottom: 30 })
        .linearGradient({
          angle: 135,
          colors: [['#E64A19', 0.0], ['#BF360C', 1.0]]
        })
        .alignItems(HorizontalAlign.Center)

        // 会员卡
        Row() {
          Column() {
            Text('🏅 团长会员').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#3E2723')
            Text('累计消费 ¥' + getTotalSales().toString() + ' · 省了 ¥' + (getTotalSales() * 15 / 100).toString().split('.')[0])
              .fontSize(11).fontColor('#795548').margin({ top: 2 })
          }.layoutWeight(1).alignItems(HorizontalAlign.Start)
          Text('查看权益 >').fontSize(11).fontColor('#E64A19')
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .margin({ left: 12, right: 12, top: -16 })
        .shadow({ radius: 6, color: '#1A000000', offsetY: 2 })

        // 功能列表
        Column() {
          Row() {
            Text('📍').fontSize(22)
            Text('收货地址管理').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('3个地址').fontSize(12).fontColor('#BCAAA4')
            Text('>').fontSize(16).fontColor('#BCAAA4').margin({ left: 8 })
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
          Divider().color('#FFF3E0')
          Row() {
            Text('🏪').fontSize(22)
            Text('自提点管理').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('2个自提点').fontSize(12).fontColor('#BCAAA4')
            Text('>').fontSize(16).fontColor('#BCAAA4').margin({ left: 8 })
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
          Divider().color('#FFF3E0')
          Row() {
            Text('💳').fontSize(22)
            Text('支付方式').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('微信支付').fontSize(12).fontColor('#BCAAA4')
            Text('>').fontSize(16).fontColor('#BCAAA4').margin({ left: 8 })
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
          Divider().color('#FFF3E0')
          Row() {
            Text('🎫').fontSize(22)
            Text('我的优惠券').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('5张可用').fontSize(12).fontColor('#E64A19')
            Text('>').fontSize(16).fontColor('#BCAAA4').margin({ left: 8 })
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
          Divider().color('#FFF3E0')
          Row() {
            Text('📞').fontSize(22)
            Text('联系客服').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('>').fontSize(16).fontColor('#BCAAA4')
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
          Divider().color('#FFF3E0')
          Row() {
            Text('⚙️').fontSize(22)
            Text('设置').fontSize(14).fontColor('#3E2723').layoutWeight(1).margin({ left: 12 })
            Text('>').fontSize(16).fontColor('#BCAAA4')
          }
          .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .margin({ left: 12, right: 12, top: 12, bottom: 12 })
      }
    }
    .layoutWeight(1)
  }
}


十八、结语

社区团购作为新零售的重要形态,其移动端管理系统涉及商品、订单、统计、用户等多个业务域,是检验一个 UI 框架综合能力的绝佳场景。本文通过对这个基于 HarmonyOS ArkTS 实现的社区团购管理系统的逐段剖析,展示了从接口定义、配置管理、数据建模、统计聚合到 UI 渲染、交互弹窗、数据可视化的完整技术链路。

在这里插入图片描述

ArkTS 的声明式 UI 范式、装饰器响应式机制、强类型系统,为构建这类复杂业务应用提供了扎实的语言级支撑。通过配置驱动的状态管理、防御性的空值处理、组件化的 UI 复用、自绘的数据可视化,系统在不依赖任何第三方库的情况下,实现了一个功能完整、视觉统一、交互流畅的团购管理原型。

当然,作为一个演示性实现,系统还有可优化的空间。例如,表单状态可以抽离为独立的表单组件;商品列表的双 Column 手动分列可以替换为真正的 Grid 组件以支持动态数据;柱状图可以增加触摸交互与动画效果;统计函数在数据量大时可以改用 filter + reduce 的函数式写法或缓存计算结果。这些优化方向也是从原型走向生产级应用需要跨越的台阶。

Logo

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

更多推荐