引言

社区团购作为新零售领域的重要业态,正在通过"集中采购+社区自提"的模式重构最后一公里商品流通链路。在这一模式中,团长作为社区节点核心角色,承担着商品上架、订单管理、团员维护、提货核销、收益统计等多重职责。本应用基于HarmonyOS ArkTS声明式UI框架,完整构建了一个面向社区团购团长的六模块全功能管理平台,覆盖了从订单到结算的完整业务闭环,充分展示了ArkTS在企业级移动应用开发中的架构表达力与组件复用能力。

从技术架构角度审视,本应用采用了"接口契约层—纯函数业务层—组件表现层"的分层架构。接口契约层定义了订单信息、商品信息、团员信息、提货信息、每日收益、分类收益、团长信息等十余个数据结构,通过TypeScript强类型系统确保了数据流转的安全性。纯函数业务层封装了按状态筛选订单、拼接商品文本、统计今日订单和流水、计算会员等级数量、掩码手机号、计算柱状图高度等十余个工具函数,实现了数据处理逻辑与UI渲染逻辑的彻底解耦。组件表现层则由六个独立的@Page级组件构成,每个组件通过@State状态驱动内部交互,通过@Builder方法封装可复用UI片段。

从交互设计角度分析,应用采用了看板式订单管理、双列网格商品上架、列表式团员管理、核销码提货验证、仪表盘式收益统计、个人中心式团长管理六种差异化的页面布局策略。每个页面都配备了对应的模态弹框,包括新增订单、订单详情、编辑商品、删除确认、团员详情、核销确认和结算提现七个弹框,构成了完整的CRUD交互体系。全局统一的pageHeader公共头部构建器、modalOverlay通用遮罩构建器和色彩常量系统,确保了六大模块之间的视觉一致性和交互连贯性。

一、数据模型体系与接口契约定义

在任何企业级应用中,数据模型的严谨定义是确保系统可维护性的第一道防线。本应用在文件顶部定义了十三个接口和一个色彩调色板,构建了覆盖团购业务全场景的强类型数据契约体系。每个接口都精确约束了字段名称、类型和业务含义,为后续组件提供了可靠的数据访问保障。

interface OrderItemInfo { itemName: string; qty: number }
interface OrderInfo {
  id: number; orderNo: string; memberName: string; phone: string; items: OrderItemInfo[]
  totalAmount: number; status: string; createDate: string; pickupCode: string
  pickupLocation: string; imageColor: string; note: string
}
interface ProductInfo {
  id: number; name: string; category: string; groupPrice: number; originalPrice: number
  stock: number; soldCount: number; minOrder: number; unit: string; supplier: string
  imageColor: string; status: string; hot: boolean
}
interface MemberInfo {
  id: number; name: string; phone: string; avatarColor: string; orderCount: number
  totalSpent: number; level: string; joinDate: string; lastOrder: string; address: string; note: string
}
interface PickupInfo {
  id: number; orderNo: string; memberName: string; pickupCode: string; items: OrderItemInfo[]
  amount: number; status: string; pickupDate: string; pickupLocation: string; verified: boolean
}
interface DailyRevenueInfo { date: string; amount: number; orderCount: number; commission: number }
interface CategoryRevenueInfo { category: string; amount: number; percent: number; barColor: string }
interface LeaderInfo {
  name: string; phone: string; communityName: string; totalRevenue: number; totalCommission: number
  memberCount: number; rating: number; level: string; badges: string[]
}
interface ColorPalette {
  primary: string; secondary: string; bg: string; card: string; text: string; subText: string
  warning: string; danger: string; border: string; white: string; purple: string; blue: string
}
interface BankAccountInfo { id: number; bankName: string; cardNo: string; holder: string }
interface StatCardInfo { title: string; value: string; trend: string; color: string }
interface MenuInfo { icon: string; title: string; hint: string }
interface TabItemInfo { index: number; icon: string; label: string }

在这里插入图片描述

上述接口体系构成了团购业务的完整数据模型。OrderItemInfo是最基础的子接口,描述订单中的单个商品项(名称+数量),被OrderInfoPickupInfo两个接口复用,体现了数据模型设计的DRY原则。OrderInfo是订单管理的核心实体,包含了订单号、团员信息、商品列表、总金额、状态、创建时间、提货码、提货地点、图标颜色和备注等完整字段。ProductInfo描述了团购商品的完整信息,特别包含了团购价、原价对比、库存、销量、起订量、单位、供应商等电商核心字段。

MemberInfo定义了团员信息结构,包含了等级、加入日期、最近下单时间、住址和备注等社群运营关键字段。PickupInfo描述了提货核销记录,verified布尔字段清晰区分了已核销和待核销状态。DailyRevenueInfoCategoryRevenueInfo分别服务于每日和分类两个维度的收益统计展示。LeaderInfo是团长个人信息的聚合接口,包含了累计销售额、佣金、团员数、评分、等级和徽章等团长画像数据。BankAccountInfoStatCardInfoMenuInfoTabItemInfo四个辅助接口分别服务于提现、统计卡片、菜单项和导航项的UI渲染需求。

二、色彩系统与业务配置常量

