HarmonyOS ArkTS 实战:实现一个校园外卖代买与跑腿应用

HarmonyOS ArkTS 实战:从零实现一个校园外卖代买与跑腿应用

HarmonyOS ArkTS 实战:从零实现一个校园外卖代买与跑腿应用

本文将带你使用 HarmonyOS + ArkTS 从零搭建一个功能完整的校园外卖代买与跑腿应用,涵盖订单发布、接单抢单、状态跟踪、支付评价、跑腿员认证等核心模块,附完整可运行代码。


目录

  • 一、项目背景与效果预览

  • 二、技术栈与环境准备

  • 三、项目结构设计

  • 四、数据模型设计

    • 4.1 跑腿订单模型

    • 4.2 跑腿员模型

    • 4.3 评价模型

    • 4.4 设计思路

  • 五、页面布局与 UI 实现

    • 5.1 整体布局架构

    • 5.2 顶部导航与发布入口

    • 5.3 数据概览面板

    • 5.4 订单大厅列表

    • 5.5 我的订单 Tab

    • 5.6 跑腿员排行

    • 5.7 发布订单弹窗

    • 5.8 订单详情弹窗

  • 六、核心功能实现详解

    • 6.1 发布跑腿订单

    • 6.2 接单与抢单

    • 6.3 订单状态流转

    • 6.4 订单完成与评价

    • 6.5 跑腿员认证

    • 6.6 余额与支付模拟

  • 七、样式与交互动效

    • 7.1 主题色系统

    • 7.2 状态颜色体系

    • 7.3 交互动效

  • 八、完整代码结构

  • 九、运行与调试

  • 十、技术要点总结

  • 十一、扩展方向


一、项目背景与效果预览

大学校园里,"帮忙取个快递""帮我带杯奶茶""帮我占个座"------这些日常的跑腿需求无处不在。本项目以"校园跑腿"为切入点,使用 HarmonyOS ArkTS 开发一个集订单发布、接单抢单、状态跟踪、在线支付、评价体系于一体的跑腿服务应用。

核心功能一览

功能模块 说明
📦 发布需求 选择类型、填写描述、设置赏金,一键发布
🏃 接单大厅 浏览所有待接订单,支持按类型筛选
🔄 状态跟踪 实时查看订单状态:待接单→进行中→已完成
💰 在线支付 余额支付,支持充值与提现(模拟)
⭐ 订单评价 完成后对跑腿员打分评价
📋 我的订单 我发布的 / 我接的,一目了然
🏅 跑腿员认证 实名认证后成为跑腿员
📊 收益统计 接单数量、总收益、好评率
🏆 跑腿排行 跑腿员接单量排行榜
🔥 悬赏跑腿 高价加急订单,优先展示

运行效果

