HarmonyOS开发:卡片交互——点击跳转与消息事件

举报
Jack20 发表于 2026/06/27 22:32:59 2026/06/27
【摘要】 HarmonyOS开发:卡片交互——点击跳转与消息事件📌 核心要点:卡片交互有三种模式——router跳转、call调用、message消息,分别对应"打开页面"、“调用方法”、"发送通知"三种交互场景。 背景与动机卡片光能看不行,还得能点。用户看到天气卡片上显示"26° 晴",想看未来几天的预报——点一下,跳到天气App的预报页面。看到待办卡片上显示"3项待办",想看具体是哪3项——点...

HarmonyOS开发:卡片交互——点击跳转与消息事件

📌 核心要点:卡片交互有三种模式——router跳转、call调用、message消息,分别对应"打开页面"、“调用方法”、"发送通知"三种交互场景。

背景与动机

卡片光能看不行,还得能点。

用户看到天气卡片上显示"26° 晴",想看未来几天的预报——点一下,跳到天气App的预报页面。看到待办卡片上显示"3项待办",想看具体是哪3项——点一下,跳到待办列表。看到音乐卡片上显示"正在播放",想暂停——点一下,暂停播放。

这些交互怎么实现?鸿蒙提供了三种卡片交互模式:

  1. router:点击卡片跳转到UIAbility的指定页面
  2. call:点击卡片调用UIAbility的指定方法(后台执行,不跳转页面)
  3. message:点击卡片向FormAbility发送消息

三种模式适用不同场景。选错了,交互体验就很别扭——用户点"暂停"结果跳到了新页面,点"查看详情"结果什么都没发生。

核心原理

三种交互模式对比

flowchart TB
    A[用户点击卡片] --> B{交互模式}
    
    B -->|router| C[拉起UIAbility]
    C --> D[携带Want参数]
    D --> E[UIAbility.onNewWant接收]
    E --> F[跳转到指定页面]
    
    B -->|call| G[拉起UIAbility<br/>后台模式]
    G --> H[携带method参数]
    H --> I[UIAbility.onNewWant接收]
    I --> J[执行指定方法]
    
    B -->|message| K[发送消息到FormAbility]
    K --> L[FormAbility.onFormEvent接收]
    L --> M[处理消息逻辑]
    M --> N[更新卡片数据]
    
    classDef userAction fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    classDef router fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
    classDef call fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef message fill:#FCE4EC,stroke:#C62828,color:#B71C1C
    
    class A userAction
    class C,D,E,F router
    class G,H,I,J call
    class K,L,M,N message
交互模式 是否跳转页面 接收方 适用场景
router UIAbility 查看详情、打开列表
call UIAbility 后台操作(刷新数据、同步)
message FormAbility 卡片内操作(切换状态、标记已读)

router模式详解

router是最常用的交互模式。用户点击卡片的某个区域,系统拉起你的UIAbility,并携带Want参数。你在UIAbility的onCreateonNewWant里解析Want参数,跳转到对应页面。

// 卡片侧:设置router动作
Button('查看详情')
  .onClick(() => {
    postCardAction(this, {
      action: 'router',
      abilityName: 'EntryAbility',
      params: {
        targetPage: 'detail',  // 自定义参数:要跳转的页面
        itemId: '12345'        // 自定义参数:项目ID
      }
    })
  })

call模式详解

call模式和router类似,也是拉起UIAbility,但不会显示界面。适合后台操作——比如"刷新数据"、“同步设置”。

// 卡片侧:设置call动作
Button('刷新')
  .onClick(() => {
    postCardAction(this, {
      action: 'call',
      abilityName: 'EntryAbility',
      params: {
        method: 'refreshData'  // 要调用的方法名
      }
    })
  })

message模式详解

message模式不拉起UIAbility,而是直接发送消息给FormAbility。适合卡片内的轻量操作——比如"标记已读"、“切换状态”。

// 卡片侧:设置message动作
Button('标记已读')
  .onClick(() => {
    postCardAction(this, {
      action: 'message',
      params: {
        action: 'markRead',
        messageId: '12345'
      }
    })
  })

代码实战

基础用法:待办卡片的router跳转

最常见的交互:点击卡片跳转到App的详情页。

卡片布局

