系列番外·随身终端篇。PC篇发布后,有读者问:“手机、车机、PC都写了,那智能手表呢?我平时跑步不想带手机,能不能抬腕就看物流、付尾款?” 太戳痛点了!HarmonyOS穿戴设备(Watch GT/Ultimate系列)的轻量化交互,恰恰是电商“即时服务”的最后一公里。今天我们把电商Demo的订单提醒、物流跟踪、NFC碰一碰支付适配到手表端,解决三大穿戴开发独有难题:1.8英寸圆形屏幕适配旋转表冠交互超低功耗约束。全程基于API23,含官方文档未提及的穿戴端专属合规要求。


一、前言:为什么穿戴设备是电商的“隐形入口”?

我们之前的系列覆盖了手机、车机、PC等大屏终端,但忽略了用户最常佩戴的设备——智能手表:

  • 场景刚需:跑步、挤地铁、骑车时,掏手机看物流/付尾款极不方便,抬腕1秒完成操作才是刚需。

  • 系统级优势:鸿蒙穿戴设备支持分布式流转,车机上下单、手表上同步提醒,手机上查的物流、手表上直接展示,数据全端打通。

  • 交互独特性:旋转表冠(Crown)、侧边按键、抬腕唤醒,这些交互方式和手机/PC完全不同,能带来全新的电商体验。

今天我们就基于之前的电商Demo,打造一个仅保留核心功能的手表端电商应用:用户不需要在手表上浏览商品(屏幕太小不适合),而是聚焦于订单通知、物流跟踪、快速支付三个高频场景,真正做到“无感服务”。


二、核心概念辨析(手机App vs 穿戴App)

穿戴设备的硬件约束极强,直接照搬手机逻辑必死无疑,先明确核心差异:

维度

手机电商App

穿戴电商App(合规版)

屏幕尺寸

6英寸+,矩形

1.4-1.8英寸,圆形为主

交互方式

触摸(精细操作)

抬腕唤醒+旋转表冠+单击按键(避免误触)

性能约束

无特殊限制

严格功耗限制:后台任务最长存活10分钟,常驻后台会触发系统杀进程

核心功能

浏览、比价、下单全流程

仅保留高频轻量功能:通知、查询、快速支付

网络能力

独立联网

多数情况依赖手机蓝牙代理联网(省电)

💡 核心认知:穿戴App的设计原则是“少即是多”。不要试图在手表上复刻手机的全部功能,只做用户必须抬腕完成的操作,其余功能全部导流到手机端。


三、代码实现:从“全功能电商”到“轻量穿戴助手”

3.1 环境准备与配置

  1. 新建穿戴模块:DevEco Studio → New → Module → Wearable Module,选择圆形模板(Round Template)。

  2. 权限配置:穿戴设备权限管控比手机更严,修改module.json5

    
      
    
      
    {
      "module": {
        "deviceTypes": ["wearable"],
        "abilities": [
          {
            "name": "WearableAbility",
            "type": "page",
            "visible": true,
            "window": {
              "isTransformScale": true, // 圆形屏幕自动适配(避免内容被边缘裁切)
              "designWidth": 466, // 圆形屏幕标准设计宽度(GT 3/4系列)
              "designHeight": 466
            }
          }
        ],
        "requestPermissions": [
          { "name": "ohos.permission.NFC_TAG", "reason": "NFC碰一碰支付" },
          { "name": "ohos.permission.VIBRATE", "reason": "支付成功震动反馈" },
          { "name": "ohos.permission.COMMUNICATION_STATE", "reason": "获取蓝牙连接状态同步数据" }
        ]
      }
    }
  3. 资源适配:在resources下新建layout-wearable-round目录,存放圆形屏幕专属布局。

3.2 核心功能1:物流跟踪卡片(负一屏元服务)

穿戴设备最适合展示的就是实时状态类信息,我们把物流进度做成手表的负一屏元服务卡片,抬腕左滑即可查看,不需要打开App。

创建entry_wearable/src/main/ets/widget/pages/LogisticsCard.ets



import { logistics } from '../../common/LogisticsUtil' // 复用之前的物流查询逻辑
import { distributedDataObject } from '@kit.ArkData' // 分布式数据同步,和手机共享订单数据

@Entry
@Component
struct LogisticsCard {
  @State logisticsInfo: string = '加载中...'
  @State deliveryProgress: number = 0 // 0-100进度
  @State arrivalTime: string = '--'
  private ddo: distributedDataObject.DistributedDataObject = null!

  aboutToAppear(): void {
    // 初始化分布式数据对象,同步手机端的订单数据
    this.ddo = distributedDataObject.createDistributedDataObject(this.getContext(), {
      packageName: 'com.example.shop',
      abilityName: 'EntryAbility',
      tableName: 'order_table'
    })
    // 监听手机端订单更新
    this.ddo.on('dataChanged', (changedData: Record<string, Object>) => {
      if (changedData['latestOrder']) {
        this.updateLogistics(changedData['latestOrder'].orderId)
      }
    })
    // 首次加载
    this.updateLogistics('latest')
  }