应用采用团购橙(#FF6F00)与社区绿(#43A047)的双色搭配作为主题色,橙色传递团购的活力与热情,绿色暗示社区生态的健康与可持续。全局色彩和业务配置通过常量统一管理,确保了六大模块之间的视觉一致性和配置可维护性。

const COLORS: ColorPalette = {
  primary: '#FF6F00', secondary: '#43A047', bg: '#FFF8E1', card: '#FFFFFF', text: '#3E2723',
  subText: '#8D6E63', warning: '#FF8F00', danger: '#E53935', border: '#FFE0B2', white: '#FFFFFF',
  purple: '#8E24AA', blue: '#1E88E5'
}

const STATUS_COLORS: Record<string, string> = {
  '待确认': '#FB8C00', '备货中': '#1E88E5', '待提货': '#8E24AA', '已完成': '#43A047', '已取消': '#9E9E9E'
}
const PRODUCT_CATEGORIES: Record<string, string> = {
  '生鲜': '生鲜', '零食': '零食', '日用': '日用', '美妆': '美妆', '家居': '家居', '饮品': '饮品'
}
const MEMBER_LEVELS: Record<string, string> = { '普通': '普通', '银卡': '银卡', '金卡': '金卡', '钻石': '钻石' }
const LEVEL_COLORS: Record<string, string> = {
  '普通': '#9E9E9E', '银卡': '#90A4AE', '金卡': '#FFB300', '钻石': '#8E24AA'
}
const LEVEL_STATS: Record<string, number> = { '钻石': 1, '金卡': 3, '银卡': 4, '普通': 8 }
const STATUS_STATS: Record<string, number> = { '待确认': 4, '备货中': 4, '待提货': 4, '已完成': 3, '已取消': 1 }

色彩系统定义了12色设计令牌。主色#FF6F00是Material Design标准橙色,用于头部背景、主按钮、价格高亮等核心视觉元素。辅助色#43A047绿色用于核销按钮、收益金额、库存状态等正面语义场景。背景色#FFF8E1是一种温暖的浅黄色,营造出社区团购的亲切感。文字色分为深棕#3E2723和浅棕#8D6E63两级,与橙黄色背景形成和谐搭配。边框色#FFE0B2是浅橙色,用于卡片分隔和表单项的分界线。

业务配置常量使用了Record<string, string>Record<string, number>两种泛型映射类型。STATUS_COLORS将五种订单状态映射到对应的颜色值,实现状态-颜色的集中管理,组件中只需STATUS_COLORS[order.status]即可获取对应色彩。LEVEL_COLORS将四种会员等级映射到对应的徽章颜色,LEVEL_STATSSTATUS_STATS则预存了统计数据,避免了每次渲染时的重复计算。这种"配置即数据"的设计模式使得后续修改状态颜色或会员等级配置时,只需修改常量即可,无需触及组件代码。

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

在前后端分离的开发模式下,Mock数据层是前端独立开发的关键支撑。本应用定义了八组Mock数据和一组团长配置数据,覆盖了订单、商品、团员、提货、每日收益、分类收益、统计卡片、银行账户、菜单项和Tab配置等全部业务场景。配合十余个纯函数封装的业务逻辑,实现了完整的数据驱动渲染效果。

const ORDER_LIST: OrderInfo[] = [
  { id: 1, orderNo: 'TG20260824001', memberName: '王丽', phone: '13812345678', items: [{ itemName: '红富士苹果', qty: 3 }, { itemName: '土鸡蛋', qty: 2 }], totalAmount: 68.5, status: '待确认', createDate: '08-24 09:15', pickupCode: 'PC824001', pickupLocation: '3号楼架空层', imageColor: '#FF6F00', note: '下班后自提' },
  { id: 2, orderNo: 'TG20260824002', memberName: '张强', phone: '13998765432', items: [{ itemName: '东北大米10kg', qty: 1 }, { itemName: '花生油5L', qty: 1 }], totalAmount: 129.9, status: '待确认', createDate: '08-24 09:32', pickupCode: 'PC824002', pickupLocation: '3号楼架空层', imageColor: '#43A047', note: '' },
  { id: 4, orderNo: 'TG20260824004', memberName: '刘洋', phone: '15811112222', items: [{ itemName: '阳光玫瑰葡萄', qty: 2 }, { itemName: '酸奶八连杯', qty: 1 }], totalAmount: 79.8, status: '备货中', createDate: '08-24 07:48', pickupCode: 'PC824004', pickupLocation: '3号楼架空层', imageColor: '#8E24AA', note: '' },
  { id: 12, orderNo: 'TG20260824012', memberName: '褚阳', phone: '13077778888', items: [{ itemName: '牛肉2斤', qty: 1 }, { itemName: '火锅底料', qty: 2 }], totalAmount: 128.0, status: '已完成', createDate: '08-22 15:10', pickupCode: 'PC822012', pickupLocation: '3号楼架空层', imageColor: '#D84315', note: '已提货' }
]

const PRODUCT_LIST: ProductInfo[] = [
  { id: 1, name: '红富士苹果 5斤装', category: '生鲜', groupPrice: 19.9, originalPrice: 29.9, stock: 86, soldCount: 432, minOrder: 1, unit: '箱', supplier: '烟台果园直供', imageColor: '#FF6F00', status: '上架', hot: true },
  { id: 2, name: '东北大米 10kg', category: '生鲜', groupPrice: 59.9, originalPrice: 79.9, stock: 42, soldCount: 218, minOrder: 1, unit: '袋', supplier: '五常产地仓', imageColor: '#FFB300', status: '上架', hot: true },
  { id: 5, name: '小龙虾 3斤装', category: '生鲜', groupPrice: 99.0, originalPrice: 139.0, stock: 12, soldCount: 98, minOrder: 1, unit: '份', supplier: '洪湖水产', imageColor: '#E53935', status: '缺货', hot: false },
  { id: 10, name: '纸巾家庭装 24卷', category: '日用', groupPrice: 29.9, originalPrice: 39.9, stock: 210, soldCount: 876, minOrder: 1, unit: '提', supplier: '洁柔工厂店', imageColor: '#00ACC1', status: '上架', hot: true }
]

const MEMBER_LIST: MemberInfo[] = [
  { id: 1, name: '王丽', phone: '13812345678', avatarColor: '#E53935', orderCount: 38, totalSpent: 2860.5, level: '钻石', joinDate: '2025-03-12', lastOrder: '2026-08-24', address: '3栋502', note: '超级活跃' },
  { id: 2, name: '张强', phone: '13998765432', avatarColor: '#1E88E5', orderCount: 26, totalSpent: 1930.0, level: '金卡', joinDate: '2025-04-05', lastOrder: '2026-08-24', address: '5栋1103', note: '' }
]

const DAILY_REVENUE: DailyRevenueInfo[] = [
  { date: '08-18', amount: 1250.0, orderCount: 26, commission: 125.0 },
  { date: '08-19', amount: 980.5, orderCount: 19, commission: 98.0 },
  { date: '08-20', amount: 1560.0, orderCount: 32, commission: 156.0 },
  { date: '08-21', amount: 720.0, orderCount: 15, commission: 72.0 },
  { date: '08-22', amount: 1890.0, orderCount: 38, commission: 189.0 },
  { date: '08-23', amount: 2105.4, orderCount: 42, commission: 210.5 },
  { date: '08-24', amount: 1690.8, orderCount: 35, commission: 169.1 }
]

const CATEGORY_REVENUE: CategoryRevenueInfo[] = [
  { category: '生鲜', amount: 8930.0, percent: 46, barColor: '#43A047' },
  { category: '饮品', amount: 3560.0, percent: 18, barColor: '#1E88E5' },
  { category: '零食', amount: 2950.0, percent: 15, barColor: '#FF6F00' },
  { category: '日用', amount: 2150.0, percent: 11, barColor: '#00897B' },
  { category: '美妆', amount: 1080.0, percent: 6, barColor: '#D81B60' },
  { category: '家居', amount: 740.0, percent: 4, barColor: '#8E24AA' }
]

const REVENUE_CARDS: StatCardInfo[] = [
  { title: '本周销售额', value: '¥10246.7', trend: '↑ 12.5%', color: '#FF6F00' },
  { title: '本周佣金', value: '¥1019.6', trend: '↑ 8.3%', color: '#43A047' },
  { title: '本周订单', value: '207单', trend: '日均29.6单', color: '#1E88E5' },
  { title: '客单价', value: '¥49.5', trend: '↑ 3.1%', color: '#8E24AA' }
]

在这里插入图片描述

Mock数据的设计贴近真实团购场景。订单数据包含了五位不同状态(待确认、备货中、待提货、已完成、已取消)的16条记录,每条订单都有真实格式的订单号(TG+日期+序号)、团员姓名、手机号、商品列表、金额和备注。商品数据涵盖了生鲜、零食、饮品、日用、美妆、家居六个品类共16件商品,每件商品配有团购价与原价对比、库存、销量、起订量、单位和供应商信息。

纯函数业务层封装了十余个工具函数。getOrdersByStatus接收状态字符串,遍历ORDER_LIST返回该状态的所有订单,是看板布局的核心数据源。getItemsText将商品列表拼接为"商品名x数量、商品名x数量"格式的文本,用于订单卡片和提货卡片的商品摘要展示。getTodayOrderCountgetTodayRevenue分别统计今日订单数和流水总额。getWeekMaxRevenue遍历七日收益数据返回最大值,用于柱状图的高度归一化计算。getMemberCountByLevel按等级统计团员数量。maskPhone将手机号中间四位替换为星号,保护团员隐私。getHotCount统计爆款商品数量。getPickupPendingCount统计待核销提货数量。getBarHeight根据金额和最大值计算柱状图高度,设置了12vp的最小高度防零值。getStockPercent将库存数转换为百分比用于库存进度条。

四、公共头部构建器与纯函数封装

应用提取了一个全局公共的@Builder函数pageHeader,作为六大页面的统一头部组件。这种全局@Builder的设计实现了真正的跨组件复用,任何@Component都可以直接调用该函数,无需通过props传递。

function getOrdersByStatus(status: string): OrderInfo[] {
  let result: OrderInfo[] = []
  for (let i = 0; i < ORDER_LIST.length; i++) {
    if (ORDER_LIST[i].status === status) {
      result.push(ORDER_LIST[i])
    }
  }
  return result
}

function getItemsText(items: OrderItemInfo[]): string {
  let parts: string[] = []
  for (let i = 0; i < items.length; i++) {
    parts.push(items[i].itemName + ' x' + items[i].qty)
  }
  return parts.join('、')
}

function getTodayRevenue(): number {
  let total = 0.0
  for (let i = 0; i < ORDER_LIST.length; i++) {
    total += ORDER_LIST[i].totalAmount
  }
  return Math.round(total * 10) / 10
}

function maskPhone(phone: string): string {
  if (phone.length < 7) {
    return phone
  }
  return phone.substring(0, 3) + '****' + phone.substring(7)
}

function getBarHeight(amount: number, maxVal: number): number {
  if (maxVal <= 0) {
    return 12
  }
  let ratio = amount / maxVal
  if (ratio < 0.08) {
    return 12
  }
  return Math.round(ratio * 120)
}

function getStockPercent(stock: number): number {
  if (stock >= 100) {
    return 100
  }
  return stock
}

// ---------------- Tab 枚举 ----------------
enum MainTab { Orders = 0, Products = 1, Members = 2, Pickup = 3, Revenue = 4, Leader = 5 }

// ---------------- 通用Builder: 社区电商头部 ----------------
@Builder function pageHeader(title: string, subtitle: string, orderCount: string, revenue: string) {
  Column() {
    Row() {
      Column() {
        Text(title).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
        Text(subtitle).fontSize(12).fontColor('#FFE0B2').margin({ top: 4 })
      }.alignItems(HorizontalAlign.Start)
      Blank()
      Column() {
        Text('今日订单').fontSize(10).fontColor('#FFE0B2')
        Text(orderCount).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
      }.alignItems(HorizontalAlign.End)
      Column() {
        Text('今日流水').fontSize(10).fontColor('#FFE0B2')
        Text('¥' + revenue).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
      }.alignItems(HorizontalAlign.End)
    }.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
    Row() {
      Text('🔍 搜索订单 / 团员 / 商品').fontSize(13).fontColor(COLORS.subText).padding({ left: 12 })
      Blank()
      Text('筛选').fontSize(13).fontColor(COLORS.primary).padding({ right: 12 })
    }.width('100%').height(38).backgroundColor(COLORS.white).borderRadius(19).margin({ left: 16, right: 16, bottom: 12 })
  }.width('100%').backgroundColor(COLORS.primary).borderRadius({ bottomLeft: 20, bottomRight: 20 })
}

在这里插入图片描述

纯函数的设计体现了函数式编程的核心理念。getOrdersByStatus采用传统的for循环遍历,返回新数组而非修改原数组,保证了数据的不可变性。getItemsText使用数组的join方法拼接文本,简洁高效。getTodayRevenue通过Math.round(total * 10) / 10实现了一位小数精度的金额计算,避免了浮点数精度问题。maskPhone通过substring截取前3位和后4位,中间替换为四个星号,在保护隐私的同时保留了号码的可辨识度。

getBarHeight是柱状图渲染的关键函数。它接收当前金额和最大值两个参数,计算比例后乘以120得到像素高度。特别处理了两个边界条件:当最大值为0时返回12vp防止除零错误,当比例小于8%时返回12vp的最小高度,确保小金额柱子也有可见高度。getStockPercent将库存数映射为0-100的百分比值,当库存大于等于100时直接返回100,用于商品卡片的库存进度条展示。

pageHeader是一个全局@Builder函数,接收标题、副标题、订单数和流水四个参数。头部采用主色橙色背景,左侧是标题和副标题,右侧是今日订单数和今日流水两个统计区块。下方是一个圆角搜索栏,通过borderRadius(19)实现胶囊形状。整个头部设置了borderRadius({ bottomLeft: 20, bottomRight: 20 })的底部圆角,使头部与内容区域形成柔和的过渡。MainTab枚举定义了六个标签页的索引,使用语义化命名使路由逻辑一目了然。

五、订单管理看板与多状态列布局

订单管理页面采用了看板式(Kanban)布局,将不同状态的订单分列展示,支持横向滚动浏览。这种布局源自敏捷开发中的看板理念,非常适合需要按状态分类管理的业务场景。页面还配备了新增订单弹框和订单详情弹框两个模态交互。

@Component
struct OrdersPage {
  @State showNewOrderDialog: boolean = false
  @State showOrderDetail: boolean = false
  @State selectedOrder: OrderInfo | null = null

  @Builder modalOverlay(onClose: () => void) {
    Column() {
      Blank()
      Column() {
        Text('点击空白处关闭').fontSize(12).fontColor('#FFFFFF').onClick(() => { onClose() })
      }
    }.width('100%').height('100%').backgroundColor('rgba(62,39,35,0.5)').justifyContent(FlexAlign.End).onClick(() => { onClose() })
  }

  @Builder statusColumn(title: string, color: string, count: number) {
    Column() {
      Row() {
        Circle({ width: 8, height: 8 }).fill(color)
        Text(title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
        Text(count + '单').fontSize(11).fontColor(COLORS.subText)
      }.width('100%').padding({ left: 10, right: 10, top: 10, bottom: 6 })
      List() {
        ForEach(getOrdersByStatus(title), (order: OrderInfo) => {
          ListItem() {
            Column() {
              Row() {
                Text(order.orderNo).fontSize(10).fontColor(COLORS.subText)
                Blank()
                Text('¥' + order.totalAmount.toFixed(2)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
              }.width('100%')
              Row() {
                Text(order.memberName).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                Text(maskPhone(order.phone)).fontSize(11).fontColor(COLORS.subText)
              }.width('100%').margin({ top: 6 })
              Text(getItemsText(order.items)).fontSize(11).fontColor(COLORS.subText).maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 4 })
              Row() {
                Text(order.pickupCode).fontSize(11).fontColor(COLORS.secondary).fontWeight(FontWeight.Bold)
                Blank()
                Text('详情 >').fontSize(11).fontColor(COLORS.primary)
              }.width('100%').margin({ top: 8 })
            }.width('100%').padding(10).backgroundColor(COLORS.white).borderRadius(12).margin({ bottom: 8 })
            .onClick(() => { this.selectedOrder = order; this.showOrderDetail = true })
          }
        }, (order: OrderInfo) => order.id.toString())
      }.layoutWeight(1).width('100%')
    }.width(170).backgroundColor('#FFF3E0').borderRadius(14).margin({ left: 6, right: 6 }).alignItems(HorizontalAlign.Start)
  }

  build() {
    Stack() {
      Column() {
        pageHeader('订单管理', '阳光花园 · 第32期团购', getTodayOrderCount().toString(), getTodayRevenue().toString())
        Row() {
          Text('新增订单').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
            .backgroundColor(COLORS.primary).borderRadius(16).padding({ left: 14, right: 14, top: 7, bottom: 7 })
            .onClick(() => { this.showNewOrderDialog = true })
          Blank()
          Text('今日: ' + getTodayOrderCount() + '单 / ¥' + getTodayRevenue().toString()).fontSize(12).fontColor(COLORS.subText)
        }.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 6 })
        Scroll() {
          Row() {
            this.statusColumn('待确认', STATUS_COLORS['待确认'], STATUS_STATS['待确认'])
            this.statusColumn('备货中', STATUS_COLORS['备货中'], STATUS_STATS['备货中'])
            this.statusColumn('待提货', STATUS_COLORS['待提货'], STATUS_STATS['待提货'])
            this.statusColumn('已完成', STATUS_COLORS['已完成'], STATUS_STATS['已完成'])
          }
        }.scrollable(ScrollDirection.Horizontal).layoutWeight(1).width('100%').padding({ left: 10, right: 10, bottom: 10 })
      }.width('100%').height('100%')
      if (this.showNewOrderDialog) {
        this.modalOverlay(() => { this.showNewOrderDialog = false })
        this.newOrderDialog()
      }
      if (this.showOrderDetail) {
        this.modalOverlay(() => { this.showOrderDetail = false })
        this.orderDetailDialog()
      }
    }.width('100%').height('100%')
  }
}

在这里插入图片描述

看板列statusColumn是该页面的核心构建器。每列固定宽度170vp,背景使用浅橙色#FFF3E0与页面背景形成区分。列头部使用Circle组件绘制8x8的状态色圆点,配合状态标题和订单数。列体使用List容器渲染该状态的订单卡片,通过getOrdersByStatus(title)纯函数获取对应状态的订单列表。每个订单卡片包含订单号、金额(主色橙色加粗)、团员姓名、掩码手机号、商品摘要(最多两行,超出省略号)、提货码和"详情>"链接。

看板整体通过Scroll容器配合scrollable(ScrollDirection.Horizontal)实现横向滚动。四列分别对应待确认、备货中、待提货和已完成四种状态,颜色和数量均从STATUS_COLORS和STATUS_STATS常量中读取。每列内的List设置了layoutWeight(1)使其占据列的剩余高度,实现列内纵向滚动。订单卡片的onClick将选中的订单赋值给selectedOrder状态并打开展示弹框,实现了看板到详情的导航链路。

modalOverlay是该应用所有页面通用的遮罩构建器模式。它使用半透明棕色rgba(62,39,35,0.5)覆盖全屏,底部居中放置"点击空白处关闭"提示文字。Blank()组件撑开上方空间使提示文字推到底部。整个遮罩设置了onClick(onClose),点击任意位置即可关闭弹框。这种设计在六个页面中被重复使用,但每个页面通过独立的@Builder定义,保持了组件的独立性。

六、商品上架双列网格与编辑删除弹框

商品上架页面采用了双列瀑布流网格布局展示在售商品,配合Flex的wrap换行属性实现自动排列。每张商品卡片包含商品图标、名称、价格对比、库存进度条和操作按钮。页面还设计了编辑商品弹框和删除确认弹框两种交互。

@Component
struct ProductsPage {
  @State showEditDialog: boolean = false
  @State showDeleteDialog: boolean = false
  @State selectedProduct: ProductInfo | null = null

