基于HarmonyOS API 24社区团购管理系统通过 GROUP_STATUS_CONFIG[status] 即可 O(1) 时间获取任意状态的完整视觉信息
一、引言:社区团购的崛起与技术挑战
近年来,社区团购作为一种新兴的零售业态,迅速渗透到中国城乡居民的日常生活中。它以社区为基本单元,以团长为纽带,将产地直发的生鲜、食品、日用品等商品通过集中采购、统一配送的方式送到消费者手中。这种模式的核心价值在于"以销定采"——通过提前拼团锁定需求,大幅降低了仓储损耗与物流成本,同时让消费者享受到比传统电商更具价格优势的商品。

对于普通家庭而言,社区团购意味着更低的生活成本和更便捷的购物体验。只需在手机上轻点几下,第二天便能在小区门口的自提点取到新鲜的水果、蔬菜、肉类和米面粮油。这种"线上拼团 + 线下自提"的模式,既规避了传统电商"最后一公里"配送成本高昂的问题,又解决了社区便利店商品品类有限的痛点。
然而,要支撑这样一个完整的团购业务闭环,开发者面临诸多技术挑战。首先是商品信息的复杂度——一个团购商品包含名称、分类、价格、团购价、成团人数、当前参团人数、库存、销量、评分、标签、产地、配送时段等十余个维度;其次是状态的多样性——团购本身有"进行中、已成团、待发货、已送达、已取消"五种状态,订单又有"待付款、已付款、配送中、已签收、已退款"五种状态;最后是交互的丰富度——发布团购、编辑商品、下架商品、查看详情、筛选分类、排序、查看订单详情等操作都需要精心设计。
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 个属性:
- 商品关联:
productName、productEmoji,冗余存储商品名称和图标,避免订单与商品的关联查询。 - 交易信息:
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.TODAY 比 this.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(当前排序方式,默认"人气优先")。 - 弹窗显隐状态:
showAddModal、showEditModal、showDeleteConfirm、showDetailModal,四个布尔值分别控制四个弹窗的显示。 - 上下文状态:
selectedProduct(当前查看详情的商品)、editingProduct(当前编辑的商品),类型为GroupProductItem | null,支持空值。 - 表单状态:
formName、formCat、formPrice等 8 个字段,用于发布团购表单的数据绑定。
注意表单字段都使用 string 类型而非 number,这是因为 TextInput 的 onChange 回调返回的是字符串,使用字符串状态避免了频繁的类型转换,在提交时再做转换即可。
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 用浅灰 #BCAAA4。onChange 回调将输入值同步到对应的 @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) 居中排列。整个弹窗用 position 和 zIndex(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 })
色块下方是标签行,左侧根据 isHot 和 isNew 条件渲染热销和新品角标。如果两个都为真,新品标签通过 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)
分类筛选条是一个横向 Scroll,scrollable(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 原生组件(Column、Row、Text)自绘,通过数学计算 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 的函数式写法或缓存计算结果。这些优化方向也是从原型走向生产级应用需要跨越的台阶。
更多推荐

所有评论(0)