应用整体采用深黄绿(#4D7C0F)为主色调,搭配清新的绿色渐变背景,传递高效、可靠、充满活力的服务感。顶部为发布入口和数据概览,中部为订单大厅,底部为 Tab 导航,支持上下滑动浏览。


二、技术栈与环境准备

开发环境

项目 版本要求 说明
DevEco Studio 5.0+ HarmonyOS 官方 IDE
HarmonyOS SDK API 24+ 基于 ArkTS 声明式开发范式
运行设备 HarmonyOS 4.0+ 手机 / 平板均可

项目亮点

本项目不仅是一个 UI 练习,更包含了完整的业务逻辑闭环

  1. 订单全生命周期:从发布 → 接单 → 配送 → 完成 → 评价,完整流程

  2. 双角色系统:发布者和跑腿员两种身份,不同视角

  3. 状态机设计:订单状态流转清晰,边界处理完善

  4. 数据驱动:所有 UI 变化由状态驱动,符合声明式开发思想


三、项目结构设计

Plaintext 复制代码
entry/src/main/
├── ets/
│   ├── pages/
│   │   └── Index.ets              # 主页面(单页应用)
│   ├── model/
│   │   ├── ErrandsOrder.ets       # 跑腿订单数据模型
│   │   ├── Runner.ets             # 跑腿员数据模型
│   │   └── Review.ets             # 评价数据模型
│   ├── components/
│   │   ├── PublishHeader.ets      # 顶部发布入口
│   │   ├── StatsOverview.ets      # 数据概览面板
│   │   ├── OrderCard.ets          # 订单卡片组件
│   │   ├── OrderList.ets          # 订单列表组件
│   │   ├── RunnerRank.ets         # 跑腿员排行组件
│   │   ├── PublishDialog.ets      # 发布订单弹窗
│   │   └── OrderDetailDialog.ets  # 订单详情弹窗
│   └── utils/
│       ├── MockData.ets           # 模拟数据
│       └── OrderStatus.ets        # 状态常量与工具函数
├── resources/
│   ├── base/
│   │   ├── element/               # 颜色、尺寸等资源
│   │   └── media/                 # 图片资源
│   └── rawfile/                   # 其他资源
└── module.json5                   # 模块配置

💡 架构说明 :本项目采用单页面 + 组件化 架构。主页面 Index.ets 负责状态管理和业务逻辑,各功能模块拆分为独立组件,通过 @Prop 和事件回调通信。这种设计既保持了代码清晰,又降低了初学者的理解门槛。


四、数据模型设计

4.1 跑腿订单模型

订单是整个应用的核心实体,记录了一笔跑腿需求的所有信息。

TypeScript 复制代码
/**
 * 跑腿订单数据结构
 * 描述一笔完整的跑腿订单
 */
interface ErrandsOrder {
  id: number;              // 订单唯一标识
  type: OrderType;         // 订单类型
  title: string;           // 订单标题(简短描述)
  description: string;     // 详细描述
  startLocation: string;   // 取货/起始地点
  endLocation: string;     // 送达/目标地点
  reward: number;          // 跑腿费(元)
  tip: number;             // 小费/悬赏(元)
  publishTime: string;     // 发布时间
  expectTime: string;      // 期望送达时间
  publisher: string;       // 发布者昵称
  publisherAvatar: string; // 发布者头像
  runner: string;          // 接单跑腿员昵称(未接单为空)
  runnerAvatar: string;    // 跑腿员头像
  status: OrderStatus;     // 订单状态
  remark: string;          // 备注信息
  isUrgent: boolean;       // 是否加急
  weight: string;          // 物品重量/大小
  review: Review | null;   // 评价(完成后才有)
}

/**
 * 订单类型
 * takeout: 外卖代取
 * express: 快递代取
 * grocery: 超市代购
 * document: 文件代办
 * queue: 排队占座
 * other: 其他
 */
type OrderType = 'takeout' | 'express' | 'grocery' | 'document' | 'queue' | 'other';

/**
 * 订单状态
 * pending:    待接单
 * accepted:   已接单(跑腿员已接,前往取货)
 * delivering: 配送中(已取货,正在送达)
 * completed:  已完成
 * cancelled:  已取消
 */
type OrderStatus = 'pending' | 'accepted' | 'delivering' | 'completed' | 'cancelled';

4.2 跑腿员模型

TypeScript 复制代码
/**
 * 跑腿员数据结构
 */
interface Runner {
  id: number;           // 跑腿员 ID
  name: string;         // 昵称
  avatar: string;       // 头像
  rating: number;       // 评分(0-5)
  orderCount: number;   // 完成订单数
  totalEarnings: number; // 总收益
  isCertified: boolean;  // 是否已认证
  level: RunnerLevel;    // 等级
  completedToday: number; // 今日完成数
}

/**
 * 跑腿员等级
 * bronze:   青铜(0-50单)
 * silver:   白银(50-200单)
 * gold:     黄金(200-500单)
 * platinum: 铂金(500-1000单)
 * diamond:  钻石(1000单以上)
 */
type RunnerLevel = 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';

4.3 评价模型

TypeScript 复制代码
/**
 * 订单评价数据结构
 */
interface Review {
  id: number;          // 评价 ID
  orderId: number;     // 关联订单 ID
  rating: number;      // 评分(1-5)
  content: string;     // 评价内容
  from: string;        // 评价人
  to: string;          // 被评价人
  time: string;        // 评价时间
  tags: string[];      // 评价标签(如"速度快""态度好")
}

4.4 设计思路

设计决策 理由
订单状态 5 种 覆盖完整生命周期:发布→接单→配送→完成/取消
分离 rewardtip 基础跑腿费和小费分开,方便统计和激励
isUrgent 加急标记 加急订单优先展示,增加平台运营空间
评价嵌入订单 一对一关系,直接嵌套比单独查询更简单
跑腿员等级体系 游戏化设计,激励跑腿员多接单

五、页面布局与 UI 实现

5.1 整体布局架构

页面采用顶部导航 + 内容区 + 底部 Tab 的经典三段式布局:

Plaintext 复制代码
┌─────────────────────────┐
│   顶部发布入口 + 搜索     │  ← PublishHeader
├─────────────────────────┤
│                         │
│      数据概览面板        │  ← StatsOverview
│                         │
├─────────────────────────┤
│                         │
│      订单大厅 / 我的     │  ← OrderList (根据 Tab 切换)
│                         │
│                         │
│                         │
├─────────────────────────┤
│  大厅 | 订单 | 排行 | 我的 │  ← BottomTabBar
└─────────────────────────┘

核心布局骨架:

TypeScript 复制代码
@Entry
@Component
struct Index {
  @State currentTab: number = 0; // 0:大厅 1:订单 2:排行 3:我的
  
  build() {
    Column() {
      // 顶部区域(固定)
      PublishHeader({ onPublish: () => this.showPublishDialog = true })
      
      // 数据概览(大厅 Tab 时显示)
      if (this.currentTab === 0) {
        StatsOverview({ stats: this.hallStats })
      }
      
      // 内容区域(可滚动)
      Scroll() {
        Column() {
          if (this.currentTab === 0) {
            // 订单大厅
            this.HallContent()
          } else if (this.currentTab === 1) {
            // 我的订单
            this.MyOrdersContent()
          } else if (this.currentTab === 2) {
            // 跑腿排行
            RunnerRank({ runners: this.runners })
          } else {
            // 个人中心
            this.ProfileContent()
          }
        }
        .width('100%')
      }
      .layoutWeight(1)
      
      // 底部 Tab 栏
      BottomTabBar({ 
        currentTab: this.currentTab,
        onTabChange: (index) => this.currentTab = index
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F7FEE7') // 浅绿背景
  }
}

5.2 顶部导航与发布入口

顶部区域包含应用标题、搜索框和醒目的发布按钮。

TypeScript 复制代码
@Component
struct PublishHeader {
  onPublish: () => void;
  @State searchText: string = '';
  
  build() {
    Column() {
      // 标题栏
      Row() {
        Column() {
          Text('校园跑腿')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4D7C0F')
          Text('你的校园生活好帮手')
            .fontSize(12)
            .fontColor('#84CC16')
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        
        // 消息通知
        Stack() {
          Image($r('app.media.bell'))
            .width(24)
            .height(24)
            .fillColor('#4D7C0F')
          Circle({ width: 8, height: 8 })
            .fill('#EF4444')
            .position({ x: 16, y: 0 })
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16 })
      
      // 搜索框
      Row() {
        Image($r('app.media.search'))
          .width(18)
          .height(18)
          .fillColor('#9CA3AF')
        
        TextInput({ placeholder: '搜索订单...', text: this.searchText })
          .layoutWeight(1)
          .margin({ left: 8 })
          .backgroundColor(Color.Transparent)
          .fontSize(14)
          .onChange((value: string) => {
            this.searchText = value;
          })
      }
      .width('100%')
      .height(40)
      .margin({ left: 16, right: 16, top: 12, bottom: 12 })
      .padding({ left: 12, right: 12 })
      .backgroundColor('#FFFFFF')
      .borderRadius(20)
      
      // 快捷发布按钮
      Row() {
        this.QuickPublishBtn('外卖代取', '🍔', 'takeout')
        this.QuickPublishBtn('快递代取', '📦', 'express')
        this.QuickPublishBtn('超市代购', '🛒', 'grocery')
        this.QuickPublishBtn('更多', '➕', 'other')
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
      .justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
  }
  
  @Builder
  QuickPublishBtn(label: string, icon: string, type: OrderType) {
    Column() {
      Text(icon)
        .fontSize(28)
      Text(label)
        .fontSize(12)
        .fontColor('#333333')
        .margin({ top: 4 })
    }
    .width('25%')
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 8, bottom: 8 })
    .onClick(() => {
      this.onPublish();
    })
  }
}

设计要点

  • 顶部白色背景与内容区浅绿背景形成对比,层次分明

  • 快捷发布按钮用 emoji 作为图标,生动有趣且无需额外图片资源

  • 搜索框圆角设计,符合现代审美

  • 消息红点用 Stack + 定位实现

5.3 数据概览面板

展示大厅关键数据,让用户一目了然当前订单情况。

TypeScript 复制代码
@Component
struct StatsOverview {
  @Prop stats: HallStats;
  
  build() {
    Row() {
      this.StatCard('待接单', this.stats.pendingCount.toString(), '#F59E0B', '📋')
      this.StatCard('进行中', this.stats.inProgressCount.toString(), '#3B82F6', '🏃')
      this.StatCard('今日完成', this.stats.todayCompleted.toString(), '#10B981', '✅')
      this.StatCard('我的收益', `¥${this.stats.myEarnings}`, '#4D7C0F', '💰')
    }
    .width('100%')
    .padding(16)
    .justifyContent(FlexAlign.SpaceBetween)
  }
  
  @Builder
  StatCard(label: string, value: string, color: string, icon: string) {
    Column() {
      Text(icon)
        .fontSize(24)
      Text(value)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
        .margin({ top: 4 })
      Text(label)
        .fontSize(11)
        .fontColor('#666666')
        .margin({ top: 2 })
    }
    .width('23%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Center)
  }
}

interface HallStats {
  pendingCount: number;      // 待接单数量
  inProgressCount: number;   // 进行中数量
  todayCompleted: number;    // 今日完成数
  myEarnings: number;        // 我的收益
}

5.4 订单大厅列表

订单大厅是应用的核心页面,展示所有可接的订单,支持按类型筛选。

TypeScript 复制代码
@Component
struct OrderList {
  @State orders: ErrandsOrder[] = [];
  @State selectedType: string = 'all'; // all / takeout / express / ...
  onOrderClick: (order: ErrandsOrder) => void;
  onTakeOrder: (orderId: number) => void;
  
  private get filteredOrders(): ErrandsOrder[] {
    if (this.selectedType === 'all') return this.orders;
    return this.orders.filter(o => o.type === this.selectedType);
  }
  
  build() {
    Column() {
      // 类型筛选栏
      Scroll() {
        Row({ space: 8 }) {
          this.TypeFilter('全部', 'all')
          this.TypeFilter('外卖', 'takeout')
          this.TypeFilter('快递', 'express')
          this.TypeFilter('超市', 'grocery')
          this.TypeFilter('文件', 'document')
          this.TypeFilter('排队', 'queue')
          this.TypeFilter('其他', 'other')
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .margin({ bottom: 12 })
      
      // 订单列表
      List({ space: 12 }) {
        ForEach(this.filteredOrders, (order: ErrandsOrder) => {
          ListItem() {
            OrderCard({
              order: order,
              onClick: () => this.onOrderClick(order),
              onTake: () => this.onTakeOrder(order.id)
            })
          }
        }, (order: ErrandsOrder) => order.id.toString())
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
    }
  }
  
  @Builder
  TypeFilter(label: string, type: string) {
    Text(label)
      .fontSize(13)
      .fontColor(this.selectedType === type ? '#FFFFFF' : '#666666')
      .padding({ left: 14, right: 14, top: 6, bottom: 6 })
      .backgroundColor(this.selectedType === type ? '#4D7C0F' : '#FFFFFF')
      .borderRadius(16)
      .onClick(() => {
        this.selectedType = type;
      })
  }
}
订单卡片组件

订单卡片是列表的基本单元,展示订单的关键信息和操作按钮。

TypeScript 复制代码
@Component
struct OrderCard {
  @Prop order: ErrandsOrder;
  onClick: () => void;
  onTake: () => void;
  
  // 订单类型图标映射
  private getTypeIcon(type: OrderType): string {
    const icons: Record<OrderType, string> = {
      takeout: '🍔',
      express: '📦',
      grocery: '🛒',
      document: '📄',
      queue: '⏰',
      other: '📝'
    };
    return icons[type] || '📝';
  }
  
  // 订单类型名称映射
  private getTypeName(type: OrderType): string {
    const names: Record<OrderType, string> = {
      takeout: '外卖代取',
      express: '快递代取',
      grocery: '超市代购',
      document: '文件代办',
      queue: '排队占座',
      other: '其他跑腿'
    };
    return names[type] || '其他';
  }
  
  // 状态颜色
  private getStatusColor(status: OrderStatus): string {
    const colors: Record<OrderStatus, string> = {
      pending: '#F59E0B',
      accepted: '#3B82F6',
      delivering: '#8B5CF6',
      completed: '#10B981',
      cancelled: '#9CA3AF'
    };
    return colors[status] || '#9CA3AF';
  }
  
  // 状态文字
  private getStatusText(status: OrderStatus): string {
    const texts: Record<OrderStatus, string> = {
      pending: '待接单',
      accepted: '已接单',
      delivering: '配送中',
      completed: '已完成',
      cancelled: '已取消'
    };
    return texts[status] || '未知';
  }
  
  build() {
    Column() {
      // 顶部:类型 + 状态 + 加急
      Row() {
        Row() {
          Text(this.getTypeIcon(this.order.type))
            .fontSize(16)
          Text(this.getTypeName(this.order.type))
            .fontSize(13)
            .fontColor('#4D7C0F')
            .fontWeight(FontWeight.Medium)
            .margin({ left: 4 })
        }
        .layoutWeight(1)
        
        // 加急标签
        if (this.order.isUrgent) {
          Text('加急')
            .fontSize(10)
            .fontColor('#FFFFFF')
            .backgroundColor('#EF4444')
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(8)
            .margin({ right: 6 })
        }
        
        // 状态标签
        Text(this.getStatusText(this.order.status))
          .fontSize(11)
          .fontColor('#FFFFFF')
          .backgroundColor(this.getStatusColor(this.order.status))
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .borderRadius(10)
      }
      .width('100%')
      
      // 订单标题
      Text(this.order.title)
        .fontSize(15)
        .fontWeight(FontWeight.Medium)
        .fontColor('#333333')
        .margin({ top: 10 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width('100%')
      
      // 路线信息
      Column({ space: 6 }) {
        Row() {
          Circle({ width: 8, height: 8 })
            .fill('#10B981')
          Text(this.order.startLocation)
            .fontSize(13)
            .fontColor('#666666')
            .margin({ left: 8 })
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .layoutWeight(1)
        }
        
        // 连接线
        Line()
          .width(1)
          .height(12)
          .backgroundColor('#E5E7EB')
          .margin({ left: 3.5 })
        
        Row() {
          Circle({ width: 8, height: 8 })
            .fill('#EF4444')
          Text(this.order.endLocation)
            .fontSize(13)
            .fontColor('#666666')
            .margin({ left: 8 })
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .layoutWeight(1)
        }
      }
      .width('100%')
      .margin({ top: 10 })
      
      // 底部:发布者 + 赏金 + 操作按钮
      Row() {
        // 发布者信息
        Row() {
          Text(this.order.publisher)
            .fontSize(12)
            .fontColor('#999999')
          Text('·')
            .fontSize(12)
            .fontColor('#E5E7EB')
            .margin({ left: 4, right: 4 })
          Text(this.order.publishTime)
            .fontSize(12)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        
        // 赏金
        Row() {
          Text('¥')
            .fontSize(12)
            .fontColor('#EF4444')
            .fontWeight(FontWeight.Bold)
          Text(this.order.reward.toString())
            .fontSize(20)
            .fontColor('#EF4444')
            .fontWeight(FontWeight.Bold)
          if (this.order.tip > 0) {
            Text(`+${this.order.tip}`)
              .fontSize(12)
              .fontColor('#F59E0B')
              .margin({ left: 2 })
          }
        }
        
        // 接单按钮(仅待接单状态显示)
        if (this.order.status === 'pending') {
          Button('接单')
            .width(60)
            .height(32)
            .margin({ left: 12 })
            .borderRadius(16)
            .backgroundColor('#4D7C0F')
            .fontColor('#FFFFFF')
            .fontSize(13)
            .onClick(() => {
              this.onTake();
            })
        }
      }
      .width('100%')
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .onClick(() => {
      this.onClick();
    })
  }
}

设计要点

  • 路线可视化:用两个圆点 + 连接线直观展示起点到终点,比纯文字更清晰

  • 状态颜色编码:每种状态对应不同颜色,一眼识别订单进度

  • 赏金突出显示:红色大字 + ¥ 符号,吸引跑腿员注意

  • 加急标签:红色背景,优先级最高的视觉信号

  • 接单按钮:仅在"待接单"状态显示,避免误操作

5.5 我的订单 Tab

"我的订单"页面分为"我发布的"和"我接的"两个子 Tab。

TypeScript 复制代码
@Component
struct MyOrders {
  @State orders: ErrandsOrder[] = [];
  @State subTab: number = 0; // 0: 我发布的 1: 我接的
  onOrderClick: (order: ErrandsOrder) => void;
  
  private get myPublishedOrders(): ErrandsOrder[] {
    return this.orders.filter(o => o.publisher === '我');
  }
  
  private get myAcceptedOrders(): ErrandsOrder[] {
    return this.orders.filter(o => o.runner === '我');
  }
  
  private get displayOrders(): ErrandsOrder[] {
    return this.subTab === 0 ? this.myPublishedOrders : this.myAcceptedOrders;
  }
  
  build() {
    Column() {
      // 子 Tab
      Row() {
        this.SubTabItem('我发布的', 0, this.myPublishedOrders.length)
        this.SubTabItem('我接的', 1, this.myAcceptedOrders.length)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      
      // 订单列表
      List({ space: 12 }) {
        ForEach(this.displayOrders, (order: ErrandsOrder) => {
          ListItem() {
            OrderCard({
              order: order,
              onClick: () => this.onOrderClick(order),
              onTake: () => {}
            })
          }
        }, (order: ErrandsOrder) => order.id.toString())
        
        if (this.displayOrders.length === 0) {
          ListItem() {
            Column() {
              Text('📭')
                .fontSize(48)
              Text('暂无订单')
                .fontSize(14)
                .fontColor('#999999')
                .margin({ top: 12 })
            }
            .width('100%')
            .padding(40)
            .alignItems(HorizontalAlign.Center)
          }
        }
      }
      .width('100%')
      .padding(16)
    }
  }
  
  @Builder
  SubTabItem(label: string, index: number, count: number) {
    Column() {
      Text(`${label} (${count})`)
        .fontSize(14)
        .fontColor(this.subTab === index ? '#4D7C0F' : '#666666')
        .fontWeight(this.subTab === index ? FontWeight.Bold : FontWeight.Normal)
      
      if (this.subTab === index) {
        Divider()
          .width(40)
          .height(3)
          .color('#4D7C0F')
          .borderRadius(2)
          .margin({ top: 8 })
      } else {
        Divider()
          .width(40)
          .height(3)
          .color(Color.Transparent)
          .margin({ top: 8 })
      }
    }
    .width('50%')
    .padding({ top: 12, bottom: 8 })
    .alignItems(HorizontalAlign.Center)
    .onClick(() => {
      this.subTab = index;
    })
  }
}

5.6 跑腿员排行

展示跑腿员接单量排行榜,增加竞争和激励。

TypeScript 复制代码
@Component
struct RunnerRank {
  @State runners: Runner[] = [];
  
  build() {
    Column() {
      // 标题
      Row() {
        Text('🏆 跑腿员排行榜')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('本周')
          .fontSize(12)
          .fontColor('#999999')
          .margin({ left: 8 })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 12 })
      
      // 前三名展示(卡片式)
      Row() {
        // 第二名(左)
        this.TopRunnerCard(this.runners[1], 2, '#9CA3AF', 100)
        // 第一名(中,更高)
        this.TopRunnerCard(this.runners[0], 1, '#F59E0B', 120)
        // 第三名(右)
        this.TopRunnerCard(this.runners[2], 3, '#D97706', 100)
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .justifyContent(FlexAlign.SpaceAround)
      .alignItems(VerticalAlign.Bottom)
      
      // 4-10 名列表
      List({ space: 8 }) {
        ForEach(this.runners.slice(3, 10), (runner: Runner, index: number) => {
          ListItem() {
            this.RunnerListItem(runner, index + 4)
          }
        }, (runner: Runner) => runner.id.toString())
      }
      .width('100%')
      .padding(16)
    }
  }
  
  @Builder
  TopRunnerCard(runner: Runner | undefined, rank: number, color: string, height: number) {
    if (!runner) return;
    
    Column() {
      // 排名数字
      Text(rank.toString())
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width(32)
        .height(32)
        .textAlign(TextAlign.Center)
        .backgroundColor(color)
        .borderRadius(16)
      
      // 头像
      Text(runner.name.charAt(0))
        .fontSize(24)
        .fontColor('#FFFFFF')
        .width(56)
        .height(56)
        .textAlign(TextAlign.Center)
        .backgroundColor('#4D7C0F')
        .borderRadius(28)
        .margin({ top: 8 })
      
      // 昵称
      Text(runner.name)
        .fontSize(13)
        .fontColor('#333333')
        .fontWeight(FontWeight.Medium)
        .margin({ top: 8 })
        .maxLines(1)
      
      // 完成数
      Text(`${runner.orderCount} 单`)
        .fontSize(12)
        .fontColor('#4D7C0F')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 2 })
      
      // 评分
      Row() {
        Text('⭐')
          .fontSize(12)
        Text(runner.rating.toFixed(1))
          .fontSize(12)
          .fontColor('#F59E0B')
      }
      .margin({ top: 4 })
    }
    .width('30%')
    .height(height)
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Center)
  }
  
  @Builder
  RunnerListItem(runner: Runner, rank: number) {
    Row() {
      // 排名
      Text(rank.toString())
        .fontSize(14)
        .fontColor('#999999')
        .width(24)
        .textAlign(TextAlign.Center)
      
      // 头像
      Text(runner.name.charAt(0))
        .fontSize(16)
        .fontColor('#FFFFFF')
        .width(36)
        .height(36)
        .textAlign(TextAlign.Center)
        .backgroundColor('#84CC16')
        .borderRadius(18)
        .margin({ left: 8 })
      
      // 信息
      Column() {
        Text(runner.name)
          .fontSize(14)
          .fontColor('#333333')
          .fontWeight(FontWeight.Medium)
        Row() {
          Text(`${runner.orderCount} 单`)
            .fontSize(12)
            .fontColor('#999999')
          Text('·')
            .fontSize(12)
            .fontColor('#E5E7EB')
            .margin({ left: 6, right: 6 })
          Text(`⭐ ${runner.rating.toFixed(1)}`)
            .fontSize(12)
            .fontColor('#F59E0B')
        }
        .margin({ top: 2 })
      }
      .layoutWeight(1)
      .margin({ left: 12 })
      .alignItems(HorizontalAlign.Start)
      
      // 等级
      Text(this.getLevelLabel(runner.level))
        .fontSize(11)
        .fontColor('#FFFFFF')
        .backgroundColor(this.getLevelColor(runner.level))
        .padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .borderRadius(10)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(10)
  }
  
  private getLevelLabel(level: RunnerLevel): string {
    const labels: Record<RunnerLevel, string> = {
      bronze: '青铜',
      silver: '白银',
      gold: '黄金',
      platinum: '铂金',
      diamond: '钻石'
    };
    return labels[level] || '青铜';
  }
  
  private getLevelColor(level: RunnerLevel): string {
    const colors: Record<RunnerLevel, string> = {
      bronze: '#CD7F32',
      silver: '#A0A0A0',
      gold: '#FFD700',
      platinum: '#E5E4E2',
      diamond: '#B9F2FF'
    };
    return colors[level] || '#CD7F32';
  }
}

5.7 发布订单弹窗

发布订单是核心操作,表单包含类型选择、描述、地点、赏金等字段。

TypeScript 复制代码
@CustomDialog
struct PublishDialog {
  controller: CustomDialogController;
  onConfirm: (order: Partial<ErrandsOrder>) => void;
  
  @State selectedType: OrderType = 'takeout';
  @State title: string = '';
  @State description: string = '';
  @State startLocation: string = '';
  @State endLocation: string = '';
  @State reward: string = '5';
  @State tip: string = '0';
  @State isUrgent: boolean = false;
  
  private types: { type: OrderType; label: string; icon: string }[] = [
    { type: 'takeout', label: '外卖', icon: '🍔' },
    { type: 'express', label: '快递', icon: '📦' },
    { type: 'grocery', label: '超市', icon: '🛒' },
    { type: 'document', label: '文件', icon: '📄' },
    { type: 'queue', label: '排队', icon: '⏰' },
    { type: 'other', label: '其他', icon: '📝' }
  ];
  
  build() {
    Column() {
      Text('发布跑腿')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 20 })
      
      // 类型选择
      Text('订单类型')
        .fontSize(14)
        .fontColor('#666666')
        .alignSelf(ItemAlign.Start)
        .margin({ bottom: 8 })
      
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
        ForEach(this.types, (item: { type: OrderType; label: string; icon: string }) => {
          Column() {
            Text(item.icon)
              .fontSize(24)
            Text(item.label)
              .fontSize(12)
              .fontColor(this.selectedType === item.type ? '#4D7C0F' : '#666666')
              .margin({ top: 4 })
          }
          .width('30%')
          .padding(10)
          .margin({ bottom: 8, right: '3%' })
          .backgroundColor(this.selectedType === item.type ? '#F7FEE7' : '#F9FAFB')
          .borderRadius(10)
          .border({
            width: this.selectedType === item.type ? 2 : 0,
            color: '#4D7C0F'
          })
          .alignItems(HorizontalAlign.Center)
          .onClick(() => {
            this.selectedType = item.type;
          })
        }, (item) => item.type)
      }
      .width('100%')
      
      // 标题
      Text('订单标题')
        .fontSize(14)
        .fontColor('#666666')
        .alignSelf(ItemAlign.Start)
        .margin({ top: 16, bottom: 6 })
      TextInput({ placeholder: '简短描述你的需求', text: this.title })
        .height(44)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .onChange((v: string) => { this.title = v; })
      
      // 详细描述
      Text('详细描述')
        .fontSize(14)
        .fontColor('#666666')
        .alignSelf(ItemAlign.Start)
        .margin({ top: 12, bottom: 6 })
      TextArea({ placeholder: '请详细描述需求,如商品名称、规格、数量等', text: this.description })
        .height(72)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .padding(12)
        .onChange((v: string) => { this.description = v; })
      
      // 取货地点
      Text('取货地点')
        .fontSize(14)
        .fontColor('#666666')
        .alignSelf(ItemAlign.Start)
        .margin({ top: 12, bottom: 6 })
      TextInput({ placeholder: '如:东门菜鸟驿站', text: this.startLocation })
        .height(44)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .onChange((v: string) => { this.startLocation = v; })
      
      // 送达地点
      Text('送达地点')
        .fontSize(14)
        .fontColor('#666666')
        .alignSelf(ItemAlign.Start)
        .margin({ top: 12, bottom: 6 })
      TextInput({ placeholder: '如:3号楼502宿舍', text: this.endLocation })
        .height(44)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .onChange((v: string) => { this.endLocation = v; })
      
      // 赏金设置
      Row() {
        Column() {
          Text('跑腿费')
            .fontSize(14)
            .fontColor('#666666')
            .alignSelf(ItemAlign.Start)
            .margin({ bottom: 6 })
          TextInput({ placeholder: '0', text: this.reward })
            .type(InputType.Number)
            .height(44)
            .backgroundColor('#F5F5F5')
            .borderRadius(8)
            .padding({ left: 12, right: 12 })
            .onChange((v: string) => { this.reward = v; })
        }
        .layoutWeight(1)
        
        Column() {
          Text('小费')
            .fontSize(14)
            .fontColor('#666666')
            .alignSelf(ItemAlign.Start)
            .margin({ bottom: 6 })
          TextInput({ placeholder: '0', text: this.tip })
            .type(InputType.Number)
            .height(44)
            .backgroundColor('#F5F5F5')
            .borderRadius(8)
            .padding({ left: 12, right: 12 })
            .onChange((v: string) => { this.tip = v; })
        }
        .layoutWeight(1)
        .margin({ left: 12 })
      }
      .width('100%')
      .margin({ top: 12 })
      
      // 加急开关
      Row() {
        Text('加急订单')
          .fontSize(14)
          .fontColor('#333333')
          .layoutWeight(1)
        Toggle({ type: ToggleType.Switch, isOn: this.isUrgent })
          .onChange((value: boolean) => {
            this.isUrgent = value;
          })
      }
      .width('100%')
      .margin({ top: 16 })
      
      // 提交按钮
      Button('立即发布')
        .width('100%')
        .height(48)
        .margin({ top: 24 })
        .borderRadius(24)
        .backgroundColor('#4D7C0F')
        .fontColor('#FFFFFF')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .enabled(this.canSubmit())
        .opacity(this.canSubmit() ? 1 : 0.5)
        .onClick(() => {
          this.onConfirm({
            type: this.selectedType,
            title: this.title,
            description: this.description,
            startLocation: this.startLocation,
            endLocation: this.endLocation,
            reward: parseInt(this.reward) || 0,
            tip: parseInt(this.tip) || 0,
            isUrgent: this.isUrgent
          });
          this.controller.close();
        })
    }
    .width('100%')
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
  }
  
  private canSubmit(): boolean {
    return this.title.trim().length > 0 
      && this.startLocation.trim().length > 0
      && this.endLocation.trim().length > 0
      && parseInt(this.reward) > 0;
  }
}

5.8 订单详情弹窗

点击订单卡片弹出详情,展示完整信息和可用操作。

TypeScript 复制代码
@CustomDialog
struct OrderDetailDialog {
  controller: CustomDialogController;
  @Prop order: ErrandsOrder | null;
  onTakeOrder: () => void;
  onCompleteOrder: () => void;
  onCancelOrder: () => void;
  
  build() {
    if (!this.order) return;
    
    Scroll() {
      Column() {
        // 顶部状态
        Column() {
          Text(this.getStatusText(this.order!.status))
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.getStatusColor(this.order!.status))
          
          Text(`订单号:${this.order!.id}`)
            .fontSize(12)
            .fontColor('#999999')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(20)
        .backgroundColor(this.getStatusBgColor(this.order!.status))
        .borderRadius(12)
        .alignItems(HorizontalAlign.Center)
        
        // 订单信息
        this.SectionTitle('订单信息')
        
        this.InfoRow('类型', this.getTypeName(this.order!.type))
        this.InfoRow('标题', this.order!.title)
        this.InfoRow('描述', this.order!.description, true)
        this.InfoRow('取货地点', this.order!.startLocation)
        this.InfoRow('送达地点', this.order!.endLocation)
        this.InfoRow('期望时间', this.order!.expectTime)
        this.InfoRow('发布时间', this.order!.publishTime)
        if (this.order!.remark) {
          this.InfoRow('备注', this.order!.remark, true)
        }
        
        // 费用信息
        this.SectionTitle('费用明细')
        this.InfoRow('跑腿费', `¥${this.order!.reward}`)
        if (this.order!.tip > 0) {
          this.InfoRow('小费', `¥${this.order!.tip}`)
        }
        this.InfoRow('合计', `¥${this.order!.reward + this.order!.tip}`, false, true)
        
        // 发布者信息
        this.SectionTitle('发布者')
        Row() {
          Text(this.order!.publisher.charAt(0))
            .fontSize(20)
            .fontColor('#FFFFFF')
            .width(40)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#4D7C0F')
            .borderRadius(20)
          
          Column() {
            Text(this.order!.publisher)
              .fontSize(15)
              .fontColor('#333333')
              .fontWeight(FontWeight.Medium)
            Text('发布者')
              .fontSize(12)
              .fontColor('#999999')
              .margin({ top: 2 })
          }
          .margin({ left: 12 })
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F9FAFB')
        .borderRadius(10)
        
        // 操作按钮
        if (this.order!.status === 'pending') {
          Button('立即接单')
            .width('100%')
            .height(48)
            .margin({ top: 20 })
            .borderRadius(24)
            .backgroundColor('#4D7C0F')
            .fontColor('#FFFFFF')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
              this.onTakeOrder();
              this.controller.close();
            })
        } else if (this.order!.status === 'delivering' && this.order!.runner === '我') {
          Button('确认送达')
            .width('100%')
            .height(48)
            .margin({ top: 20 })
            .borderRadius(24)
            .backgroundColor('#10B981')
            .fontColor('#FFFFFF')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .onClick(() => {
              this.onCompleteOrder();
              this.controller.close();
            })
        }
        
        if (this.order!.status === 'pending' && this.order!.publisher === '我') {
          Button('取消订单')
            .width('100%')
            .height(44)
            .margin({ top: 12 })
            .borderRadius(22)
            .backgroundColor('#FEF2F2')
            .fontColor('#EF4444')
            .fontSize(14)
            .onClick(() => {
              this.onCancelOrder();
              this.controller.close();
            })
        }
      }
      .width('100%')
    }
    .layoutWeight(1)
  }
  
  @Builder
  SectionTitle(title: string) {
    Text(title)
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
      .width('100%')
      .margin({ top: 20, bottom: 10 })
  }
  
  @Builder
  InfoRow(label: string, value: string, isMultiLine: boolean = false, isHighlight: boolean = false) {
    Row() {
      Text(label)
        .fontSize(13)
        .fontColor('#999999')
        .width(80)
      
      Text(value)
        .fontSize(13)
        .fontColor(isHighlight ? '#EF4444' : '#333333')
        .fontWeight(isHighlight ? FontWeight.Bold : FontWeight.Normal)
        .layoutWeight(1)
        .textAlign(TextAlign.End)
        .maxLines(isMultiLine ? 3 : 1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .width('100%')
    .padding({ top: 8, bottom: 8 })
  }
  
  // 辅助方法(同 OrderCard 中的方法,此处省略重复代码)
  private getStatusColor(status: OrderStatus): string { ... }
  private getStatusText(status: OrderStatus): string { ... }
  private getStatusBgColor(status: OrderStatus): string { ... }
  private getTypeName(type: OrderType): string { ... }
}

六、核心功能实现详解

6.1 发布跑腿订单

发布订单是业务流程的起点。用户填写表单后,系统创建订单并加入大厅。

TypeScript 复制代码
/**
 * 发布新的跑腿订单
 */
private publishOrder(orderData: Partial<ErrandsOrder>): void {
  // 1. 校验
  if (!orderData.title || !orderData.startLocation || !orderData.endLocation) {
    console.warn('请填写完整的订单信息');
    return;
  }
  
  const reward = orderData.reward || 0;
  if (reward <= 0) {
    console.warn('跑腿费必须大于0');
    return;
  }
  
  // 2. 检查余额
  const totalCost = reward + (orderData.tip || 0);
  if (this.balance < totalCost) {
    console.warn('余额不足,请先充值');
    return;
  }
  
  // 3. 扣除费用(冻结)
  this.balance -= totalCost;
  
  // 4. 创建订单
  const newOrder: ErrandsOrder = {
    id: this.nextOrderId,
    type: orderData.type || 'other',
    title: orderData.title || '',
    description: orderData.description || '',
    startLocation: orderData.startLocation || '',
    endLocation: orderData.endLocation || '',
    reward: reward,
    tip: orderData.tip || 0,
    publishTime: this.formatDate(new Date()),
    expectTime: orderData.expectTime || '1小时内',
    publisher: '我',
    publisherAvatar: '',
    runner: '',
    runnerAvatar: '',
    status: 'pending',
    remark: orderData.remark || '',
    isUrgent: orderData.isUrgent || false,
    weight: orderData.weight || '小件',
    review: null
  };
  
  // 5. 加入列表(加急订单排前面)
  if (newOrder.isUrgent) {
    this.orders = [newOrder, ...this.orders];
  } else {
    // 找到第一个非加急的位置插入
    const insertIndex = this.orders.findIndex(o => !o.isUrgent);
    if (insertIndex === -1) {
      this.orders = [...this.orders, newOrder];
    } else {
      this.orders = [
        ...this.orders.slice(0, insertIndex),
        newOrder,
        ...this.orders.slice(insertIndex)
      ];
    }
  }
  
  // 6. 更新计数器和统计
  this.nextOrderId++;
  this.hallStats.pendingCount++;
}

关键设计

  • 费用预冻结:发布时即扣除费用,避免完成后付不起钱

  • 加急优先:加急订单自动排在列表前面,提高曝光

  • 余额校验:发布前检查余额,防止透支

6.2 接单与抢单

跑腿员在大厅浏览订单,点击接单后订单状态变为"已接单"。

TypeScript 复制代码
/**
 * 接单
 * @param orderId 订单 ID
 */
private takeOrder(orderId: number): void {
  const order = this.orders.find(o => o.id === orderId);
  if (!order) return;
  
  // 1. 检查订单状态
  if (order.status !== 'pending') {
    console.warn('该订单已被接单');
    return;
  }
  
  // 2. 检查是否已认证
  if (!this.isCertifiedRunner) {
    console.warn('请先完成跑腿员认证');
    return;
  }
  
  // 3. 更新订单状态
  this.orders = this.orders.map(o =>
    o.id === orderId
      ? {
          ...o,
          status: 'accepted' as OrderStatus,
          runner: '我',
          runnerAvatar: ''
        }
      : o
  );
  
  // 4. 更新统计
  this.hallStats.pendingCount--;
  this.hallStats.inProgressCount++;
}

为什么需要认证?

  • 保证服务质量:认证跑腿员有基本的信誉保障

  • 安全考虑:实名认证降低风险

  • 平台运营:认证体系便于管理和分级

6.3 订单状态流转

订单状态机是核心业务逻辑,需要清晰定义每个状态的转换条件。

Plaintext 复制代码
┌──────────┐
                    │  pending │  待接单
                    └────┬─────┘
                         │ 接单
                         ▼
                    ┌──────────┐
                    │ accepted │  已接单(取货中)
                    └────┬─────┘
                         │ 取货完成
                         ▼
                   ┌────────────┐
                   │ delivering │  配送中
                   └─────┬──────┘
                         │ 送达确认
                         ▼
                   ┌────────────┐
                   │ completed  │  已完成
                   └────────────┘
                    
          pending / accepted  ──取消──→  cancelled
TypeScript 复制代码
/**
 * 更新订单状态
 * @param orderId 订单 ID
 * @param newStatus 新状态
 */
private updateOrderStatus(orderId: number, newStatus: OrderStatus): void {
  this.orders = this.orders.map(o => {
    if (o.id !== orderId) return o;
    
    // 状态流转校验
    if (!this.canTransition(o.status, newStatus)) {
      console.warn(`无法从 ${o.status} 转换到 ${newStatus}`);
      return o;
    }
    
    const updated = { ...o, status: newStatus };
    
    // 完成订单时的特殊处理
    if (newStatus === 'completed') {
      // 结算费用给跑腿员
      this.balance += o.reward + o.tip;
      this.hallStats.todayCompleted++;
      this.hallStats.myEarnings += o.reward + o.tip;
      this.hallStats.inProgressCount--;
      
      // 更新跑腿员数据
      this.updateRunnerStats(o.runner, o.reward + o.tip);
    }
    
    // 取消订单时的特殊处理
    if (newStatus === 'cancelled' && o.status === 'pending') {
      // 退款给发布者
      this.balance += o.reward + o.tip;
      this.hallStats.pendingCount--;
    }
    
    return updated;
  });
}

/**
 * 检查状态转换是否合法
 */
private canTransition(current: OrderStatus, target: OrderStatus): boolean {
  const validTransitions: Record<OrderStatus, OrderStatus[]> = {
    pending: ['accepted', 'cancelled'],
    accepted: ['delivering', 'cancelled'],
    delivering: ['completed'],
    completed: [],
    cancelled: []
  };
  return validTransitions[current]?.includes(target) ?? false;
}

状态机设计的好处

  1. 清晰可控:每个状态只能转换到特定状态,不会出现非法状态

  2. 易于扩展:新增状态只需修改转换规则

  3. 便于调试:状态异常时容易定位问题

  4. 符合业务:真实订单流程就是一个状态机

6.4 订单完成与评价

订单完成后,发布者可以对跑腿员进行评价。

TypeScript 复制代码
/**
 * 提交评价
 */
private submitReview(orderId: number, rating: number, content: string, tags: string[]): void {
  const review: Review = {
    id: this.nextReviewId++,
    orderId,
    rating,
    content,
    from: '我',
    to: '', // 跑腿员名字
    time: this.formatDate(new Date()),
    tags
  };
  
  this.orders = this.orders.map(o =>
    o.id === orderId ? { ...o, review } : o
  );
  
  // 更新跑腿员评分
  this.updateRunnerRating(review.to, rating);
}

/**
 * 更新跑腿员评分(加权平均)
 */
private updateRunnerRating(runnerName: string, newRating: number): void {
  this.runners = this.runners.map(r => {
    if (r.name !== runnerName) return r;
    const totalRating = r.rating * r.orderCount + newRating;
    const newCount = r.orderCount + 1;
    return {
      ...r,
      rating: totalRating / newCount,
      orderCount: newCount
    };
  });
}

6.5 跑腿员认证

TypeScript 复制代码
/**
 * 提交跑腿员认证
 */
private submitCertification(name: string, studentId: string, phone: string): void {
  // 模拟认证流程
  if (!name || !studentId || !phone) {
    console.warn('请填写完整信息');
    return;
  }
  
  this.isCertifiedRunner = true;
  this.runnerProfile = {
    id: 999,
    name,
    avatar: '',
    rating: 5.0,
    orderCount: 0,
    totalEarnings: 0,
    isCertified: true,
    level: 'bronze',
    completedToday: 0
  };
}

6.6 余额与支付模拟

TypeScript 复制代码
@State balance: number = 100; // 初始余额 100 元

/**
 * 充值
 */
private recharge(amount: number): void {
  if (amount <= 0) return;
  this.balance += amount;
}

/**
 * 提现
 */
private withdraw(amount: number): boolean {
  if (amount <= 0 || amount > this.balance) {
    return false;
  }
  this.balance -= amount;
  return true;
}

七、样式与交互动效

7.1 主题色系统

应用采用深黄绿为主色调,传递"高效、可靠、活力"的服务感:

用途 色值 说明
主色 #4D7C0F 深黄绿,按钮、标题、重点元素
主色浅 #84CC16 亮黄绿,渐变、高亮
主色极浅 #F7FEE7 浅绿背景
辅助色-橙 #F59E0B 待接单状态、加急
辅助色-蓝 #3B82F6 已接单状态
辅助色-紫 #8B5CF6 配送中状态
辅助色-绿 #10B981 已完成状态
辅助色-红 #EF4444 取消、错误、价格
文字主色 #333333 正文
文字次色 #666666 辅助信息
文字弱色 #999999 次要信息
背景色 #F7FEE7 页面背景
卡片背景 #FFFFFF 卡片、输入框

7.2 状态颜色体系

订单的 5 种状态各有专属颜色,形成统一的视觉语言:

状态 颜色 语义
待接单 🟠 橙色 #F59E0B 等待中,需要关注
已接单 🔵 蓝色 #3B82F6 进行中,正在处理
配送中 🟣 紫色 #8B5CF6 在路上,即将到达
已完成 🟢 绿色 #10B981 完成,成功
已取消 ⚪ 灰色 #9CA3AF 结束,无效

7.3 交互动效

TypeScript 复制代码
// 接单按钮点击动画
private animateTakeButton(): void {
  animateTo({
    duration: 200,
    curve: Curve.EaseInOut,
    onFinish: () => {
      animateTo({
        duration: 200,
        curve: Curve.EaseInOut
      }, () => {
        this.buttonScale = 1;
      });
    }
  }, () => {
    this.buttonScale = 0.95;
  });
}

// 订单状态变更过渡
private animateStatusChange(): void {
  animateTo({
    duration: 300,
    curve: Curve.EaseOut
  }, () => {
    this.statusOpacity = 1;
  });
}

八、完整代码结构

由于篇幅限制,以上展示了核心组件和逻辑。完整的 Index.ets 文件包含所有组件定义、状态管理、业务逻辑和模拟数据,可直接在 DevEco Studio 中运行。

TypeScript 复制代码
// Index.ets 完整结构概览

@Entry
@Component
struct Index {
  // ===== 状态定义 =====
  @State orders: ErrandsOrder[] = [];
  @State runners: Runner[] = [];
  @State currentTab: number = 0;
  @State balance: number = 100;
  @State nextOrderId: number = 30;
  @State nextReviewId: number = 1;
  @State isCertifiedRunner: boolean = false;
  @State hallStats: HallStats = { ... };
  @State showPublishDialog: boolean = false;
  @State showDetailDialog: boolean = false;
  @State selectedOrder: ErrandsOrder | null = null;
  
  // ===== 生命周期 =====
  aboutToAppear() {
    this.initMockData();
  }
  
  // ===== 业务方法 =====
  private publishOrder(...) { ... }
  private takeOrder(...) { ... }
  private updateOrderStatus(...) { ... }
  private canTransition(...) { ... }
  private submitReview(...) { ... }
  private submitCertification(...) { ... }
  private recharge(...) { ... }
  private withdraw(...) { ... }
  
  // ===== 页面构建 =====
  build() {
    Column() {
      PublishHeader({ ... })
      if (this.currentTab === 0) { StatsOverview({ ... }) }
      Scroll() { /* 内容区 */ }
      BottomTabBar({ ... })
    }
  }
}

// ===== 子组件 =====
@Component struct PublishHeader { ... }
@Component struct StatsOverview { ... }
@Component struct OrderList { ... }
@Component struct OrderCard { ... }
@Component struct MyOrders { ... }
@Component struct RunnerRank { ... }
@Component struct BottomTabBar { ... }
@CustomDialog struct PublishDialog { ... }
@CustomDialog struct OrderDetailDialog { ... }

// ===== 接口与类型 =====
interface ErrandsOrder { ... }
interface Runner { ... }
interface Review { ... }
interface HallStats { ... }
type OrderType = ...;
type OrderStatus = ...;
type RunnerLevel = ...;

九、运行与调试

9.1 运行步骤

  1. 打开 DevEco Studio,新建 HarmonyOS 项目(Empty Ability 模板)

  2. entry/src/main/ets/pages/Index.ets 替换为完整代码

  3. resources/base/media 中添加图标资源(搜索、铃铛等)

  4. 连接 HarmonyOS 设备或启动模拟器

  5. 点击运行按钮 ▶️

9.2 常见问题

Q: 列表数据不更新?

  • A: ArkTS 中 @State 装饰的数组需要整体替换才能触发更新。使用 mapfilter、展开运算符等方式返回新数组,不要直接修改元素属性。

Q: 自定义弹窗不显示?

  • A: 确保 CustomDialogController 已正确初始化,且 @CustomDialog 装饰器已添加到组件上。调用 controller.open() 打开弹窗。

Q: 页面内容超出屏幕?

  • A: 使用 Scroll 组件包裹可滚动内容,固定区域放在 Scroll 外面。使用 layoutWeight(1) 让 Scroll 自动填充剩余空间。

Q: 类型报错?

  • A: 确保所有 interfacetype 定义在使用之前,或者放在文件底部但在使用处前向声明。ArkTS 对类型检查较严格。

Q: 图片资源引用报错?

  • A: 确认图片文件名使用小写字母和下划线,放在 resources/base/media 目录下,引用格式为 $r('app.media.xxx')

9.3 调试技巧

  1. Previewer 预览:DevEco Studio 内置 Previewer,可实时预览 UI 效果,支持热重载

  2. console 调试 :使用 console.log() 输出状态变化,在 HiLog 中查看

  3. 组件树检查:使用 DevTools 查看组件层级和属性

  4. 状态快照:在关键节点打印状态快照,追踪数据流


十、技术要点总结

通过本项目,你将掌握以下 ArkTS 核心技能:

技术点 应用场景 重要程度
@State 状态管理 所有动态数据驱动 ⭐⭐⭐⭐⭐
@Component 自定义组件 模块化 UI 开发 ⭐⭐⭐⭐⭐
@Prop 父子组件通信 组件间数据传递 ⭐⭐⭐⭐⭐
@CustomDialog 自定义弹窗 表单、详情、确认框 ⭐⭐⭐⭐
@Builder 构建函数 UI 片段复用 ⭐⭐⭐⭐
List + ForEach 列表 订单列表、排行榜 ⭐⭐⭐⭐⭐
条件渲染(if/else) Tab 切换、状态显示 ⭐⭐⭐⭐
状态机模式 订单状态流转 ⭐⭐⭐⭐
Flex 弹性布局 类型选择、自适应布局 ⭐⭐⭐⭐
事件回调 子组件向父组件通信 ⭐⭐⭐⭐⭐

业务知识收获

除了技术技能,本项目还涵盖了丰富的业务知识:

  • 订单全生命周期管理:从发布到完成的完整流程

  • 双角色系统设计:发布者和跑腿员的不同视角

  • 支付与结算:余额、冻结、结算的资金流转

  • 评价体系:评分、标签、加权平均计算

  • 等级与激励:跑腿员等级体系的设计思路


十一、扩展方向

本项目作为学习示例,还有大量可以扩展和深化的方向:

功能扩展

  1. 实时位置追踪:接入地图 SDK,实时显示跑腿员位置

  2. 即时通讯:发布者与跑腿员的聊天功能

  3. 跑腿保险:为高价值订单提供保险服务

  4. 超时赔付:超时自动赔付,保障用户体验

  5. 校园专送:固定路线的批量配送服务

  6. 物品拍照:取货时拍照留证,减少纠纷

  7. 语音下单:语音识别发布订单

  8. 智能推荐:根据跑腿员位置和偏好推荐订单

  9. 拼单功能:同一路线的订单可以拼单配送

  10. 会员体系:会员享受优先接单、折扣等权益

技术优化

  1. 数据持久化 :使用 @ohos.data.relationalStore 数据库保存数据

  2. 网络请求:接入后端 API,实现真实数据交互

  3. 多页面架构 :使用 Navigation + Router 拆分为多个页面

  4. 全局状态管理 :引入 @ohos.arkui.observed 或 Redux 模式

  5. 性能优化LazyForEach 懒加载、虚拟列表

  6. 推送通知:订单状态变更时推送通知

  7. 深色模式:适配系统深色模式

  8. 国际化:支持多语言

架构升级

  1. MVVM 架构:引入 ViewModel 层,分离业务逻辑和 UI

  2. Repository 模式:数据层抽象,支持本地和远程数据源切换

  3. 依赖注入:使用依赖注入容器管理组件依赖

  4. 单元测试:为业务逻辑编写单元测试

  5. 组件库:抽离通用组件,建立项目 UI 组件库

后端对接

  1. 用户系统:注册、登录、个人信息

  2. 订单服务:订单 CRUD、状态流转、搜索筛选

  3. 支付系统:对接第三方支付

  4. 消息推送:WebSocket 实时推送

  5. 文件存储:图片、语音等文件上传

运行效果

相关推荐
<小智>3 小时前
鸿蒙多功能工具箱开发实战(十二)-二十四节气与黄历数据展示
ui·华为·harmonyos
Georgewu3 小时前
【HarmonyOS AI】DevEco CLI、Skills、知识库运用AI Coding提效详解
harmonyos
2501_919749033 小时前
华为鸿蒙免费听歌APP+免费铃声APP
华为·harmonyos
tsqtsqtsq03094 小时前
ArkTS 泛型概念详解
华为·harmonyos·鸿蒙系统
程序员黑豆5 小时前
鸿蒙应用开发教程:以红绿灯切换为例,掌握条件渲染的核心用法
前端·harmonyos
Catrice05 小时前
HarmonyOS ArkTS 实战:实现一个校园证件照拍摄与预约应用
华为·harmonyos
qizayaoshuap6 小时前
# [特殊字符] 手电筒 — 鸿蒙ArkTS设备功能调用与UI交互设计
ui·华为·交互·harmonyos
JaneConan6 小时前
鸿蒙报错速查:arkts-no-any any 类型禁用,用了就炸,根因 + 真解法
后端·harmonyos
listening7776 小时前
HarmonyOS 6.1 UX动效实战:从“生硬”到“灵动”的物理动画引擎
华为·harmonyos·ux