  build() {
    Stack() {
      Column() {
        pageHeader('商品上架', '在售商品 · 第32期团购', PRODUCT_LIST.length.toString(), '8930')
        Row() {
          Text('全部 (' + PRODUCT_LIST.length + ')').fontSize(12).fontColor(COLORS.white).backgroundColor(COLORS.primary)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          Text('爆款 (' + getHotCount() + ')').fontSize(12).fontColor(COLORS.primary).backgroundColor(COLORS.white)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          Text('缺货').fontSize(12).fontColor(COLORS.danger).backgroundColor(COLORS.white)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          Blank()
          Text('+ 新增商品').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.secondary)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 }).onClick(() => { this.showEditDialog = true })
        }.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 8 })
        Scroll() {
          Column() {
            Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
              ForEach(PRODUCT_LIST, (product: ProductInfo) => {
                Column() {
                  Stack() {
                    Column() {
                      Text(product.name.substring(0, 1)).fontSize(26).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                    }.width('100%').height(80).backgroundColor(product.imageColor)
                    .borderRadius({ topLeft: 12, topRight: 12 }).justifyContent(FlexAlign.Center)
                    .constraintSize({ maxHeight: '100%' })
                    Text(product.hot ? 'HOT' : '').fontSize(9).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                      .backgroundColor(COLORS.danger).borderRadius(8)
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 }).position({ x: 8, y: 8 })
                  }.width('100%').height(80)
                  Text(product.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text).maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 8, bottom: 2 }).padding({ left: 8, right: 8 })
                  Row() {
                    Text('¥' + product.groupPrice.toFixed(2)).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
                    Text('¥' + product.originalPrice.toFixed(2)).fontSize(11).fontColor(COLORS.subText)
                      .decoration({ type: TextDecorationType.LineThrough })
                  }.width('100%').padding({ left: 8, right: 8 })
                  Row() {
                    Text('已售' + product.soldCount).fontSize(10).fontColor(COLORS.subText)
                    Blank()
                    Text('库存' + product.stock).fontSize(10).fontColor(product.stock < 30 ? COLORS.danger : COLORS.secondary)
                  }.width('100%').padding({ left: 8, right: 8, top: 4 })
                  Progress({ value: getStockPercent(product.stock), total: 100, type: ProgressType.Linear })
                    .width('90%').height(4).color(COLORS.secondary).margin({ top: 6 })
                  Row() {
                    Text(product.status).fontSize(10).fontColor(product.status === '上架' ? COLORS.secondary : COLORS.warning)
                      .backgroundColor('#E8F5E9').borderRadius(8).padding({ left: 8, right: 8, top: 2, bottom: 2 })
                    Blank()
                    Text('编辑').fontSize(11).fontColor(COLORS.primary).padding(4)
                      .onClick(() => { this.selectedProduct = product; this.showEditDialog = true })
                    Text('删除').fontSize(11).fontColor(COLORS.danger).padding(4)
                      .onClick(() => { this.selectedProduct = product; this.showDeleteDialog = true })
                  }.width('100%').padding({ left: 8, right: 8, top: 6, bottom: 10 })
                }.width('48%').backgroundColor(COLORS.white).borderRadius(12).margin({ bottom: 10 })
              }, (product: ProductInfo) => product.id.toString())
            }.width('100%').padding({ left: 16, right: 16, bottom: 12 })
          }.width('100%')
        }.layoutWeight(1).width('100%')
      }.width('100%').height('100%')
      if (this.showEditDialog) {
        this.modalOverlay(() => { this.showEditDialog = false })
        this.editProductDialog()
      }
      if (this.showDeleteDialog) {
        this.modalOverlay(() => { this.showDeleteDialog = false })
        this.deleteConfirmDialog()
      }
    }.width('100%').height('100%')
  }

  @Builder editProductDialog() {
    Column() {
      Text('编辑商品').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      Divider().margin({ top: 12, bottom: 12 })
      Row() {
        Text('商品名称').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text(this.selectedProduct?.name ?? '红富士苹果 5斤装').fontSize(13).fontColor(COLORS.text)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('团购价').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text('¥' + (this.selectedProduct?.groupPrice ?? 19.9).toFixed(2)).fontSize(13)
          .fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('库存').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text((this.selectedProduct?.stock ?? 0).toString() + ' ' + (this.selectedProduct?.unit ?? '箱'))
          .fontSize(13).fontColor(COLORS.text)
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('分类').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text(this.selectedProduct?.category ?? '生鲜').fontSize(12).fontColor(COLORS.white)
          .backgroundColor(COLORS.secondary).borderRadius(10).padding({ left: 10, right: 10, top: 3, bottom: 3 })
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('供应商').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text(this.selectedProduct?.supplier ?? '烟台果园直供').fontSize(13).fontColor(COLORS.text)
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('商品图').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Column().width(36).height(36).backgroundColor(this.selectedProduct?.imageColor ?? COLORS.primary).borderRadius(8)
      }.width('100%').padding({ top: 8, bottom: 8 })
      Row() {
        Text('取消').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.subText).backgroundColor('#FFF3E0')
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showEditDialog = false })
        Blank()
        Text('保存').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.secondary)
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showEditDialog = false })
      }.width('100%').margin({ top: 14 })
    }.width('86%').padding(20).backgroundColor(COLORS.white).borderRadius(18)
  }

  @Builder deleteConfirmDialog() {
    Column() {
      Text('⚠️').fontSize(40).margin({ top: 8, bottom: 8 })
      Text('确认删除商品?').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      Text('删除后「' + (this.selectedProduct?.name ?? '') + '」将从本期团购下架,已产生的订单不受影响。')
        .fontSize(12).fontColor(COLORS.subText).textAlign(TextAlign.Center).margin({ top: 10, bottom: 16 })
        .padding({ left: 10, right: 10 })
      Row() {
        Text('取消').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.subText).backgroundColor('#FFF3E0')
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showDeleteDialog = false })
        Text('确认删除').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.danger)
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showDeleteDialog = false })
      }.margin({ top: 4 })
    }.width('80%').padding(20).backgroundColor(COLORS.white).borderRadius(18).alignItems(HorizontalAlign.Center)
  }
}

在这里插入图片描述
商品网格使用Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween })实现自动换行双列布局。每张卡片宽度为48%,通过SpaceBetween两端对齐使两列卡片之间产生自然间距。卡片采用上下结构:顶部是80vp高的商品图标区,使用Stack容器叠加图标文字和"HOT"角标。图标文字取商品名称的首字符product.name.substring(0, 1),通过商品颜色product.imageColor作为背景色,视觉上形成了色彩丰富的商品网格。

卡片中部是商品信息区,包含名称(单行+省略号)、团购价与原价对比(原价使用删除线)、销量与库存(库存低于30时使用红色警告色)。库存进度条使用了ArkTS内置的Progress组件,设置ProgressType.Linear线性类型,通过getStockPercent(product.stock)函数计算进度值。卡片底部是操作区,左侧是上架状态标签(浅绿背景),右侧是"编辑"和"删除"两个文字按钮,点击分别打开展示弹框和删除确认弹框。

编辑弹框editProductDialog采用了表单式布局,每行使用Row-Text-Blank-Text结构展示标签和值。所有值都通过this.selectedProduct?.字段 ?? 默认值的可选链安全访问。删除确认弹框deleteConfirmDialog设计了警示性的图标、确认标题和说明文案,双按钮分别使用浅橙色和红色背景,视觉上区分了取消和确认操作的语义权重。

流程图

Orders=0

Products=1

Members=2

Pickup=3

Revenue=4

Leader=5

点击新增

点击订单

点击编辑

点击删除

点击团员

点击核销

点击提现

确认下单

关闭

保存

确认删除

确认核销

确认提现

应用启动 GroupBuyingApp入口

初始化 currentTab = 0

渲染内容区域

订单管理 OrdersPage

商品上架 ProductsPage

团员管理 MembersPage

提货核销 PickupPage

收益统计 RevenuePage

团长中心 LeaderPage

公共头部 pageHeader

横向滚动看板

待确认列

备货中列

待提货列

已完成列

新增订单弹框

订单详情弹框

筛选标签栏

Flex双列网格

编辑商品弹框

删除确认弹框

等级统计栏

团员列表

团员详情弹框

扫码核销入口

核销码列表

核销确认弹框

统计卡片网格

每日收益柱状图

分类销售额条形图

会员等级分布

每日明细列表

团长个人卡片

结算提现卡片

本期数据统计

设置菜单列表

结算提现弹框

七、团员管理与提货核销模块

团员管理页面采用列表式布局展示团员信息,配合等级统计栏和团员详情弹框。提货核销页面设计了扫码核销入口、核销码列表和核销确认弹框,构建了完整的提货验证流程。

@Component
struct MembersPage {
  @State showMemberDialog: boolean = false
  @State selectedMember: MemberInfo | null = null

  build() {
    Stack() {
      Column() {
        pageHeader('团员管理', '阳光花园 · 团员总数', MEMBER_LIST.length.toString(), '2860')
        Row() {
          ForEach(['钻石', '金卡', '银卡', '普通'], (levelName: string) => {
            Column() {
              Text(getMemberCountByLevel(levelName).toString()).fontSize(18).fontWeight(FontWeight.Bold)
                .fontColor(LEVEL_COLORS[levelName])
              Text(levelName).fontSize(11).fontColor(COLORS.subText)
            }.layoutWeight(1)
          }, (levelName: string) => levelName)
        }.width('100%').backgroundColor(COLORS.white).borderRadius(14)
        .padding({ top: 12, bottom: 12 }).margin({ left: 16, right: 16, top: 10 })
        Scroll() {
          Column() {
            ForEach(MEMBER_LIST, (member: MemberInfo) => {
              Row() {
                Stack() {
                  Column().width(48).height(48).borderRadius(24).backgroundColor(member.avatarColor)
                  Text(member.name.substring(0, 1)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                }.width(48).height(48)
                Column() {
                  Row() {
                    Text(member.name).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                    Text(member.level).fontSize(10).fontColor(COLORS.white).backgroundColor(LEVEL_COLORS[member.level])
                      .borderRadius(8).padding({ left: 8, right: 8, top: 2, bottom: 2 })
                  }.width('100%')
                  Text(maskPhone(member.phone) + ' · ' + member.address).fontSize(11).fontColor(COLORS.subText).margin({ top: 3 })
                  Text('最近下单: ' + member.lastOrder).fontSize(10).fontColor(COLORS.subText).margin({ top: 2 })
                }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
                Column() {
                  Text(member.orderCount + '单').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
                  Text('¥' + member.totalSpent.toFixed(0)).fontSize(11).fontColor(COLORS.subText).margin({ top: 2 })
                }.alignItems(HorizontalAlign.End)
              }.width('100%').padding(12).backgroundColor(COLORS.white).borderRadius(14).margin({ bottom: 8 })
              .onClick(() => { this.selectedMember = member; this.showMemberDialog = true })
            }, (member: MemberInfo) => member.id.toString())
          }.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 12 })
        }.layoutWeight(1).width('100%')
      }.width('100%').height('100%')
      if (this.showMemberDialog) {
        this.modalOverlay(() => { this.showMemberDialog = false })
        this.memberDetailDialog()
      }
    }.width('100%').height('100%')
  }
}

团员管理页面的等级统计栏使用ForEach渲染四个等级(钻石、金卡、银卡、普通),每个等级通过getMemberCountByLevel函数统计数量,颜色从LEVEL_COLORS常量读取。团员列表的每行采用三段式布局:左侧是48x48的圆形头像,使用Stack叠加背景色Column和姓名首字符Text;中部是团员信息区,包含姓名+等级徽章、掩码手机号+住址、最近下单时间;右侧是订单数和消费金额统计。

等级徽章是一个亮点设计:通过LEVEL_COLORS[member.level]获取对应等级颜色作为背景色,白色文字显示等级名称,borderRadius(8)和紧凑的padding形成小尺寸胶囊标签。这种设计让等级信息在列表中一目了然,同时色彩区分度极高。

提货核销页面是该应用最具业务特色的模块之一:

@Component
struct PickupPage {
  @State showVerifyDialog: boolean = false
  @State selectedPickup: PickupInfo | null = null

  build() {
    Stack() {
      Column() {
        pageHeader('提货核销', '今日待核销 ' + getPickupPendingCount() + ' 件', PICKUP_LIST.length.toString(), '305')
        Row() {
          Text('📷 扫码核销').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.primary)
            .borderRadius(24).padding({ left: 30, right: 30, top: 12, bottom: 12 })
            .onClick(() => { this.selectedPickup = PICKUP_LIST[0]; this.showVerifyDialog = true })
        }.width('100%').justifyContent(FlexAlign.Center).margin({ top: 12, bottom: 10 })
        Scroll() {
          Column() {
            ForEach(PICKUP_LIST, (pickup: PickupInfo) => {
              Row() {
                Column() {
                  Text('▣').fontSize(24).fontWeight(FontWeight.Bold)
                    .fontColor(pickup.verified ? COLORS.secondary : COLORS.primary)
                  Text(pickup.verified ? '已核销' : '扫码').fontSize(9).fontColor(COLORS.subText).margin({ top: 4 })
                }.width(56).height(64).backgroundColor('#FFF3E0').borderRadius(12).justifyContent(FlexAlign.Center)
                Column() {
                  Row() {
                    Text(pickup.memberName).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                    Text(pickup.pickupCode).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary).letterSpacing(1.5)
                    Blank()
                    Circle({ width: 8, height: 8 })
                      .fill(pickup.status === '待核销' ? COLORS.warning : (pickup.status === '已核销' ? COLORS.secondary : COLORS.border))
                  }.width('100%')
                  Text(getItemsText(pickup.items)).fontSize(11).fontColor(COLORS.subText).maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 4 })
                  Row() {
                    Text(pickup.pickupDate + ' · ' + pickup.pickupLocation).fontSize(10).fontColor(COLORS.subText)
                    Blank()
                    Text('¥' + pickup.amount.toFixed(2)).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                  }.width('100%').margin({ top: 4 })
                }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
                Text(pickup.status === '待核销' ? '核销' : '查看').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                  .backgroundColor(pickup.status === '待核销' ? COLORS.secondary : '#BDBDBD').borderRadius(14)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                  .onClick(() => { this.selectedPickup = pickup; this.showVerifyDialog = true })
              }.width('100%').padding(12).backgroundColor(COLORS.white).borderRadius(14).margin({ bottom: 8 })
            }, (pickup: PickupInfo) => pickup.id.toString())
          }.width('100%').padding({ left: 16, right: 16, bottom: 12 })
        }.layoutWeight(1).width('100%')
      }.width('100%').height('100%')
      if (this.showVerifyDialog) {
        this.modalOverlay(() => { this.showVerifyDialog = false })
        this.verifyConfirmDialog()
      }
    }.width('100%').height('100%')
  }

  @Builder verifyConfirmDialog() {
    Column() {
      Text('核销确认').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text).margin({ top: 8 })
      Stack() {
        Column().width(120).height(120).backgroundColor(COLORS.white).borderRadius(12)
          .border({ width: 2, color: COLORS.primary })
        Column() {
          Row() {
            Column().width(10).height(10).backgroundColor(COLORS.text)
            Column().width(10).height(10).backgroundColor(COLORS.text)
            Column().width(10).height(10).backgroundColor(COLORS.text)
          }.margin({ top: 10 })
          Row() {
            Column().width(10).height(10).backgroundColor(COLORS.text)
            Column().width(10).height(10).backgroundColor(COLORS.text)
          }.margin({ top: 6 })
          Row() {
            Column().width(10).height(10).backgroundColor(COLORS.text)
            Column().width(10).height(10).backgroundColor(COLORS.text)
            Column().width(10).height(10).backgroundColor(COLORS.text)
          }.margin({ top: 6, bottom: 10 })
        }.width(120).height(120).justifyContent(FlexAlign.Center)
      }.width(120).height(120).margin({ top: 16, bottom: 12 })
      Text(this.selectedPickup?.pickupCode ?? '').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.primary).letterSpacing(3)
      Text((this.selectedPickup?.memberName ?? '') + ' · ¥' + (this.selectedPickup?.amount ?? 0).toFixed(2))
        .fontSize(13).fontColor(COLORS.subText).margin({ top: 6 })
      Text(getItemsText(this.selectedPickup?.items ?? [])).fontSize(12).fontColor(COLORS.subText).margin({ top: 4 })
      Row() {
        Text('取消').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.subText).backgroundColor('#FFF3E0')
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showVerifyDialog = false })
        Text('确认核销').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.secondary)
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showVerifyDialog = false })
      }.margin({ top: 20, bottom: 8 })
    }.width('86%').padding(20).backgroundColor(COLORS.bg).borderRadius(18).alignItems(HorizontalAlign.Center)
  }
}