// TodoCard.ets
@Entry
@Component
struct TodoCard {
  @State todoCount: string = '0'
  @State latestTodo: string = '暂无待办'
  @State dateStr: string = ''

  build() {
    Column() {
      // 日期
      Text(this.dateStr)
        .fontSize(12)
        .fontColor('#999999')
      
      // 待办数量
      Row() {
        Text(this.todoCount)
          .fontSize(32)
          .fontColor('#FF6B35')
          .fontWeight(FontWeight.Bold)
        Text('项待办')
          .fontSize(14)
          .fontColor('#666666')
          .margin({ left: 4, top: 14 })
      }
      .margin({ top: 8 })
      
      // 最近一条待办
      Text(this.latestTodo)
        .fontSize(12)
        .fontColor('#333333')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .margin({ top: 8 })
      
      // 查看全部按钮
      Button('查看全部')
        .fontSize(12)
        .fontColor('#FFFFFF')
        .backgroundColor('#FF6B35')
        .borderRadius(16)
        .height(28)
        .margin({ top: 12 })
        .onClick(() => {
          // router跳转——点击后打开App的待办列表页
          postCardAction(this, {
            action: 'router',
            abilityName: 'EntryAbility',
            params: {
              targetPage: 'todoList',
              date: this.dateStr
            }
          })
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}

UIAbility侧接收跳转

// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { hilog } from '@kit.PerformanceAnalysisKit'

export default class EntryAbility extends UIAbility {
  
  // 首次创建时调用
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'EntryAbility', 'onCreate')
    this.handleRouterAction(want)
  }
  
  // 已有实例时调用(singleInstance模式)
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'EntryAbility', 'onNewWant')
    this.handleRouterAction(want)
  }
  
  // 处理router跳转
  private handleRouterAction(want: Want): void {
    const params = want.parameters
    if (!params) return
    
    const targetPage = params.targetPage as string
    
    switch (targetPage) {
      case 'todoList':
        // 跳转到待办列表页
        this.navigateToPage('TodoListPage', {
          date: params.date as string
        })
        break
      case 'todoDetail':
        // 跳转到待办详情页
        this.navigateToPage('TodoDetailPage', {
          todoId: params.todoId as string
        })
        break
      default:
        // 默认跳转首页
        this.navigateToPage('Index', {})
    }
  }
  
  private navigateToPage(pageName: string, params: Record<string, string>): void {
    // 通过AppStorage或全局状态管理传递页面参数
    AppStorage.setOrCreate('targetPage', pageName)
    AppStorage.setOrCreate('pageParams', JSON.stringify(params))
  }
  
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'EntryAbility', `加载失败: ${JSON.stringify(err)}`)
        return
      }
    })
  }
}

页面侧根据参数跳转

// Index.ets
@Entry
@Component
struct Index {
  @StorageProp('targetPage') targetPage: string = ''
  @StorageProp('pageParams') pageParams: string = ''
  
  // 页面路由栈
  private navPathStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.navPathStack) {
      // 首页内容
      Column() {
        Text('待办应用首页')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
    }
    .onAppear(() => {
      // 检查是否需要跳转
      this.checkAndNavigate()
    })
  }
  
  private checkAndNavigate(): void {
    if (this.targetPage === 'todoList') {
      const params = JSON.parse(this.pageParams || '{}')
      this.navPathStack.pushPath({
        name: 'TodoListPage',
        param: params
      })
      // 清除标记
      AppStorage.setOrCreate('targetPage', '')
    } else if (this.targetPage === 'todoDetail') {
      const params = JSON.parse(this.pageParams || '{}')
      this.navPathStack.pushPath({
        name: 'TodoDetailPage',
        param: params
      })
      AppStorage.setOrCreate('targetPage', '')
    }
  }
}

进阶用法:消息卡片的message交互

消息卡片需要"标记已读"功能——点击后不跳转页面,只更新卡片状态。

// MessageCard.ets
@Entry
@Component
struct MessageCard {
  @State unreadCount: string = '0'
  @State latestMessage: string = '暂无消息'
  @State sender: string = ''
  @State isRead: string = 'false'