  /**
   * 更新物流信息
   */
  async updateLogistics(orderId: string): Promise<void> {
    try {
      const info = await logistics.getLatestLogistics(orderId)
      this.logisticsInfo = info.status // 如“运输中”
      this.deliveryProgress = info.progress // 如75%
      this.arrivalTime = info.arrivalTime // 如“今日18:00前送达”
    } catch (err) {
      this.logisticsInfo = '物流信息加载失败'
    }
  }

  build() {
    Column() {
      // 顶部标题(圆形屏幕顶部留足空间,避免被表冠遮挡)
      Text('物流跟踪')
        .fontSize(18)
        .fontColor(Color.White)
        .margin({ top: 40 })

      // 进度圆环(圆形屏幕最佳可视化方案)
      Stack() {
        Progress({
          value: this.deliveryProgress,
          total: 100,
          type: ProgressType.Ring
        })
          .width(160)
          .height(160)
          .style({ strokeWidth: 12, backgroundColor: '#333', foregroundColor: '#0A59F7' })
        Column() {
          Text(`${this.deliveryProgress}%`)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
          Text('已送达')
            .fontSize(14)
            .fontColor('#AAA')
        }
      }
      .margin({ top: 20 })

      // 物流状态
      Text(this.logisticsInfo)
        .fontSize(16)
        .fontColor(Color.White)
        .margin({ top: 20 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      // 预计送达时间
      Text(`预计${this.arrivalTime}送达`)
        .fontSize(14)
        .fontColor('#AAA')
        .margin({ top: 8 })

      // 底部提示:旋转表冠查看更多
      Text('旋转表冠查看详情')
        .fontSize(12)
        .fontColor('#666')
        .margin({ top: 30 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000')
    .padding(16)
    .onClick(() => {
      // 点击卡片跳转手机端详情页(分布式流转)
      this.jumpToPhone()
    })
  }

  /**
   * 跳转手机端详情页(分布式流转)
   */
  jumpToPhone(): void {
    const context = getContext(this) as AbilityContext
    context.startAbility({
      bundleName: 'com.example.shop',
      abilityName: 'EntryAbility',
      parameters: { from: 'wearable', page: 'logistics' }
    })
  }
}

3.3 核心功能2:旋转表冠切换订单

圆形手表没有足够的空间放底部Tab,最适合的交互是旋转表冠切换内容。我们实现用表冠切换最近3笔订单的功能:

创建entry_wearable/src/main/ets/pages/OrderList.ets



import { wearable } from '@kit.WearableKit'
import { haptic } from '@kit.SensorServiceKit'

@Entry
@Component
struct OrderList {
  @State currentIndex: number = 0
  private orders: Array<{ id: string, name: string, price: number }> = [
    { id: '001', name: 'HarmonyOS手机', price: 5999 },
    { id: '002', name: '车载充电器', price: 199 },
    { id: '003', name: '星闪鼠标', price: 299 }
  ]
  private crownSubscriber: wearable.CrownSubscriber | null = null

  aboutToAppear(): void {
    // 订阅旋转表冠事件
    this.crownSubscriber = wearable.createCrownSubscriber()
    this.crownSubscriber.on('rotate', (delta: number) => {
      // delta为正:顺时针旋转,切换到下一单
      // delta为负:逆时针旋转,切换到上一单
      if (delta > 0) {
        this.currentIndex = (this.currentIndex + 1) % this.orders.length
      } else if (delta < 0) {
        this.currentIndex = (this.currentIndex - 1 + this.orders.length) % this.orders.length
      }
      // 旋转时触发短震动反馈(增强交互感知)
      haptic.vibrate({ type: haptic.HapticType.SHORT_PRESS })
    })
  }

  aboutToDisappear(): void {
    // 取消订阅,避免内存泄漏
    this.crownSubscriber?.off('rotate')
  }

  build() {
    Column() {
      // 顶部标题
      Text('最近订单')
        .fontSize(18)
        .fontColor(Color.White)
        .margin({ top: 40 })

      // 订单卡片(随表冠旋转切换)
      Stack() {
        // 背景圆环(指示当前是第几个订单)
        Progress({
          value: this.currentIndex + 1,
          total: this.orders.length,
          type: ProgressType.Ring
        })
          .width(220)
          .height(220)
          .style({ strokeWidth: 8, backgroundColor: '#333', foregroundColor: '#0A59F7' })

        // 订单内容
        Column() {
          Text(this.orders[this.currentIndex].name)
            .fontSize(20)
            .fontWeight(FontWeight.Medium)
            .fontColor(Color.White)
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .margin({ bottom: 12 })
          Text(`¥${this.orders[this.currentIndex].price}`)
            .fontSize(24)
            .fontColor('#FFD700')
            .fontWeight(FontWeight.Bold)
          Text(`订单号:${this.orders[this.currentIndex].id}`)
            .fontSize(12)
            .fontColor('#AAA')
            .margin({ top: 12 })
        }
        .padding(20)
      }
      .margin({ top: 30 })

      // 底部操作提示
      Row() {
        Text('旋转表冠切换')
          .fontSize(12)
          .fontColor('#666')
        Blank()
        Text('单击表冠支付')
          .fontSize(12)
          .fontColor('#666')
      }
      .width('80%')
      .margin({ top: 40 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000')
    .onClick(() => {
      // 单击表冠触发支付(需配合侧边按键逻辑,此处简化)
      this.quickPay()
    })
  }

  /**
   * 快速支付(调用手机端的IAP能力)
   */
  quickPay(): void {
    // 手表本身不做支付逻辑,通过分布式调用手机端的支付接口
    const context = getContext(this) as AbilityContext
    context.startAbility({
      bundleName: 'com.example.shop',
      abilityName: 'PaymentAbility',
      parameters: { orderId: this.orders[this.currentIndex].id }
    })
    promptAction.showToast({ message: '正在调用手机支付...' })
  }
}

3.4 核心功能3:NFC碰一碰支付

穿戴设备最常用的支付方式是NFC碰一碰,我们把电商订单的尾款支付集成到NFC标签中,碰一下POS机即可完成支付:

创建entry_wearable/src/main/ets/common/NfcPayment.ets



import { nfc } from '@kit.ConnectivityKit'
import { BusinessError } from '@kit.BasicServicesKit'

/**
 * NFC碰一碰支付工具类
 */
export class NfcPayment {
  private static nfcController: nfc.NfcController | null = null

  /**
   * 初始化NFC控制器
   */
  static async init(context: Context): Promise<boolean> {
    try {
      this.nfcController = nfc.getDefaultNfcController(context)
      if (!this.nfcController.isEnabled()) {
        promptAction.showToast({ message: '请先开启NFC功能' })
        return false
      }
      return true
    } catch (err) {
      const e = err as BusinessError
      console.error('NFC初始化失败:', e.message)
      return false
    }
  }

  /**
   * 生成支付NFC标签(写入订单信息)
   * @param orderId 订单ID
   * @param amount 支付金额
   */
  static async generatePaymentTag(orderId: string, amount: number): Promise<void> {
    if (!this.nfcController) return
    try {
      // 构造NFC标签数据(仅包含订单ID和金额,不包含敏感信息)
      const tagData = {
        type: 'payment',
        orderId: orderId,
        amount: amount,
        timestamp: Date.now()
      }
      // 写入NFC标签(设置为只读,防止篡改)
      await this.nfcController.writeNdefMessage({
        records: [
          nfc.createNdefRecord(nfc.NdefRecordType.TEXT, JSON.stringify(tagData))
        ],
        readOnly: true
      })
      promptAction.showToast({ message: 'NFC支付标签生成成功,请碰触POS机' })
    } catch (err) {
      const e = err as BusinessError
      console.error('NFC标签写入失败:', e.message)
    }
  }

  /**
   * 监听NFC碰触事件(支付回调)
   */
  static onPaymentReceived(callback: (orderId: string, amount: number) => void): void {
    if (!this.nfcController) return
    this.nfcController.on('tagDiscovered', (tag: nfc.TagInfo) => {
      // 读取NFC标签中的订单信息
      const ndefMessage = tag.ndefMessage
      if (ndefMessage && ndefMessage.records.length > 0) {
        const record = ndefMessage.records[0]
        if (record.type === nfc.NdefRecordType.TEXT) {
          const data = JSON.parse(record.payload.toString())
          if (data.type === 'payment') {
            callback(data.orderId, data.amount)
          }
        }
      }
    })
  }
}

四、踩坑记录(官方文档没写的6个穿戴坑)

  1. 圆形屏幕裁切问题:圆形屏幕的边缘会被表壳遮挡,官方文档只说开isTransformScale,但实际开发中,内容距离边缘至少要留30vp的安全距离,否则文字会被裁掉。解决方法:用SafeArea组件包裹内容,或者手动设置margin: { top: 40, bottom: 40 }

  2. 旋转表冠事件冲突:旋转表冠默认会触发系统音量调节/亮度调节,需要申请ohos.permission.MANAGE_CROWN_EVENT权限才能独占事件,否则你的监听会失效。

  3. NFC权限的特殊要求:穿戴设备的NFC支付必须符合银联的EMVCo规范,不能随便写入自定义数据,否则会被系统拦截。正确的做法是调用华为支付的SDK,而不是直接操作NFC标签。

  4. 后台任务限制:穿戴设备的后台任务最长只能存活10分钟,且不能常驻后台。如果要实现定时查询物流,必须用系统的WorkScheduler,而不是自己开定时器,否则会被系统立刻杀掉。

  5. 蓝牙代理联网延迟:穿戴设备多数情况下通过蓝牙代理手机联网,网络延迟比手机高200-500ms,调用云函数时必须设置更长的超时时间(建议5秒以上),否则会频繁超时。

  6. 功耗优化陷阱:不要在onAppear中做耗时操作,不要频繁调用vibrate(震动很耗电),不要开启不必要的传感器(如心率、加速度计),否则手表续航会从7天降到1天,用户会直接卸载App。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