在这里插入图片描述

提货核销页面的顶部"扫码核销"按钮使用大号圆角设计borderRadius(24)和较大的padding,视觉上突出核心操作。核销列表的每行采用左侧二维码图标区+中间信息区+右侧操作按钮的三段式结构。左侧56x64的图标区使用"▣"符号表示二维码,颜色根据核销状态在主色和绿色间切换。中间信息区包含团员姓名+提货码(带letterSpacing(1.5)字间距)+状态指示圆点+商品摘要+提货日期和地点。右侧操作按钮的文字根据状态变化为"核销"或"查看"。

核销确认弹框verifyConfirmDialog是该应用最独特的UI设计。它使用Stack容器叠加了一个120x120的二维码图形模拟区——外层是带边框的白色圆角容器,内层通过多个Column().width(10).height(10)的小方块拼接出一个二维码图案。虽然这是静态模拟而非真正的二维码渲染,但视觉上极具辨识度,展现了ArkTS用基础组件拼装复杂图形的能力。提货码使用letterSpacing(3)增加字间距,增强了"验证码"的视觉特征。

八、收益统计仪表盘与数据可视化

收益统计页面是该应用数据可视化的集中展现模块,包含了统计卡片网格、每日收益柱状图、分类销售额横向条形图、会员等级分布、订单状态分布和每日明细列表六个数据展示区域。全部通过纯ArkTS组件实现,无需任何第三方图表库。

@Component
struct RevenuePage {
  build() {
    Stack() {
      Column() {
        pageHeader('收益统计', '本周收益概览', '207', '10246.7')
        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(REVENUE_CARDS, (card: StatCardInfo) => {
            Column() {
              Text(card.title).fontSize(11).fontColor(COLORS.subText)
              Text(card.value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(card.color).margin({ top: 4 })
              Text(card.trend).fontSize(10).fontColor(COLORS.secondary).margin({ top: 2 })
            }.width('48%').padding(12).backgroundColor(COLORS.white).borderRadius(14)
            .margin({ bottom: 8 }).alignItems(HorizontalAlign.Start)
          }, (card: StatCardInfo) => card.title)
        }.width('100%').padding({ left: 16, right: 16, top: 10 })
        Scroll() {
          Column() {
            Column() {
              Text('每日收益 (近7天)').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text).width('100%')
              Row() {
                ForEach(DAILY_REVENUE, (day: DailyRevenueInfo) => {
                  Column() {
                    Column().height(14)
                    Column().width(18).height(getBarHeight(day.amount, getWeekMaxRevenue()))
                      .backgroundColor(day.amount > 1500 ? COLORS.primary : COLORS.secondary).borderRadius(4)
                    Text(day.date.substring(3)).fontSize(9).fontColor(COLORS.subText).margin({ top: 4 })
                  }.layoutWeight(1).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.End)
                }, (day: DailyRevenueInfo) => day.date)
              }.width('100%').height(160).alignItems(VerticalAlign.Bottom).margin({ top: 12 })
            }.width('100%').padding(14).backgroundColor(COLORS.white).borderRadius(14).margin({ bottom: 10 })
            Column() {
              Text('分类销售额占比').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text).width('100%')
              ForEach(CATEGORY_REVENUE, (cat: CategoryRevenueInfo) => {
                Column() {
                  Row() {
                    Text(cat.category).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                    Blank()
                    Text('¥' + cat.amount.toFixed(0)).fontSize(12).fontColor(COLORS.subText)
                    Text(cat.percent + '%').fontSize(12).fontWeight(FontWeight.Bold).fontColor(cat.barColor)
                  }.width('100%')
                  Row() {
                    Column().width(cat.percent + '%').height(10).backgroundColor(cat.barColor).borderRadius(5)
                    Blank()
                  }.width('100%').margin({ top: 4, bottom: 8 })
                }.width('100%')
              }, (cat: CategoryRevenueInfo) => cat.category)
            }.width('100%').padding(14).backgroundColor(COLORS.white).borderRadius(14).margin({ bottom: 10 })
            Row() {
              Column() {
                Text('会员等级分布').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text).width('100%')
                Row() {
                  ForEach(['钻石', '金卡', '银卡', '普通'], (levelName: string) => {
                    Column() {
                      Text(LEVEL_STATS[levelName].toString()).fontSize(16).fontWeight(FontWeight.Bold).fontColor(LEVEL_COLORS[levelName])
                      Text(levelName).fontSize(10).fontColor(COLORS.subText)
                    }.layoutWeight(1)
                  }, (levelName: string) => levelName)
                }.width('100%').margin({ top: 12 })
              }.layoutWeight(1).padding(12).backgroundColor(COLORS.white).borderRadius(14)
              Column() {
                Text('订单状态分布').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text).width('100%')
                ForEach(['待确认', '备货中', '待提货', '已完成'], (statusName: string) => {
                  Column() {
                    Row() {
                      Text(statusName).fontSize(10).fontColor(COLORS.subText)
                      Blank()
                      Text(STATUS_STATS[statusName] + '单').fontSize(10).fontColor(COLORS.text)
                    }.width('100%')
                    Row() {
                      Column().width(Math.round(STATUS_STATS[statusName] / ORDER_LIST.length * 100) + '%')
                        .height(6).backgroundColor(STATUS_COLORS[statusName]).borderRadius(3)
                      Blank()
                    }.width('100%').margin({ top: 2, bottom: 5 })
                  }.width('100%')
                }, (statusName: string) => statusName)
              }.layoutWeight(1).padding(12).backgroundColor(COLORS.white).borderRadius(14)
            }.width('100%').margin({ bottom: 10 })
            Column() {
              Text('每日明细').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text).width('100%')
              ForEach(DAILY_REVENUE, (day: DailyRevenueInfo) => {
                Row() {
                  Text(day.date).fontSize(12).fontColor(COLORS.text)
                  Blank()
                  Text(day.orderCount + '单').fontSize(11).fontColor(COLORS.subText)
                  Text('¥' + day.amount.toFixed(1)).fontSize(12).fontColor(COLORS.text).fontWeight(FontWeight.Bold)
                  Text('佣金¥' + day.commission.toFixed(1)).fontSize(11).fontColor(COLORS.secondary)
                }.width('100%').padding({ top: 8, bottom: 2 })
              }, (day: DailyRevenueInfo) => day.date)
              Divider().margin({ top: 6, bottom: 6 })
              Row() {
                Text('本周合计').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                Blank()
                Text('¥10246.7 / 佣金¥1019.6').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
              }.width('100%')
            }.width('100%').padding(14).backgroundColor(COLORS.white).borderRadius(14).margin({ bottom: 12 })
          }.width('100%').padding({ left: 16, right: 16 })
        }.layoutWeight(1).width('100%')
      }.width('100%').height('100%')
    }.width('100%').height('100%')
  }
}

在这里插入图片描述

统计卡片网格使用Flex双列布局渲染四张卡片,每张卡片包含标题、数值和趋势信息,数值颜色由card.color指定,趋势使用绿色COLORS.secondary表示正向变化。每日收益柱状图是该页面的核心可视化组件。它使用ForEach渲染七根柱子,每根柱子通过getBarHeight(day.amount, getWeekMaxRevenue())函数计算高度,颜色根据金额是否超过1500在主色橙色和辅助色绿色间切换。容器设置了alignItems(VerticalAlign.Bottom)使柱子从底部对齐,配合height(160)的固定高度形成了标准的柱状图效果。

分类销售额横向条形图使用了与进度条类似的设计模式。每个分类行包含标签、金额、百分比和一条横向进度条,进度条宽度设为cat.percent + '%',颜色使用cat.barColor。这种"标签+数值+百分比+条形"的四元素组合在数据仪表盘中非常经典。会员等级分布和订单状态分布并排展示,使用RowlayoutWeight(1)实现等分。订单状态分布的进度条宽度通过Math.round(STATUS_STATS[statusName] / ORDER_LIST.length * 100) + '%'动态计算,实现了基于数据的自适应渲染。

九、团长中心与结算提现弹框

团长中心是应用的个人中心页面,集成了团长信息卡片、结算提现卡片、本期数据统计和设置菜单列表四个功能区。其中结算提现弹框是涉及金融操作的关键交互模块。

@Component
struct LeaderPage {
  @State showSettleDialog: boolean = false

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Stack() {
              Column().width(60).height(60).borderRadius(30).backgroundColor(COLORS.secondary)
              Text(LEADER_PROFILE.name.substring(0, 1)).fontSize(24).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
            }.width(60).height(60)
            Column() {
              Row() {
                Text(LEADER_PROFILE.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                Text(LEADER_PROFILE.level).fontSize(10).fontColor(COLORS.white).backgroundColor('rgba(255,255,255,0.3)')
                  .borderRadius(8).padding({ left: 8, right: 8, top: 2, bottom: 2 })
              }.width('100%')
              Text(LEADER_PROFILE.communityName).fontSize(12).fontColor('#FFE0B2').margin({ top: 4 })
              Text('⭐ ' + LEADER_PROFILE.rating + ' · ' + maskPhone(LEADER_PROFILE.phone)).fontSize(11).fontColor('#FFE0B2').margin({ top: 2 })
            }.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
          }.width('100%')
          Row() {
            Column() {
              Text('¥' + LEADER_PROFILE.totalRevenue.toFixed(0)).fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
              Text('累计销售额').fontSize(10).fontColor('#FFE0B2').margin({ top: 2 })
            }.layoutWeight(1)
            Column() {
              Text('¥' + LEADER_PROFILE.totalCommission.toFixed(0)).fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
              Text('累计佣金').fontSize(10).fontColor('#FFE0B2').margin({ top: 2 })
            }.layoutWeight(1)
            Column() {
              Text(LEADER_PROFILE.memberCount.toString()).fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
              Text('团员数').fontSize(10).fontColor('#FFE0B2').margin({ top: 2 })
            }.layoutWeight(1)
          }.width('100%').margin({ top: 14 })
          Row() {
            ForEach(LEADER_PROFILE.badges, (badge: string) => {
              Text('🏅' + badge).fontSize(10).fontColor(COLORS.white).backgroundColor('rgba(255,255,255,0.25)')
                .borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ right: 6 })
            }, (badge: string) => badge)
          }.width('100%').margin({ top: 12 })
        }.width('100%').padding(16).backgroundColor(COLORS.primary)
        .borderRadius({ bottomLeft: 20, bottomRight: 20 }).alignItems(HorizontalAlign.Start)
        Scroll() {
          Column() {
            Column() {
              Row() {
                Text('可提现佣金').fontSize(13).fontColor(COLORS.subText)
                Blank()
                Text('待结算 ¥286.0').fontSize(11).fontColor(COLORS.warning)
              }.width('100%')
              Row() {
                Text('¥1019.60').fontSize(26).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
                Blank()
                Text('立即提现').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.secondary)
                  .borderRadius(18).padding({ left: 20, right: 20, top: 8, bottom: 8 }).onClick(() => { this.showSettleDialog = true })
              }.width('100%').margin({ top: 8 })
              Divider().margin({ top: 12, bottom: 8 })
              Row() {
                Text('本月结算 2 笔').fontSize(11).fontColor(COLORS.subText)
                Blank()
                Text('最近: 08-15 已到账 ¥520.0').fontSize(11).fontColor(COLORS.secondary)
              }.width('100%')
            }.width('100%').padding(16).backgroundColor(COLORS.white).borderRadius(14).margin({ top: 12 })
            Row() {
              Column() {
                Text('32').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.blue)
                Text('开团期数').fontSize(10).fontColor(COLORS.subText).margin({ top: 2 })
              }.layoutWeight(1)
              Column() {
                Text('98%').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.secondary)
                Text('履约率').fontSize(10).fontColor(COLORS.subText).margin({ top: 2 })
              }.layoutWeight(1)
              Column() {
                Text('4.9').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.warning)
                Text('服务评分').fontSize(10).fontColor(COLORS.subText).margin({ top: 2 })
              }.layoutWeight(1)
            }.width('100%').padding(14).backgroundColor(COLORS.white).borderRadius(14).margin({ top: 10 })
            Column() {
              ForEach(LEADER_MENU, (menu: MenuInfo) => {
                Column() {
                  Row() {
                    Text(menu.icon).fontSize(18)
                    Text(menu.title).fontSize(14).fontColor(COLORS.text)
                    Blank()
                    Text(menu.hint).fontSize(12).fontColor(COLORS.subText)
                  }.width('100%').padding({ top: 14, bottom: 14 })
                  Divider()
                }.width('100%')
              }, (menu: MenuInfo) => menu.title)
            }.width('100%').padding({ left: 16, right: 16 }).backgroundColor(COLORS.white)
            .borderRadius(14).margin({ top: 10, bottom: 12 })
          }.width('100%').padding({ left: 16, right: 16 })
        }.layoutWeight(1).width('100%')
      }.width('100%').height('100%')
      if (this.showSettleDialog) {
        this.modalOverlay(() => { this.showSettleDialog = false })
        this.settleDialog()
      }
    }.width('100%').height('100%')
  }

  @Builder settleDialog() {
    Column() {
      Text('结算提现').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      Divider().margin({ top: 12, bottom: 12 })
      Column() {
        Text('可提现金额').fontSize(12).fontColor(COLORS.subText)
        Text('¥1019.60').fontSize(28).fontWeight(FontWeight.Bold).fontColor(COLORS.primary).margin({ top: 4 })
      }.width('100%').alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 14 })
      Text('选择到账账户').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text).width('100%')
      ForEach(BANK_ACCOUNTS, (bank: BankAccountInfo) => {
        Row() {
          Column() {
            Text(bank.bankName).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
            Text(bank.cardNo + ' · ' + bank.holder).fontSize(11).fontColor(COLORS.subText).margin({ top: 2 })
          }.alignItems(HorizontalAlign.Start).layoutWeight(1)
          Blank()
          Circle({ width: 16, height: 16 }).fill(bank.id === 1 ? COLORS.secondary : COLORS.border)
        }.width('100%').padding({ top: 10, bottom: 10 })
        .backgroundColor(bank.id === 1 ? '#F1F8E9' : COLORS.bg).borderRadius(10).margin({ top: 6 })
      }, (bank: BankAccountInfo) => bank.id.toString())
      Row() {
        Text('手续费').fontSize(12).fontColor(COLORS.subText)
        Blank()
        Text('¥0 (团长免手续费)').fontSize(12).fontColor(COLORS.secondary)
      }.width('100%').margin({ top: 12 })
      Row() {
        Text('取消').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.subText).backgroundColor('#FFF3E0')
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showSettleDialog = false })
        Text('确认提现 ¥1019.60').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.primary)
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showSettleDialog = false })
      }.width('100%').margin({ top: 16 })
    }.width('86%').padding(20).backgroundColor(COLORS.white).borderRadius(18)
  }
}