  build() {
    Column() {
      // 未读数量
      Row() {
        Text(this.unreadCount)
          .fontSize(24)
          .fontColor('#FF6B35')
          .fontWeight(FontWeight.Bold)
        Text('条未读')
          .fontSize(12)
          .fontColor('#666666')
          .margin({ left: 4, top: 8 })
        
        Blank()
        
        // 查看全部——router跳转
        Text('查看全部 >')
          .fontSize(12)
          .fontColor('#2196F3')
          .onClick(() => {
            postCardAction(this, {
              action: 'router',
              abilityName: 'EntryAbility',
              params: { targetPage: 'messageList' }
            })
          })
      }
      .width('100%')
      
      // 最新消息
      Row() {
        Column() {
          Text(this.sender)
            .fontSize(12)
            .fontColor('#333333')
            .fontWeight(FontWeight.Medium)
          Text(this.latestMessage)
            .fontSize(11)
            .fontColor('#666666')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        
        // 标记已读按钮——message模式
        if (this.isRead === 'false') {
          Button('已读')
            .fontSize(10)
            .fontColor('#FFFFFF')
            .backgroundColor('#4CAF50')
            .borderRadius(12)
            .height(24)
            .width(48)
            .onClick(() => {
              postCardAction(this, {
                action: 'message',
                params: {
                  action: 'markRead',
                  messageId: this.getMessageId()
                }
              })
            })
        }
      }
      .width('100%')
      .margin({ top: 12 })
      .padding(12)
      .backgroundColor('#F5F5F5')
      .borderRadius(8)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
  
  private getMessageId(): string {
    // 从卡片数据中获取消息ID
    return 'msg_001'
  }
}

FormAbility侧处理message

// MessageFormAbility.ets
import { formBindingData, FormExtensionAbility, formProvider } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

export default class MessageFormAbility extends FormExtensionAbility {
  
  onAddForm(want: Want): formBindingData.FormBindingData {
    return formBindingData.createFormBindingData({
      unreadCount: '3',
      latestMessage: '会议时间已调整到下午3点',
      sender: '张三',
      isRead: 'false'
    })
  }
  
  onUpdateForm(formId: string): formBindingData.FormBindingData {
    return this.onAddForm(null)
  }
  
  onRemoveForm(formId: string): void {
    hilog.info(0x0000, 'MsgForm', `卡片删除: ${formId}`)
  }
  
  // 处理message事件
  onFormEvent(formId: string, message: string): void {
    hilog.info(0x0000, 'MsgForm', `收到消息: ${formId}, ${message}`)
    
    try {
      const msg = JSON.parse(message)
      
      switch (msg.action) {
        case 'markRead':
          this.handleMarkRead(formId, msg.messageId)
          break
        case 'delete':
          this.handleDelete(formId, msg.messageId)
          break
        default:
          hilog.warn(0x0000, 'MsgForm', `未知操作: ${msg.action}`)
      }
    } catch (err) {
      hilog.error(0x0000, 'MsgForm', `消息解析失败: ${err}`)
    }
  }
  
  // 标记已读
  private handleMarkRead(formId: string, messageId: string): void {
    // 1. 更新数据库
    this.markMessageReadInDB(messageId)
    
    // 2. 获取新的未读数量
    const newUnreadCount = this.getUnreadCount()
    
    // 3. 获取新的最新消息
    const latestMsg = this.getLatestUnreadMessage()
    
    // 4. 更新卡片
    const data = {
      unreadCount: newUnreadCount.toString(),
      latestMessage: latestMsg.content,
      sender: latestMsg.sender,
      isRead: 'true'
    }
    
    formProvider.updateForm(formId, formBindingData.createFormBindingData(data))
      .then(() => {
        hilog.info(0x0000, 'MsgForm', '卡片已更新')
      })
      .catch((err: Error) => {
        hilog.error(0x0000, 'MsgForm', `更新失败: ${err.message}`)
      })
  }
  
  private markMessageReadInDB(messageId: string): void {
    // 更新数据库中的消息状态
    hilog.info(0x0000, 'MsgForm', `标记已读: ${messageId}`)
  }
  
  private getUnreadCount(): number {
    // 从数据库查询未读数量
    return 2  // 模拟
  }
  
  private getLatestUnreadMessage(): { content: string; sender: string } {
    return { content: '请确认明天的会议安排', sender: '李四' }
  }
  
  private handleDelete(formId: string, messageId: string): void {
    // 删除消息逻辑
    hilog.info(0x0000, 'MsgForm', `删除消息: ${messageId}`)
  }
}

完整示例:深链接跳转

深链接(Deep Link)是卡片交互的高级用法。你可以在form_config.json里配置deepLink,用户点击卡片时自动跳转到App的指定页面,不需要在onClick里手动调用postCardAction。

// form_config.json
{
  "forms": [{
    "name": "orderCard",
    "displayName": "我的订单",
    "src": "./ets/widget/pages/OrderCard.ets",
    "deepLink": "myapp://order/detail",
    "defaultDimension": "2*4",
    "supportDimensions": ["2*4", "4*4"]
  }]
}

配置了deepLink后,用户点击卡片的任何区域(没有绑定onClick的区域),都会触发deepLink跳转。

// OrderCard.ets —— 订单追踪卡片
@Entry
@Component
struct OrderCard {
  @State orderNo: string = ''
  @State status: string = ''
  @State location: string = ''
  @State estimatedTime: string = ''
  @State progress: number = 0

  build() {
    Column() {
      // 订单号
      Row() {
        Text(this.orderNo)
          .fontSize(12)
          .fontColor('#999999')
        
        Blank()
        
        // 复制订单号——使用message模式,不触发deepLink
        Text('复制')
          .fontSize(11)
          .fontColor('#2196F3')
          .onClick(() => {
            postCardAction(this, {
              action: 'message',
              params: { action: 'copyOrderNo', orderNo: this.orderNo }
            })
          })
      }
      .width('100%')
      
      // 物流状态
      Text(this.status)
        .fontSize(16)
        .fontColor('#333333')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })
      
      // 当前位置
      Row() {
        Text('📍')
          .fontSize(14)
        Text(this.location)
          .fontSize(12)
          .fontColor('#666666')
          .margin({ left: 4 })
      }
      .margin({ top: 4 })
      
      // 进度条
      Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
        .width('100%')
        .color('#4CAF50')
        .margin({ top: 12 })
      
      // 预计到达
      Row() {
        Text(`预计 ${this.estimatedTime} 送达`)
          .fontSize(11)
          .fontColor('#999999')
        
        Blank()
        
        // 查看详情——router模式,携带订单号
        Text('详情 >')
          .fontSize(12)
          .fontColor('#2196F3')
          .onClick(() => {
            postCardAction(this, {
              action: 'router',
              abilityName: 'EntryAbility',
              params: {
                targetPage: 'orderDetail',
                orderNo: this.orderNo
              }
            })
          })
      }
      .width('100%')
      .margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
  }
}

UIAbility侧处理深链接:

// EntryAbility.ets
export default class EntryAbility extends UIAbility {
  
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleDeepLink(want)
  }
  
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.handleDeepLink(want)
  }
  