团长个人卡片是该页面的视觉焦点。橙色背景的头部区域包含三个层次的信息:最上层是60x60的圆形头像(绿色背景+白色首字)和右侧的姓名、等级徽章、社区名称、评分和掩码手机号;中间层是累计销售额、累计佣金和团员数的三等分统计区;最底层是通过ForEach渲染的徽章列表,每个徽章使用半透明白色背景rgba(255,255,255,0.25)的胶囊标签,配合"🏅"图标形成荣誉展示效果。

结算提现弹框settleDialog是该应用涉及金融操作的核心交互。弹框使用ForEach渲染银行账户列表,每个账户行包含银行名称、卡号和持卡人信息。通过bank.id === 1的判断实现默认选中状态——选中账户使用浅绿色背景#F1F8E9和绿色圆形指示器COLORS.secondary,未选中账户使用普通背景和边框色圆形指示器。弹框底部展示手续费信息(团长免手续费)和双按钮,确认按钮文字包含金额"确认提现 ¥1019.60",增强了操作的安全意识。

设置菜单列表使用ForEach渲染LEADER_MENU配置,每行包含图标、标题、右侧提示文字和分隔线。这种"图标+标题+箭头提示"的列表项是个人中心页面的标准设计模式,简洁明了且信息密度适中。

十、主入口与六Tab底部导航架构

主入口组件GroupBuyingApp负责六大页面的路由管理和底部导航控制。通过@State状态驱动的条件渲染实现页面切换,底部导航使用ForEach渲染Tab项,实现了数据驱动的导航配置。

@Entry
@Component
struct GroupBuyingApp {
  @State currentTab: number = 0

  build() {
    Column() {
      Stack() {
        Column() {
          if (this.currentTab === MainTab.Orders) {
            OrdersPage()
          } else if (this.currentTab === MainTab.Products) {
            ProductsPage()
          } else if (this.currentTab === MainTab.Members) {
            MembersPage()
          } else if (this.currentTab === MainTab.Pickup) {
            PickupPage()
          } else if (this.currentTab === MainTab.Revenue) {
            RevenuePage()
          } else {
            LeaderPage()
          }
        }.width('100%').height('100%')
      }.layoutWeight(1).width('100%')
      Divider()
      Row() {
        ForEach(TAB_ITEMS, (tab: TabItemInfo) => {
          Column() {
            Text(tab.icon).fontSize(20).fontColor(this.currentTab === tab.index ? COLORS.primary : COLORS.subText)
            Text(tab.label).fontSize(10)
              .fontColor(this.currentTab === tab.index ? COLORS.primary : COLORS.subText).margin({ top: 2 })
          }.layoutWeight(1).onClick(() => { this.currentTab = tab.index })
        }, (tab: TabItemInfo) => tab.label)
      }.width('100%').height(56).backgroundColor(COLORS.white)
    }.width('100%').height('100%').backgroundColor(COLORS.bg)
  }
}

在这里插入图片描述

主入口组件的结构简洁而清晰。外层Column分为内容区和导航栏两部分,通过Divider分隔。内容区使用Stack包裹条件渲染链,根据currentTab的值渲染对应的页面组件。六个页面组件(OrdersPage、ProductsPage、MembersPage、PickupPage、RevenuePage、LeaderPage)都是独立的@Component,各自管理内部状态,实现了高内聚低耦合的组件化架构。

底部导航栏使用ForEach渲染TAB_ITEMS配置数组,每个Tab项包含图标和标签两行文字,通过this.currentTab === tab.index的三元运算设置选中态的主色和未选中态的灰色。导航栏固定高度56vp,白色背景,与内容区之间通过Divider形成视觉分隔。onClick中直接设置this.currentTab = tab.index即可切换页面,ArkTS的状态驱动机制会自动触发重新渲染。

核心技术点对比总结

技术维度 实现方式 关键API/装饰器 设计特点 适用场景
页面路由 枚举+条件渲染 enum MainTab, @State currentTab 六Tab路由,ForEach驱动导航项 多模块企业级应用底部导航
看板布局 横向Scroll+固定列 Scroll(Horizontal), List, ForEach 四状态列横向滚动,列内纵向List 订单管理、任务管理等多状态场景
双列网格 Flex换行布局 FlexWrap.Wrap, SpaceBetween 自动换行双列,宽度48%等分 商品列表、卡片网格展示
公共组件 全局@Builder函数 @Builder function, 参数传递 跨组件复用,无需props传递 统一头部、通用遮罩等共享UI
模态弹框 Stack层叠+遮罩 Stack, modalOverlay, onClick 半透明遮罩+居中卡片,点击关闭 表单、确认、详情展示
数据可视化 纯组件柱状图/条形图 ForEach, Column.height, Progress 无需图表库,基础组件拼装 收益统计、数据仪表盘
进度条 双层Column+Progress组件 Column.width(%), Progress(Linear) 百分比宽度或内置Progress 库存进度、状态分布、完成度
隐私保护 字符串截取掩码 substring, 字符串拼接 手机号中间四位替换星号 用户信息展示场景
金融交互 单选账户+金额确认 ForEach, Circle指示器, 条件背景 选中态视觉反馈,金额嵌入按钮 提现、转账等资金操作
配置管理 Record映射常量 Record<string,string/number> 状态-颜色、等级-数量集中配置 需要统一管理的映射关系

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 社区团购团长APP - HarmonyOS ArkTS
// 主题: 团购橙 #FF6F00 + 社区绿 #43A047, 背景 #FFF8E1
// ============================================================

// ---------------- 接口定义 ----------------
interface OrderItemInfo { itemName: string; qty: number }
interface OrderInfo {
  id: number; orderNo: string; memberName: string; phone: string; items: OrderItemInfo[]
  totalAmount: number; status: string; createDate: string; pickupCode: string
  pickupLocation: string; imageColor: string; note: string
}
interface ProductInfo {
  id: number; name: string; category: string; groupPrice: number; originalPrice: number
  stock: number; soldCount: number; minOrder: number; unit: string; supplier: string
  imageColor: string; status: string; hot: boolean
}
interface MemberInfo {
  id: number; name: string; phone: string; avatarColor: string; orderCount: number
  totalSpent: number; level: string; joinDate: string; lastOrder: string; address: string; note: string
}
interface PickupInfo {
  id: number; orderNo: string; memberName: string; pickupCode: string; items: OrderItemInfo[]
  amount: number; status: string; pickupDate: string; pickupLocation: string; verified: boolean
}
interface DailyRevenueInfo { date: string; amount: number; orderCount: number; commission: number }
interface CategoryRevenueInfo { category: string; amount: number; percent: number; barColor: string }
interface LeaderInfo {
  name: string; phone: string; communityName: string; totalRevenue: number; totalCommission: number
  memberCount: number; rating: number; level: string; badges: string[]
}
interface ColorPalette {
  primary: string; secondary: string; bg: string; card: string; text: string; subText: string
  warning: string; danger: string; border: string; white: string; purple: string; blue: string
}
interface BankAccountInfo { id: number; bankName: string; cardNo: string; holder: string }
interface StatCardInfo { title: string; value: string; trend: string; color: string }
interface MenuInfo { icon: string; title: string; hint: string }
interface TabItemInfo { index: number; icon: string; label: string }

// ---------------- 颜色 ----------------
const COLORS: ColorPalette = {
  primary: '#FF6F00', secondary: '#43A047', bg: '#FFF8E1', card: '#FFFFFF', text: '#3E2723',
  subText: '#8D6E63', warning: '#FF8F00', danger: '#E53935', border: '#FFE0B2', white: '#FFFFFF',
  purple: '#8E24AA', blue: '#1E88E5'
}

// ---------------- 配置 ----------------
const STATUS_COLORS: Record<string, string> = {
  '待确认': '#FB8C00', '备货中': '#1E88E5', '待提货': '#8E24AA', '已完成': '#43A047', '已取消': '#9E9E9E'
}
const PRODUCT_CATEGORIES: Record<string, string> = {
  '生鲜': '生鲜', '零食': '零食', '日用': '日用', '美妆': '美妆', '家居': '家居', '饮品': '饮品'
}
const MEMBER_LEVELS: Record<string, string> = { '普通': '普通', '银卡': '银卡', '金卡': '金卡', '钻石': '钻石' }
const LEVEL_COLORS: Record<string, string> = {
  '普通': '#9E9E9E', '银卡': '#90A4AE', '金卡': '#FFB300', '钻石': '#8E24AA'
}
const LEVEL_STATS: Record<string, number> = { '钻石': 1, '金卡': 3, '银卡': 4, '普通': 8 }
const STATUS_STATS: Record<string, number> = { '待确认': 4, '备货中': 4, '待提货': 4, '已完成': 3, '已取消': 1 }

// ---------------- 模拟数据 ----------------
const ORDER_LIST: OrderInfo[] = [
  { id: 1, orderNo: 'TG20260824001', memberName: '王丽', phone: '13812345678', items: [{ itemName: '红富士苹果', qty: 3 }, { itemName: '土鸡蛋', qty: 2 }], totalAmount: 68.5, status: '待确认', createDate: '08-24 09:15', pickupCode: 'PC824001', pickupLocation: '3号楼架空层', imageColor: '#FF6F00', note: '下班后自提' },
  { id: 2, orderNo: 'TG20260824002', memberName: '张强', phone: '13998765432', items: [{ itemName: '东北大米10kg', qty: 1 }, { itemName: '花生油5L', qty: 1 }], totalAmount: 129.9, status: '待确认', createDate: '08-24 09:32', pickupCode: 'PC824002', pickupLocation: '3号楼架空层', imageColor: '#43A047', note: '' },
  { id: 3, orderNo: 'TG20260824003', memberName: '李梅', phone: '13655557777', items: [{ itemName: '纸巾家庭装', qty: 4 }], totalAmount: 39.6, status: '待确认', createDate: '08-24 10:05', pickupCode: 'PC824003', pickupLocation: '3号楼架空层', imageColor: '#1E88E5', note: '多备一提' },
  { id: 4, orderNo: 'TG20260824004', memberName: '刘洋', phone: '15811112222', items: [{ itemName: '阳光玫瑰葡萄', qty: 2 }, { itemName: '酸奶八连杯', qty: 1 }], totalAmount: 79.8, status: '备货中', createDate: '08-24 07:48', pickupCode: 'PC824004', pickupLocation: '3号楼架空层', imageColor: '#8E24AA', note: '' },
  { id: 5, orderNo: 'TG20260824005', memberName: '陈静', phone: '18633334444', items: [{ itemName: '洗衣液2kg', qty: 2 }, { itemName: '消毒湿巾', qty: 3 }], totalAmount: 55.0, status: '备货中', createDate: '08-24 08:20', pickupCode: 'PC824005', pickupLocation: '3号楼架空层', imageColor: '#00897B', note: '凑单' },
  { id: 6, orderNo: 'TG20260824006', memberName: '赵磊', phone: '17788889999', items: [{ itemName: '小龙虾3斤装', qty: 1 }, { itemName: '冰啤酒500ml', qty: 6 }], totalAmount: 158.0, status: '备货中', createDate: '08-24 10:42', pickupCode: 'PC824006', pickupLocation: '3号楼架空层', imageColor: '#E53935', note: '周末聚餐' },
  { id: 7, orderNo: 'TG20260824007', memberName: '孙悦', phone: '13566665555', items: [{ itemName: '有机蔬菜套餐', qty: 1 }], totalAmount: 45.9, status: '备货中', createDate: '08-24 11:03', pickupCode: 'PC824007', pickupLocation: '3号楼架空层', imageColor: '#7CB342', note: '' },
  { id: 8, orderNo: 'TG20260824008', memberName: '周涛', phone: '18899990000', items: [{ itemName: '五常大米5kg', qty: 2 }, { itemName: '红枣1kg', qty: 1 }], totalAmount: 98.0, status: '待提货', createDate: '08-23 16:30', pickupCode: 'PC823008', pickupLocation: '3号楼架空层', imageColor: '#F4511E', note: '已到货' },
  { id: 9, orderNo: 'TG20260824009', memberName: '吴敏', phone: '15922223333', items: [{ itemName: '进口牛奶1L', qty: 6 }], totalAmount: 85.2, status: '待提货', createDate: '08-23 17:12', pickupCode: 'PC823009', pickupLocation: '3号楼架空层', imageColor: '#039BE5', note: '' },
  { id: 10, orderNo: 'TG20260824010', memberName: '郑华', phone: '13644445555', items: [{ itemName: '沐浴露750ml', qty: 2 }, { itemName: '洗发水500ml', qty: 1 }], totalAmount: 92.5, status: '待提货', createDate: '08-23 18:45', pickupCode: 'PC823010', pickupLocation: '3号楼架空层', imageColor: '#6D4C41', note: '尽快通知' },
  { id: 11, orderNo: 'TG20260824011', memberName: '冯雪', phone: '17855556666', items: [{ itemName: '芒果5斤', qty: 1 }], totalAmount: 29.9, status: '待提货', createDate: '08-23 19:20', pickupCode: 'PC823011', pickupLocation: '3号楼架空层', imageColor: '#FFB300', note: '' },
  { id: 12, orderNo: 'TG20260824012', memberName: '褚阳', phone: '13077778888', items: [{ itemName: '牛肉2斤', qty: 1 }, { itemName: '火锅底料', qty: 2 }], totalAmount: 128.0, status: '已完成', createDate: '08-22 15:10', pickupCode: 'PC822012', pickupLocation: '3号楼架空层', imageColor: '#D84315', note: '已提货' },
  { id: 13, orderNo: 'TG20260824013', memberName: '卫红', phone: '18512123434', items: [{ itemName: '纸尿裤L码', qty: 1 }], totalAmount: 89.0, status: '已完成', createDate: '08-22 16:00', pickupCode: 'PC822013', pickupLocation: '3号楼架空层', imageColor: '#5E35B1', note: '' },
  { id: 14, orderNo: 'TG20260824014', memberName: '蒋欣', phone: '13734345656', items: [{ itemName: '坚果礼盒', qty: 1 }, { itemName: '橙子10斤', qty: 1 }], totalAmount: 118.0, status: '已完成', createDate: '08-22 17:33', pickupCode: 'PC822014', pickupLocation: '3号楼架空层', imageColor: '#FB8C00', note: '复购客户' },
  { id: 15, orderNo: 'TG20260824015', memberName: '沈月', phone: '18698761234', items: [{ itemName: '牙膏家庭装', qty: 3 }], totalAmount: 42.0, status: '已取消', createDate: '08-22 11:25', pickupCode: 'PC822015', pickupLocation: '3号楼架空层', imageColor: '#78909C', note: '缺货退款' },
  { id: 16, orderNo: 'TG20260824016', memberName: '韩雪', phone: '15901234567', items: [{ itemName: '豆制品套餐', qty: 2 }], totalAmount: 36.0, status: '待确认', createDate: '08-24 11:50', pickupCode: 'PC824016', pickupLocation: '3号楼架空层', imageColor: '#43A047', note: '' }
]

const PRODUCT_LIST: ProductInfo[] = [
  { id: 1, name: '红富士苹果 5斤装', category: '生鲜', groupPrice: 19.9, originalPrice: 29.9, stock: 86, soldCount: 432, minOrder: 1, unit: '箱', supplier: '烟台果园直供', imageColor: '#FF6F00', status: '上架', hot: true },
  { id: 2, name: '东北大米 10kg', category: '生鲜', groupPrice: 59.9, originalPrice: 79.9, stock: 42, soldCount: 218, minOrder: 1, unit: '袋', supplier: '五常产地仓', imageColor: '#FFB300', status: '上架', hot: true },
  { id: 3, name: '土鸡蛋 30枚', category: '生鲜', groupPrice: 32.8, originalPrice: 45.0, stock: 55, soldCount: 366, minOrder: 1, unit: '盒', supplier: '农家散养基地', imageColor: '#F4511E', status: '上架', hot: false },
  { id: 4, name: '阳光玫瑰葡萄 3斤', category: '生鲜', groupPrice: 39.9, originalPrice: 59.9, stock: 28, soldCount: 154, minOrder: 1, unit: '份', supplier: '云南种植园', imageColor: '#7CB342', status: '上架', hot: true },
  { id: 5, name: '小龙虾 3斤装', category: '生鲜', groupPrice: 99.0, originalPrice: 139.0, stock: 12, soldCount: 98, minOrder: 1, unit: '份', supplier: '洪湖水产', imageColor: '#E53935', status: '缺货', hot: false },
  { id: 6, name: '坚果大礼包 1.5kg', category: '零食', groupPrice: 69.0, originalPrice: 99.0, stock: 63, soldCount: 287, minOrder: 1, unit: '盒', supplier: '沃隆代工', imageColor: '#8D6E63', status: '上架', hot: true },
  { id: 7, name: '酸奶八连杯', category: '饮品', groupPrice: 25.9, originalPrice: 35.9, stock: 120, soldCount: 512, minOrder: 2, unit: '组', supplier: '光明乳业', imageColor: '#039BE5', status: '上架', hot: false },
  { id: 8, name: '进口牛奶 1L*6', category: '饮品', groupPrice: 49.9, originalPrice: 68.0, stock: 74, soldCount: 193, minOrder: 1, unit: '箱', supplier: '澳伯顿', imageColor: '#1E88E5', status: '上架', hot: false },
  { id: 9, name: '冰啤酒 500ml*12', category: '饮品', groupPrice: 39.0, originalPrice: 52.0, stock: 36, soldCount: 145, minOrder: 1, unit: '提', supplier: '青岛啤酒', imageColor: '#FFB300', status: '上架', hot: false },
  { id: 10, name: '纸巾家庭装 24卷', category: '日用', groupPrice: 29.9, originalPrice: 39.9, stock: 210, soldCount: 876, minOrder: 1, unit: '提', supplier: '洁柔工厂店', imageColor: '#00ACC1', status: '上架', hot: true },
  { id: 11, name: '洗衣液 2kg*2', category: '日用', groupPrice: 35.9, originalPrice: 49.9, stock: 88, soldCount: 341, minOrder: 1, unit: '组', supplier: '蓝月亮', imageColor: '#3949AB', status: '上架', hot: false },
  { id: 12, name: '消毒湿巾 80片*3', category: '日用', groupPrice: 19.9, originalPrice: 29.9, stock: 45, soldCount: 122, minOrder: 1, unit: '组', supplier: '维达', imageColor: '#00897B', status: '下架', hot: false },
  { id: 13, name: '补水面膜 20片', category: '美妆', groupPrice: 59.0, originalPrice: 99.0, stock: 32, soldCount: 176, minOrder: 1, unit: '盒', supplier: '珀莱雅', imageColor: '#D81B60', status: '上架', hot: false },
  { id: 14, name: '洗发水 500ml*2', category: '美妆', groupPrice: 79.0, originalPrice: 118.0, stock: 26, soldCount: 89, minOrder: 1, unit: '组', supplier: '潘婷', imageColor: '#8E24AA', status: '上架', hot: false },
  { id: 15, name: '收纳箱 55L', category: '家居', groupPrice: 29.0, originalPrice: 45.0, stock: 58, soldCount: 143, minOrder: 1, unit: '个', supplier: '爱丽思', imageColor: '#5E35B1', status: '上架', hot: false },
  { id: 16, name: '保温杯 500ml', category: '家居', groupPrice: 49.0, originalPrice: 79.0, stock: 40, soldCount: 97, minOrder: 1, unit: '个', supplier: '膳魔师', imageColor: '#6D4C41', status: '上架', hot: false }
]

const MEMBER_LIST: MemberInfo[] = [
  { id: 1, name: '王丽', phone: '13812345678', avatarColor: '#E53935', orderCount: 38, totalSpent: 2860.5, level: '钻石', joinDate: '2025-03-12', lastOrder: '2026-08-24', address: '3栋502', note: '超级活跃' },
  { id: 2, name: '张强', phone: '13998765432', avatarColor: '#1E88E5', orderCount: 26, totalSpent: 1930.0, level: '金卡', joinDate: '2025-04-05', lastOrder: '2026-08-24', address: '5栋1103', note: '' },
  { id: 3, name: '李梅', phone: '13655557777', avatarColor: '#7CB342', orderCount: 22, totalSpent: 1156.8, level: '金卡', joinDate: '2025-05-18', lastOrder: '2026-08-23', address: '2栋306', note: '爱买日用' },
  { id: 4, name: '刘洋', phone: '15811112222', avatarColor: '#FB8C00', orderCount: 19, totalSpent: 980.6, level: '金卡', joinDate: '2025-04-22', lastOrder: '2026-08-24', address: '7栋808', note: '' },
  { id: 5, name: '陈静', phone: '18633334444', avatarColor: '#8E24AA', orderCount: 16, totalSpent: 765.0, level: '银卡', joinDate: '2025-06-01', lastOrder: '2026-08-24', address: '3栋1201', note: '' },
  { id: 6, name: '赵磊', phone: '17788889999', avatarColor: '#00897B', orderCount: 14, totalSpent: 1122.0, level: '银卡', joinDate: '2025-06-15', lastOrder: '2026-08-24', address: '1栋602', note: '爱买酒水' },
  { id: 7, name: '孙悦', phone: '13566665555', avatarColor: '#D81B60', orderCount: 12, totalSpent: 435.9, level: '银卡', joinDate: '2025-07-08', lastOrder: '2026-08-24', address: '6栋409', note: '' },
  { id: 8, name: '周涛', phone: '18899990000', avatarColor: '#5E35B1', orderCount: 11, totalSpent: 698.0, level: '银卡', joinDate: '2025-07-20', lastOrder: '2026-08-23', address: '2栋1101', note: '' },
  { id: 9, name: '吴敏', phone: '15922223333', avatarColor: '#039BE5', orderCount: 9, totalSpent: 326.4, level: '普通', joinDate: '2025-08-02', lastOrder: '2026-08-23', address: '4栋703', note: '' },
  { id: 10, name: '郑华', phone: '13644445555', avatarColor: '#6D4C41', orderCount: 8, totalSpent: 512.5, level: '普通', joinDate: '2025-08-15', lastOrder: '2026-08-23', address: '5栋205', note: '' },
  { id: 11, name: '冯雪', phone: '17855556666', avatarColor: '#F4511E', orderCount: 7, totalSpent: 189.9, level: '普通', joinDate: '2025-09-01', lastOrder: '2026-08-23', address: '1栋907', note: '' },
  { id: 12, name: '褚阳', phone: '13077778888', avatarColor: '#43A047', orderCount: 6, totalSpent: 478.0, level: '普通', joinDate: '2025-09-12', lastOrder: '2026-08-22', address: '7栋301', note: '' },
  { id: 13, name: '卫红', phone: '18512123434', avatarColor: '#FFB300', orderCount: 5, totalSpent: 267.0, level: '普通', joinDate: '2025-10-05', lastOrder: '2026-08-22', address: '3栋810', note: '新宝妈' },
  { id: 14, name: '蒋欣', phone: '13734345656', avatarColor: '#00ACC1', orderCount: 5, totalSpent: 348.0, level: '普通', joinDate: '2025-10-18', lastOrder: '2026-08-22', address: '6栋512', note: '' },
  { id: 15, name: '沈月', phone: '18698761234', avatarColor: '#78909C', orderCount: 3, totalSpent: 121.5, level: '普通', joinDate: '2025-11-30', lastOrder: '2026-08-20', address: '4栋105', note: '较少下单' },
  { id: 16, name: '韩雪', phone: '15901234567', avatarColor: '#3949AB', orderCount: 2, totalSpent: 68.0, level: '普通', joinDate: '2026-01-15', lastOrder: '2026-08-24', address: '2栋208', note: '新团员' }
]