  private handleDeepLink(want: Want): void {
    // 处理deepLink
    const uri = want.uri || want.parameters?.['ohos.extra.param.key.form_identity'] as string
    
    if (uri && uri.startsWith('myapp://')) {
      const path = uri.replace('myapp://', '')
      
      switch (path) {
        case 'order/detail':
          this.navigateToPage('OrderDetailPage', {})
          break
        case 'order/list':
          this.navigateToPage('OrderListPage', {})
          break
      }
    }
    
    // 处理router参数
    const params = want.parameters
    if (params?.targetPage) {
      this.navigateToPage(params.targetPage as string, params as Record<string, string>)
    }
  }
  
  private navigateToPage(page: string, params: Record<string, string>): void {
    AppStorage.setOrCreate('targetPage', page)
    AppStorage.setOrCreate('pageParams', JSON.stringify(params))
  }
}

踩坑与注意事项

坑1:postCardAction只能在onClick里调用

postCardAction只能在UI组件的onClick回调里调用,不能在onAppear、定时器、异步回调里调用。

// ❌ 错误:在onAppear里调用
onAppear(() => {
  postCardAction(this, { action: 'router', params: {} })  // 报错!
})

// ❌ 错误:在定时器里调用
setInterval(() => {
  postCardAction(this, { action: 'message', params: {} })  // 报错!
}, 1000)