const PICKUP_LIST: PickupInfo[] = [
  { id: 1, orderNo: 'TG20260824008', memberName: '周涛', pickupCode: 'PC823008', items: [{ itemName: '五常大米5kg', qty: 2 }, { itemName: '红枣1kg', qty: 1 }], amount: 98.0, status: '待核销', pickupDate: '08-23 16:30', pickupLocation: '3号楼架空层', verified: false },
  { id: 2, orderNo: 'TG20260824009', memberName: '吴敏', pickupCode: 'PC823009', items: [{ itemName: '进口牛奶1L', qty: 6 }], amount: 85.2, status: '待核销', pickupDate: '08-23 17:12', pickupLocation: '3号楼架空层', verified: false },
  { id: 3, orderNo: 'TG20260824010', memberName: '郑华', pickupCode: 'PC823010', items: [{ itemName: '沐浴露750ml', qty: 2 }, { itemName: '洗发水500ml', qty: 1 }], amount: 92.5, status: '待核销', pickupDate: '08-23 18:45', pickupLocation: '3号楼架空层', verified: false },
  { id: 4, orderNo: 'TG20260824011', memberName: '冯雪', pickupCode: 'PC823011', items: [{ itemName: '芒果5斤', qty: 1 }], amount: 29.9, status: '待核销', pickupDate: '08-23 19:20', pickupLocation: '3号楼架空层', verified: false },
  { id: 5, orderNo: 'TG20260824012', memberName: '褚阳', pickupCode: 'PC822012', items: [{ itemName: '牛肉2斤', qty: 1 }, { itemName: '火锅底料', qty: 2 }], amount: 128.0, status: '已核销', pickupDate: '08-22 15:10', pickupLocation: '3号楼架空层', verified: true },
  { id: 6, orderNo: 'TG20260824013', memberName: '卫红', pickupCode: 'PC822013', items: [{ itemName: '纸尿裤L码', qty: 1 }], amount: 89.0, status: '已核销', pickupDate: '08-22 16:00', pickupLocation: '3号楼架空层', verified: true },
  { id: 7, orderNo: 'TG20260824014', memberName: '蒋欣', pickupCode: 'PC822014', items: [{ itemName: '坚果礼盒', qty: 1 }, { itemName: '橙子10斤', qty: 1 }], amount: 118.0, status: '已核销', pickupDate: '08-22 17:33', pickupLocation: '3号楼架空层', verified: true },
  { id: 8, orderNo: 'TG20260823009', memberName: '何芳', pickupCode: 'PC821009', items: [{ itemName: '洗衣液2kg', qty: 1 }], amount: 27.9, status: '已过期', pickupDate: '08-21 14:22', pickupLocation: '3号楼架空层', verified: false },
  { id: 9, orderNo: 'TG20260823010', memberName: '许飞', pickupCode: 'PC821010', items: [{ itemName: '酸奶八连杯', qty: 2 }], amount: 51.8, status: '已过期', pickupDate: '08-21 15:40', pickupLocation: '3号楼架空层', verified: false },
  { id: 10, orderNo: 'TG20260823011', memberName: '吕鹏', pickupCode: 'PC820011', items: [{ itemName: '纸巾家庭装', qty: 1 }], amount: 29.9, status: '已核销', pickupDate: '08-20 09:15', pickupLocation: '3号楼架空层', verified: true },
  { id: 11, orderNo: 'TG20260823012', memberName: '苏晴', pickupCode: 'PC820012', items: [{ itemName: '土鸡蛋30枚', qty: 1 }], amount: 32.8, status: '已核销', pickupDate: '08-20 10:33', pickupLocation: '3号楼架空层', verified: true },
  { id: 12, orderNo: 'TG20260823013', memberName: '魏东', pickupCode: 'PC820013', items: [{ itemName: '保温杯500ml', qty: 1 }], amount: 49.0, status: '已核销', pickupDate: '08-20 11:50', pickupLocation: '3号楼架空层', verified: true },
  { id: 13, orderNo: 'TG20260823014', memberName: '宋佳', pickupCode: 'PC819014', items: [{ itemName: '补水面膜20片', qty: 1 }], amount: 59.0, status: '已核销', pickupDate: '08-19 16:08', pickupLocation: '3号楼架空层', verified: true },
  { id: 14, orderNo: 'TG20260823015', memberName: '罗兰', pickupCode: 'PC819015', items: [{ itemName: '坚果大礼包', qty: 2 }], amount: 138.0, status: '已核销', pickupDate: '08-19 17:26', pickupLocation: '3号楼架空层', verified: true },
  { id: 15, orderNo: 'TG20260823016', memberName: '梁波', pickupCode: 'PC818016', items: [{ itemName: '冰啤酒12听', qty: 1 }], amount: 39.0, status: '已核销', pickupDate: '08-18 18:44', pickupLocation: '3号楼架空层', verified: true }
]

const DAILY_REVENUE: DailyRevenueInfo[] = [
  { date: '08-18', amount: 1250.0, orderCount: 26, commission: 125.0 },
  { date: '08-19', amount: 980.5, orderCount: 19, commission: 98.0 },
  { date: '08-20', amount: 1560.0, orderCount: 32, commission: 156.0 },
  { date: '08-21', amount: 720.0, orderCount: 15, commission: 72.0 },
  { date: '08-22', amount: 1890.0, orderCount: 38, commission: 189.0 },
  { date: '08-23', amount: 2105.4, orderCount: 42, commission: 210.5 },
  { date: '08-24', amount: 1690.8, orderCount: 35, commission: 169.1 }
]

const CATEGORY_REVENUE: CategoryRevenueInfo[] = [
  { category: '生鲜', amount: 8930.0, percent: 46, barColor: '#43A047' },
  { category: '饮品', amount: 3560.0, percent: 18, barColor: '#1E88E5' },
  { category: '零食', amount: 2950.0, percent: 15, barColor: '#FF6F00' },
  { category: '日用', amount: 2150.0, percent: 11, barColor: '#00897B' },
  { category: '美妆', amount: 1080.0, percent: 6, barColor: '#D81B60' },
  { category: '家居', amount: 740.0, percent: 4, barColor: '#8E24AA' }
]

const REVENUE_CARDS: StatCardInfo[] = [
  { title: '本周销售额', value: '¥10246.7', trend: '↑ 12.5%', color: '#FF6F00' },
  { title: '本周佣金', value: '¥1019.6', trend: '↑ 8.3%', color: '#43A047' },
  { title: '本周订单', value: '207单', trend: '日均29.6单', color: '#1E88E5' },
  { title: '客单价', value: '¥49.5', trend: '↑ 3.1%', color: '#8E24AA' }
]

const LEVEL_BLOCK_COLORS: Record<string, string> = { '钻石': '#8E24AA', '金卡': '#FFB300', '银卡': '#1E88E5', '普通': '#8D6E63' }

const BANK_ACCOUNTS: BankAccountInfo[] = [
  { id: 1, bankName: '招商银行', cardNo: '**** **** **** 8867', holder: '李美琪' },
  { id: 2, bankName: '工商银行', cardNo: '**** **** **** 2345', holder: '李美琪' },
  { id: 3, bankName: '微信零钱', cardNo: '微信账户', holder: 'li_tuan' }
]

const LEADER_MENU: MenuInfo[] = [
  { icon: '🏬', title: '我的提货点', hint: '3号楼架空层 >' },
  { icon: '📦', title: '供应商管理', hint: '12家 >' },
  { icon: '📢', title: '群发通知', hint: '16位团员 >' },
  { icon: '📋', title: '团购规则设置', hint: ' >' },
  { icon: '🏦', title: '结算账户', hint: '招行 ****8867 >' },
  { icon: '⚙️', title: '设置', hint: ' >' }
]

const TAB_ITEMS: TabItemInfo[] = [
  { index: 0, icon: '📋', label: '订单管理' }, { index: 1, icon: '🛒', label: '商品上架' },
  { index: 2, icon: '👥', label: '团员管理' }, { index: 3, icon: '✅', label: '提货核销' },
  { index: 4, icon: '📊', label: '收益统计' }, { index: 5, icon: '👤', label: '团长中心' }
]

const LEADER_PROFILE: LeaderInfo = {
  name: '李美琪', phone: '13712345678', communityName: '阳光花园社区3号楼',
  totalRevenue: 19405.7, totalCommission: 1940.6, memberCount: 16, rating: 4.9,
  level: '金牌团长', badges: ['月度销冠', '服务之星', '社群达人', '增长先锋']
}

// ---------------- 纯函数 ----------------
function getOrdersByStatus(status: string): OrderInfo[] {
  let result: OrderInfo[] = []
  for (let i = 0; i < ORDER_LIST.length; i++) {
    if (ORDER_LIST[i].status === status) {
      result.push(ORDER_LIST[i])
    }
  }
  return result
}

function getItemsText(items: OrderItemInfo[]): string {
  let parts: string[] = []
  for (let i = 0; i < items.length; i++) {
    parts.push(items[i].itemName + ' x' + items[i].qty)
  }
  return parts.join('、')
}

function getTodayOrderCount(): number {
  return ORDER_LIST.length
}

function getTodayRevenue(): number {
  let total = 0.0
  for (let i = 0; i < ORDER_LIST.length; i++) {
    total += ORDER_LIST[i].totalAmount
  }
  return Math.round(total * 10) / 10
}

function getWeekMaxRevenue(): number {
  let maxVal = 0.0
  for (let i = 0; i < DAILY_REVENUE.length; i++) {
    if (DAILY_REVENUE[i].amount > maxVal) {
      maxVal = DAILY_REVENUE[i].amount
    }
  }
  return maxVal
}

function getMemberCountByLevel(level: string): number {
  let count = 0
  for (let i = 0; i < MEMBER_LIST.length; i++) {
    if (MEMBER_LIST[i].level === level) {
      count++
    }
  }
  return count
}

function getTotalStatusCount(): number {
  return ORDER_LIST.length
}

function maskPhone(phone: string): string {
  if (phone.length < 7) {
    return phone
  }
  return phone.substring(0, 3) + '****' + phone.substring(7)
}

function getHotCount(): number {
  let count = 0
  for (let i = 0; i < PRODUCT_LIST.length; i++) {
    if (PRODUCT_LIST[i].hot) {
      count++
    }
  }
  return count
}

function getPickupPendingCount(): number {
  let count = 0
  for (let i = 0; i < PICKUP_LIST.length; i++) {
    if (PICKUP_LIST[i].status === '待核销') {
      count++
    }
  }
  return count
}

function getBarHeight(amount: number, maxVal: number): number {
  if (maxVal <= 0) {
    return 12
  }
  let ratio = amount / maxVal
  if (ratio < 0.08) {
    return 12
  }
  return Math.round(ratio * 120)
}

function getStockPercent(stock: number): number {
  if (stock >= 100) {
    return 100
  }
  return stock
}

// ---------------- Tab 枚举 ----------------
enum MainTab { Orders = 0, Products = 1, Members = 2, Pickup = 3, Revenue = 4, Leader = 5 }

// ---------------- 通用Builder: 社区电商头部 ----------------
@Builder function pageHeader(title: string, subtitle: string, orderCount: string, revenue: string) {
  Column() {
    Row() {
      Column() {
        Text(title).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
        Text(subtitle).fontSize(12).fontColor('#FFE0B2').margin({ top: 4 })
      }.alignItems(HorizontalAlign.Start)
      Blank()
      Column() {
        Text('今日订单').fontSize(10).fontColor('#FFE0B2')
        Text(orderCount).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
      }.alignItems(HorizontalAlign.End)
      Column() {
        Text('今日流水').fontSize(10).fontColor('#FFE0B2')
        Text('¥' + revenue).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
      }.alignItems(HorizontalAlign.End)
    }.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
    Row() {
      Text('🔍 搜索订单 / 团员 / 商品').fontSize(13).fontColor(COLORS.subText).padding({ left: 12 })
      Blank()
      Text('筛选').fontSize(13).fontColor(COLORS.primary).padding({ right: 12 })
    }.width('100%').height(38).backgroundColor(COLORS.white).borderRadius(19).margin({ left: 16, right: 16, bottom: 12 })
  }.width('100%').backgroundColor(COLORS.primary).borderRadius({ bottomLeft: 20, bottomRight: 20 })
}

// ---------------- Tab1: 订单管理 (看板布局) ----------------
@Component
struct OrdersPage {
  @State showNewOrderDialog: boolean = false
  @State showOrderDetail: boolean = false
  @State selectedOrder: OrderInfo | null = null

  @Builder modalOverlay(onClose: () => void) {
    Column() {
      Blank()
      Column() {
        Text('点击空白处关闭').fontSize(12).fontColor('#FFFFFF').onClick(() => { onClose() })
      }
    }.width('100%').height('100%').backgroundColor('rgba(62,39,35,0.5)').justifyContent(FlexAlign.End).onClick(() => { onClose() })
  }

  @Builder statusColumn(title: string, color: string, count: number) {
    Column() {
      Row() {
        Circle({ width: 8, height: 8 }).fill(color)
        Text(title).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
        Text(count + '单').fontSize(11).fontColor(COLORS.subText)
      }.width('100%').padding({ left: 10, right: 10, top: 10, bottom: 6 })
      List() {
        ForEach(getOrdersByStatus(title), (order: OrderInfo) => {
          ListItem() {
            Column() {
              Row() {
                Text(order.orderNo).fontSize(10).fontColor(COLORS.subText)
                Blank()
                Text('¥' + order.totalAmount.toFixed(2)).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
              }.width('100%')
              Row() {
                Text(order.memberName).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
                Text(maskPhone(order.phone)).fontSize(11).fontColor(COLORS.subText)
              }.width('100%').margin({ top: 6 })
              Text(getItemsText(order.items)).fontSize(11).fontColor(COLORS.subText).maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 4 })
              Row() {
                Text(order.pickupCode).fontSize(11).fontColor(COLORS.secondary).fontWeight(FontWeight.Bold)
                Blank()
                Text('详情 >').fontSize(11).fontColor(COLORS.primary)
              }.width('100%').margin({ top: 8 })
            }.width('100%').padding(10).backgroundColor(COLORS.white).borderRadius(12).margin({ bottom: 8 })
            .onClick(() => { this.selectedOrder = order; this.showOrderDetail = true })
          }
        }, (order: OrderInfo) => order.id.toString())
      }.layoutWeight(1).width('100%')
    }.width(170).backgroundColor('#FFF3E0').borderRadius(14).margin({ left: 6, right: 6 }).alignItems(HorizontalAlign.Start)
  }

  build() {
    Stack() {
      Column() {
        pageHeader('订单管理', '阳光花园 · 第32期团购', getTodayOrderCount().toString(), getTodayRevenue().toString())
        Row() {
          Text('新增订单').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
            .backgroundColor(COLORS.primary).borderRadius(16).padding({ left: 14, right: 14, top: 7, bottom: 7 })
            .onClick(() => { this.showNewOrderDialog = true })
          Blank()
          Text('今日: ' + getTodayOrderCount() + '单 / ¥' + getTodayRevenue().toString()).fontSize(12).fontColor(COLORS.subText)
        }.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 6 })
        Scroll() {
          Row() {
            this.statusColumn('待确认', STATUS_COLORS['待确认'], STATUS_STATS['待确认'])
            this.statusColumn('备货中', STATUS_COLORS['备货中'], STATUS_STATS['备货中'])
            this.statusColumn('待提货', STATUS_COLORS['待提货'], STATUS_STATS['待提货'])
            this.statusColumn('已完成', STATUS_COLORS['已完成'], STATUS_STATS['已完成'])
          }
        }.scrollable(ScrollDirection.Horizontal).layoutWeight(1).width('100%').padding({ left: 10, right: 10, bottom: 10 })
      }.width('100%').height('100%')
      if (this.showNewOrderDialog) {
        this.modalOverlay(() => { this.showNewOrderDialog = false })
        this.newOrderDialog()
      }
      if (this.showOrderDetail) {
        this.modalOverlay(() => { this.showOrderDetail = false })
        this.orderDetailDialog()
      }
    }.width('100%').height('100%')
  }

  // 弹框1: 新增订单
  @Builder newOrderDialog() {
    Column() {
      Text('新增订单').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      Divider().margin({ top: 12, bottom: 12 })
      Row() {
        Text('选择团员').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text('王丽 (钻石)').fontSize(13).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
      }.width('100%').padding({ top: 6, bottom: 6 })
      Row() {
        Text('选择商品').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text('红富士苹果 5斤装').fontSize(13).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
      }.width('100%').padding({ top: 6, bottom: 6 })
      Row() {
        Text('数量').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text('−').fontSize(18).fontColor(COLORS.primary).padding(6)
        Text('2').fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.text).padding(6)
        Text('+').fontSize(18).fontColor(COLORS.primary).padding(6)
      }.width('100%').padding({ top: 6, bottom: 6 })
      Divider().margin({ top: 8, bottom: 8 })
      Row() {
        Text('合计').fontSize(14).fontColor(COLORS.subText)
        Blank()
        Text('¥39.80').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
      }.width('100%')
      Row() {
        Text('取消').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.subText).backgroundColor('#FFF3E0')
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showNewOrderDialog = false })
        Blank()
        Text('确认下单').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.primary)
          .borderRadius(20).padding({ left: 24, right: 24, top: 9, bottom: 9 }).onClick(() => { this.showNewOrderDialog = false })
      }.width('100%').margin({ top: 16 })
    }.width('86%').padding(20).backgroundColor(COLORS.white).borderRadius(18)
  }

  // 弹框4: 订单详情
  @Builder orderDetailDialog() {
    Column() {
      Text('订单详情').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      Divider().margin({ top: 12, bottom: 12 })
      Row() {
        Column() {
          Text(this.selectedOrder?.memberName ?? '').fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
          Text(this.selectedOrder?.phone ?? '').fontSize(12).fontColor(COLORS.subText).margin({ top: 2 })
        }.alignItems(HorizontalAlign.Start)
        Blank()
        Text(this.selectedOrder?.status ?? '').fontSize(12).fontColor(COLORS.white)
          .backgroundColor(STATUS_COLORS[this.selectedOrder?.status ?? '待确认'])
          .borderRadius(10).padding({ left: 10, right: 10, top: 4, bottom: 4 })
      }.width('100%')
      Divider().margin({ top: 10, bottom: 10 })
      ForEach(this.selectedOrder?.items ?? [], (item: OrderItemInfo) => {
        Row() {
          Text(item.itemName).fontSize(13).fontColor(COLORS.text)
          Blank()
          Text('x' + item.qty).fontSize(13).fontColor(COLORS.subText)
        }.width('100%').padding({ top: 4, bottom: 4 })
      }, (item: OrderItemInfo, idx: number) => item.itemName + idx.toString())
      Divider().margin({ top: 8, bottom: 8 })
      Column() {
        Text('时间线').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
        Row() {
          Circle({ width: 8, height: 8 }).fill(COLORS.secondary)
          Text('下单 ' + (this.selectedOrder?.createDate ?? '')).fontSize(12).fontColor(COLORS.subText)
        }.width('100%').margin({ top: 8 })
        Row() {
          Circle({ width: 8, height: 8 }).fill(COLORS.secondary)
          Text('备货 供应商发货中').fontSize(12).fontColor(COLORS.subText)
        }.width('100%').margin({ top: 6 })
        Row() {
          Circle({ width: 8, height: 8 }).fill(COLORS.border)
          Text('提货 ' + (this.selectedOrder?.pickupLocation ?? '')).fontSize(12).fontColor(COLORS.subText)
        }.width('100%').margin({ top: 6 })
      }.alignItems(HorizontalAlign.Start).width('100%')
      Divider().margin({ top: 10, bottom: 10 })
      Row() {
        Text('提货码: ' + (this.selectedOrder?.pickupCode ?? '')).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
        Blank()
        Text('合计 ¥' + (this.selectedOrder?.totalAmount ?? 0).toFixed(2)).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      }.width('100%')
      Text('备注: ' + (this.selectedOrder?.note || '无')).fontSize(12).fontColor(COLORS.subText).margin({ top: 8 })
      Text('关闭').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.primary)
        .borderRadius(20).padding({ left: 40, right: 40, top: 9, bottom: 9 }).margin({ top: 16 })
        .onClick(() => { this.showOrderDetail = false })
    }.width('86%').padding(20).backgroundColor(COLORS.white).borderRadius(18)
  }
}

// ---------------- Tab2: 商品上架 (2列网格) ----------------
@Component
struct ProductsPage {
  @State showEditDialog: boolean = false
  @State showDeleteDialog: boolean = false
  @State selectedProduct: ProductInfo | null = null

  @Builder modalOverlay(onClose: () => void) {
    Column() {
      Blank()
      Column() {
        Text('点击空白处关闭').fontSize(12).fontColor('#FFFFFF').onClick(() => { onClose() })
      }
    }.width('100%').height('100%').backgroundColor('rgba(62,39,35,0.5)').justifyContent(FlexAlign.End).onClick(() => { onClose() })
  }

  build() {
    Stack() {
      Column() {
        pageHeader('商品上架', '在售商品 · 第32期团购', PRODUCT_LIST.length.toString(), '8930')
        Row() {
          Text('全部 (' + PRODUCT_LIST.length + ')').fontSize(12).fontColor(COLORS.white).backgroundColor(COLORS.primary)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          Text('爆款 (' + getHotCount() + ')').fontSize(12).fontColor(COLORS.primary).backgroundColor(COLORS.white)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          Text('缺货').fontSize(12).fontColor(COLORS.danger).backgroundColor(COLORS.white)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          Blank()
          Text('+ 新增商品').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.white).backgroundColor(COLORS.secondary)
            .borderRadius(14).padding({ left: 12, right: 12, top: 5, bottom: 5 }).onClick(() => { this.showEditDialog = true })
        }.width('100%').padding({ left: 16, right: 16, top: 10, bottom: 8 })
        Scroll() {
          Column() {
            Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
              ForEach(PRODUCT_LIST, (product: ProductInfo) => {
                Column() {
                  Stack() {
                    Column() {
                      Text(product.name.substring(0, 1)).fontSize(26).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                    }.width('100%').height(80).backgroundColor(product.imageColor)
                    .borderRadius({ topLeft: 12, topRight: 12 }).justifyContent(FlexAlign.Center)
                    .constraintSize({ maxHeight: '100%' })
                    Text(product.hot ? 'HOT' : '').fontSize(9).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
                      .backgroundColor(COLORS.danger).borderRadius(8)
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 }).position({ x: 8, y: 8 })
                  }.width('100%').height(80)
                  Text(product.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.text).maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis }).margin({ top: 8, bottom: 2 }).padding({ left: 8, right: 8 })
                  Row() {
                    Text('¥' + product.groupPrice.toFixed(2)).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
                    Text('¥' + product.originalPrice.toFixed(2)).fontSize(11).fontColor(COLORS.subText)
                      .decoration({ type: TextDecorationType.LineThrough })
                  }.width('100%').padding({ left: 8, right: 8 })
                  Row() {
                    Text('已售' + product.soldCount).fontSize(10).fontColor(COLORS.subText)
                    Blank()
                    Text('库存' + product.stock).fontSize(10).fontColor(product.stock < 30 ? COLORS.danger : COLORS.secondary)
                  }.width('100%').padding({ left: 8, right: 8, top: 4 })
                  Progress({ value: getStockPercent(product.stock), total: 100, type: ProgressType.Linear })
                    .width('90%').height(4).color(COLORS.secondary).margin({ top: 6 })
                  Row() {
                    Text(product.status).fontSize(10).fontColor(product.status === '上架' ? COLORS.secondary : COLORS.warning)
                      .backgroundColor('#E8F5E9').borderRadius(8).padding({ left: 8, right: 8, top: 2, bottom: 2 })
                    Blank()
                    Text('编辑').fontSize(11).fontColor(COLORS.primary).padding(4)
                      .onClick(() => { this.selectedProduct = product; this.showEditDialog = true })
                    Text('删除').fontSize(11).fontColor(COLORS.danger).padding(4)
                      .onClick(() => { this.selectedProduct = product; this.showDeleteDialog = true })
                  }.width('100%').padding({ left: 8, right: 8, top: 6, bottom: 10 })
                }.width('48%').backgroundColor(COLORS.white).borderRadius(12).margin({ bottom: 10 })
              }, (product: ProductInfo) => product.id.toString())
            }.width('100%').padding({ left: 16, right: 16, bottom: 12 })
          }.width('100%')
        }.layoutWeight(1).width('100%')
      }.width('100%').height('100%')
      if (this.showEditDialog) {
        this.modalOverlay(() => { this.showEditDialog = false })
        this.editProductDialog()
      }
      if (this.showDeleteDialog) {
        this.modalOverlay(() => { this.showDeleteDialog = false })
        this.deleteConfirmDialog()
      }
    }.width('100%').height('100%')
  }

  // 弹框2: 编辑商品
  @Builder editProductDialog() {
    Column() {
      Text('编辑商品').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.text)
      Divider().margin({ top: 12, bottom: 12 })
      Row() {
        Text('商品名称').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text(this.selectedProduct?.name ?? '红富士苹果 5斤装').fontSize(13).fontColor(COLORS.text)
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('团购价').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text('¥' + (this.selectedProduct?.groupPrice ?? 19.9).toFixed(2)).fontSize(13)
          .fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('库存').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text((this.selectedProduct?.stock ?? 0).toString() + ' ' + (this.selectedProduct?.unit ?? '箱'))
          .fontSize(13).fontColor(COLORS.text)
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('分类').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text(this.selectedProduct?.category ?? '生鲜').fontSize(12).fontColor(COLORS.white)
          .backgroundColor(COLORS.secondary).borderRadius(10).padding({ left: 10, right: 10, top: 3, bottom: 3 })
      }.width('100%').padding({ top: 4, bottom: 4 })
      Row() {
        Text('供应商').fontSize(13).fontColor(COLORS.subText)
        Blank()
        Text(this.selectedProduct?.supplier ?? '烟台果园直供').fontSize(13).fontColor(COLORS.text)
      }.width('100%').padding({ top: 4, bottom: 4 })
 
  @State selectedPickup: PickupInfo | null = null

 
  }
}

// ---------------- 主入口: 6 Tab 底部导航 ----------------
@Entry
@Component
struct GroupBuyingApp {
  @State currentTab: number = 0

  build() {
    Column() {
      Stack() {
        Column() {
          if (this.currentTab === MainTab.Orders) {
            OrdersPage()
          } else if (this.currentTab === MainTab.Products) {
            ProductsPage()
          } else if (this.currentTab === MainTab.Members) {
            MembersPage()
          } else if (this.currentTab === MainTab.Pickup) {
            PickupPage()
          } else if (this.currentTab === MainTab.Revenue) {
            RevenuePage()
          } else {
            LeaderPage()
          }
        }.width('100%').height('100%')
      }.layoutWeight(1).width('100%')
      Divider()
      Row() {
        ForEach(TAB_ITEMS, (tab: TabItemInfo) => {
          Column() {
            Text(tab.icon).fontSize(20).fontColor(this.currentTab === tab.index ? COLORS.primary : COLORS.subText)
            Text(tab.label).fontSize(10)
              .fontColor(this.currentTab === tab.index ? COLORS.primary : COLORS.subText).margin({ top: 2 })
          }.layoutWeight(1).onClick(() => { this.currentTab = tab.index })
        }, (tab: TabItemInfo) => tab.label)
      }.width('100%').height(56).backgroundColor(COLORS.white)
    }.width('100%').height('100%').backgroundColor(COLORS.bg)
  }
}



在这里插入图片描述

总结

本文对基于HarmonyOS 6.1.1的ArkTS社区团购团长管理应用进行了六大模块的全栈深度解析。从十三个接口定义的强类型数据契约,到十余个纯函数封装的业务逻辑,再到六个独立@Component构建的页面组件,该应用完整呈现了社区团购团长从订单管理到收益结算的全业务链路。应用采用的三段式分层架构(接口层-函数层-组件层)保证了代码的可维护性和可测试性,每个层次职责清晰、边界分明,为企业级ArkTS应用的架构设计提供了优秀的参考范本。

在UI实现层面,应用展示了丰富的ArkTS声明式UI能力。看板式布局通过横向Scroll+List+ForEach实现了多状态订单的分列展示;双列网格通过Flex的wrap换行属性实现了自适应的商品卡片排列;柱状图和条形图通过纯组件拼装实现了无需第三方库的数据可视化;二维码模拟图形通过多个小方块Column的Stack叠加展示了ArkTS用基础组件构建复杂图形的能力。全局@Builder函数pageHeader和各页面通用的modalOverlay模式,充分体现了ArkTS在UI复用方面的设计哲学。特别是核销确认弹框中用Column方块拼装的二维码图案、结算提现弹框中的单选账户指示器等细节设计,展现了开发者对业务场景的深入理解和对ArkTS组件能力的充分挖掘。

设计了订单管理(看板CRUD)、商品上架(网格管理)、团员管理(列表+详情)、提货核销(扫码+确认)、收益统计(仪表盘可视化)、团长中心(个人中心+提现)六个功能模块,形成了一个完整的团长工作台。每个模块都配备了对应的模态弹框,实现了从浏览到操作的闭环交互。色彩系统以团购橙和社区绿为双主色,通过12色设计令牌和多个Record映射常量保障了全局视觉一致性。对于希望掌握HarmonyOS ArkTS在企业级移动应用开发中的实践技巧的开发者而言,本应用的架构设计、组件封装策略和数据可视化方案都具有重要的学习价值。

Logo

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

更多推荐