// ✅ 正确:在onClick里调用
Button('点击')
  .onClick(() => {
    postCardAction(this, { action: 'router', params: {} })
  })

坑2:router跳转需要UIAbility配置launchType

如果你的UIAbility的launchType是singleton(单实例),卡片点击跳转时会走onNewWant而不是onCreate。你必须同时处理两个回调。

// module.json5
{
  "abilities": [{
    "name": "EntryAbility",
    "launchType": "singleton",  // 单实例模式
    ...
  }]
}
export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 首次创建时调用
    this.handleRouterAction(want)
  }
  
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 单实例模式下,后续跳转都走这里
    this.handleRouterAction(want)
  }
  
  private handleRouterAction(want: Want): void {
    // 统一处理
  }
}

坑3:message事件的数据大小限制

message模式发送的数据不能超过1KB。如果你需要传大量数据,用router模式跳转到UIAbility处理。

// ❌ 数据太大
postCardAction(this, {
  action: 'message',
  params: {
    action: 'update',
    data: veryLargeJsonObject  // 可能超过1KB
  }
})

// ✅ 只传关键信息,在FormAbility里查询完整数据
postCardAction(this, {
  action: 'message',
  params: {
    action: 'update',
    itemId: '12345'  // 只传ID
  }
})

坑4:call模式需要申请后台权限

call模式会在后台拉起UIAbility,但系统对后台启动有严格限制。你需要在module.json5里申请ohos.permission.KEEP_BACKGROUND_RUNNING权限,否则call模式可能被系统拒绝。

// module.json5
{
  "requestPermissions": [{
    "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
  }]
}

坑5:卡片区域点击优先级

如果卡片同时配置了deepLink和onClick,onClick的优先级更高。点击绑定了onClick的区域,触发onClick;点击未绑定onClick的区域,触发deepLink。

// 整个卡片配置了deepLink
// 但"复制"按钮绑定了onClick
// 点击"复制"→ 触发onClick(message模式)
// 点击其他区域 → 触发deepLink(跳转到订单详情)

HarmonyOS 6适配说明

  1. postCardAction增强:6.0新增了call模式的直接方法调用。你可以在params里指定method,UIAbility会直接调用对应的方法,不需要在onNewWant里手动解析:
// 6.0新增的call模式
postCardAction(this, {
  action: 'call',
  abilityName: 'EntryAbility',
  params: {
    method: 'refreshData',  // 直接调用UIAbility的refreshData方法
    args: { type: 'weather' }
  }
})
  1. 深链接支持通配符:6.0的deepLink支持路径参数,类似Web路由:
{
  "deepLink": "myapp://order/{orderId}"
}

卡片点击时,系统会自动将orderId替换为实际值,你在UIAbility里解析即可。

  1. 交互事件统计:6.0新增了卡片交互事件的统计API,你可以获取卡片的点击次数、跳转成功率等数据:
import { formProvider } from '@kit.AbilityKit'

// 获取卡片交互统计
const stats = await formProvider.getFormInteractionStats(formId)
console.log(`点击次数: ${stats.clickCount}`)
console.log(`跳转成功: ${stats.routerSuccessCount}`)
  1. 多区域独立交互:6.0支持在同一个卡片内为不同区域设置不同的交互动作,不再需要整个卡片共用一个deepLink。

总结

卡片交互是让卡片从"信息展示"升级为"功能入口"的关键。router跳转打开页面,call调用后台方法,message发送轻量消息——三种模式各司其职,选对模式才能给用户最好的体验。

核心要点回顾:

  • router模式:跳转页面,适合"查看详情"场景
  • call模式:后台执行,适合"刷新数据"场景
  • message模式:卡片内操作,适合"标记已读"场景
  • postCardAction只能在onClick里调用
  • 单实例UIAbility要同时处理onCreate和onNewWant
  • message数据不超过1KB,call模式需要后台权限
评估维度 说明
学习难度 ⭐⭐⭐ 三种模式概念简单,但配合使用需要经验
使用频率 ⭐⭐⭐⭐⭐ 每个卡片都需要交互
重要程度 ⭐⭐⭐⭐⭐ 没有交互的卡片就是张图片

下一篇聊卡片UI设计——多尺寸适配、响应式布局、设计规范,让卡片好看又好用。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。