一、 摘要与引言

在万物互联的鸿蒙生态中,应用的价值正从“点开即用”向“服务直达”演进。桌面卡片(Form)作为元服务(Atomic Service)的核心载体,能够将应用的关键信息和功能前置到用户桌面,实现零层级、高频率的交互。

开发桌面卡片将为美寇商城带来三大核心价值:

  • 体验革新:用户无需打开完整应用,在桌面即可快速查看订单状态、浏览热销推荐、使用智能导购,实现“服务找人”。
  • 流量新入口:卡片成为独立于应用图标的“第二入口”,显著提升用户活跃度与功能触达率,开辟新的增长通道。
  • 生态融合:卡片作为鸿蒙“一多”能力(一次开发,多端部署)的典范,可无缝适配手机、平板、智慧屏等多种设备,融入全场景智慧生活。

二、 元服务卡片架构设计

我们将美寇商城的核心业务抽象为几个独立的卡片服务单元,每个单元遵循“Provider提供数据 -> Card渲染UI -> Manager管理生命周期”的架构模式,并与主应用共享核心业务逻辑。
在这里插入图片描述
架构层次说明:

  1. 设备桌面层

    • 用户实际添加和交互的卡片,支持方形、矩形、圆形等多种尺寸(2x2, 2x4, 4x4等)。
    • 每种尺寸对应不同的UI布局和交互密度,例如2x2卡片展示单个核心信息(如待收货订单数),4x4卡片可展示商品瀑布流。
  2. 卡片运行层 (系统级)

    • 渲染引擎:由HarmonyOS系统提供,负责高效、安全地解析和运行卡片提供的UI描述代码(ArkTS)。
    • 通信桥:负责在隔离的卡片运行环境与应用主进程之间安全地传递消息和事件。
  3. 应用侧:卡片提供方

    • FormManager:全局卡片管理器,负责响应系统对卡片生命周期的所有请求(创建、更新、销毁)。
    • FormProviderAbility:一个或多个卡片提供者Ability,是卡片与主应用通信的桥梁。它接收来自卡片的请求,并调用对应的Service获取数据。
    • CardDataService:专为卡片设计的轻量级数据服务,它聚合或直接调用商城原有的OrderServiceRecommendationService等,并做数据裁剪和格式化,以适应卡片展示。
    • CardUI.ets:使用ArkUI编写的卡片界面组件,与普通页面组件类似,但受限于卡片运行环境和安全策略。
  4. 共享能力中心

    • 即美寇商城已存在的、稳定的业务服务模块。卡片通过CardDataService以“客户端”身份复用这些能力,保障业务逻辑一致性与数据真实性。

三、 核心开发流程与代码实现

3.1 开发准备与卡片配置
  1. 工程与模块配置:在DevEco Studio中,确保应用工程已正确配置。卡片作为应用的一部分,其配置主要在module.json5文件中。
    // module.json5 片段 - 定义FormProviderAbility和卡片信息
    {
      "module": {
        "abilities": [
          {
            "name": "MeikouFormProviderAbility",
            "srcEntry": "./ets/meikouformability/MeikouFormProviderAbility.ets",
            "description": "$string:form_provider_description",
            "type": "service", // 类型必须为service
            "visible": true,
            "formsEnabled": true, // 启用卡片能力
            "forms": [
              {
                "name": "OrderStatusCard", // 卡片名称
                "description": "$string:order_card_description",
                "src": "./ets/cardui/OrderStatusCard.ets", // 卡片UI组件路径
                "window": {
                  "designWidth": 360,
                  "autoDesignWidth": true
                },
                "colorMode": "auto",
                "formConfigAbility": "ability://MeikouFormProviderAbility",
                "isDefault": true,
                "updateEnabled": true, // 允许更新
                "scheduledUpdateTime": "10:30", // 定时更新时间
                "updateDuration": 1, // 定时更新频率(天)
                "defaultDimension": "2*2", // 默认尺寸
                "supportDimensions": ["2*2", "2*4"] // 支持的尺寸
              },
              {
                "name": "HotRecommendationCard",
                "description": "$string:recommend_card_description",
                "src": "./ets/cardui/HotRecommendationCard.ets",
                // ... 其他配置类似
                "supportDimensions": ["2*4", "4*4"]
              }
            ]
          }
        ]
      }
    }
    
  2. 权限配置:若卡片需要网络数据,同样需要声明ohos.permission.INTERNET权限。
3.2 核心业务流程与代码实现

卡片的核心流程始于用户在桌面上添加卡片,随后进入“静态显示 -> 动态更新 -> 交互响应”的循环。下图详细展示了卡片从创建到与用户及后端服务交互的完整生命周期:

美寇商城后端 CardDataService FormProviderAbility 鸿蒙系统桌面 用户 美寇商城后端 CardDataService FormProviderAbility 鸿蒙系统桌面 用户 loop [卡片生命周期] 1. 长按应用图标,选择“服务卡片” 2. 触发onAddForm(创建卡片) 3. 根据formId绑定数据 4. 请求初始卡片数据 5. 调用API获取初始数据(如订单列表) 6. 返回业务数据 7. 格式化卡片数据 8. 返回FormBindingData(含初始UI数据) 9. 在桌面渲染出卡片 10. 定时/事件触发onUpdateForm 11. 请求最新数据 12. 调用API获取更新 13. 返回新数据 14. 返回更新后数据 15. 发送更新指令 16. 刷新卡片UI 17. 点击卡片内按钮/区域 18. 路由事件(onEvent) 19. 处理事件(如跳转详情页)

步骤1:卡片提供者Ability (MeikouFormProviderAbility.ets)
这是卡片能力的核心枢纽,负责处理系统所有的卡片生命周期回调。

// MeikouFormProviderAbility.ets
import { FormExtensionAbility } from '@ohos.app.form.FormExtensionAbility';
import { formBindingData, FormBindingData, formInfo } from '@ohos.app.form.formInfo';
import { Want } from '@ohos.app.ability.Want';
import { BusinessError } from '@ohos.base';
import { CardDataService } from '../services/CardDataService';

export default class MeikouFormProviderAbility extends FormExtensionAbility {
  private cardDataService: CardDataService = new CardDataService();

  // 1. 创建卡片时调用,提供初始数据
  onAddForm(want: Want): formBindingData.FormBindingData {
    console.info(`[MeikouFormProviderAbility] onAddForm, want: ${JSON.stringify(want)}`);
    const formId: string = want.parameters['ohos.extra.param.key.form_identity'] as string;
    const formName: string = want.parameters['ohos.extra.param.key.form_name'] as string;

    let initialData: Record<string, Object> = {};
    try {
      // 根据不同的卡片名称,获取不同的初始数据
      switch (formName) {
        case 'OrderStatusCard':
          initialData = this.cardDataService.getInitialOrderData();
          break;
        case 'HotRecommendationCard':
          initialData = this.cardDataService.getInitialRecommendationData();
          break;
        default:
          initialData = { 'title': '美寇商城', 'placeholder': '加载中...' };
      }
    } catch (error) {
      console.error(`[MeikouFormProviderAbility] 获取初始数据失败: ${error}`);
      initialData = { 'error': true, 'message': '数据加载失败' };
    }

    // 将数据包装成FormBindingData返回
    const formData = formBindingData.createFormBindingData(initialData);
    // 可以将formId与业务关联起来,例如存储到本地
    this.saveFormIdAssociation(formId, formName);
    return formData;
  }

  // 2. 卡片需要更新时调用(定时或手动触发)
  onUpdateForm(formId: string): void {
    console.info(`[MeikouFormProviderAbility] onUpdateForm, formId: ${formId}`);
    // 根据formId获取卡片名称和所需数据
    const cardMeta = this.getFormMeta(formId);
    if (!cardMeta) {
      return;
    }

    this.cardDataService.fetchDynamicData(cardMeta.name).then((newData: Record<string, Object>) => {
      // 使用formProvider.updateForm更新指定卡片
      const formData = formBindingData.createFormBindingData(newData);
      this.formProvider.updateForm(formId, formData).then(() => {
        console.info(`[MeikouFormProviderAbility] 卡片 ${formId} 更新成功`);
      }).catch((err: BusinessError) => {
        console.error(`[MeikouFormProviderAbility] 更新卡片失败: ${err.code}, ${err.message}`);
      });
    }).catch((err: BusinessError) => {
      console.error(`[MeikouFormProviderAbility] 获取动态数据失败: ${err.message}`);
    });
  }

  // 3. 删除卡片时调用,进行资源清理
  onRemoveForm(formId: string): void {
    console.info(`[MeikouFormProviderAbility] onRemoveForm, formId: ${formId}`);
    this.clearFormIdAssociation(formId);
  }

  // 4. 处理卡片内部的自定义事件(如按钮点击)
  onFormEvent(formId: string, message: string): void {
    console.info(`[MeikouFormProviderAbility] onFormEvent, formId: ${formId}, message: ${message}`);
    const cardMeta = this.getFormMeta(formId);
    if (!cardMeta) return;

    try {
      const event = JSON.parse(message);
      switch (event.action) {
        case 'NAVIGATE_TO_ORDER_DETAIL':
          // 触发卡片路由,跳转到主应用的订单详情页
          this.postFormEventToAbility('order.detail', { orderId: event.orderId });
          break;
        case 'REFRESH_DATA':
          // 手动刷新卡片数据
          this.onUpdateForm(formId);
          break;
        case 'ADD_TO_CART':
          // 调用加入购物车服务
          this.cardDataService.addToCart(event.productId).then(() => {
            this.onUpdateForm(formId); // 操作成功后刷新卡片
          });
          break;
      }
    } catch (error) {
      console.error(`[MeikouFormProviderAbility] 处理卡片事件失败: ${error}`);
    }
  }

  // 辅助方法:将事件传递给主应用UIAbility(通过postEvent)
  private postFormEventToAbility(eventName: string, params: Object): void {
    // 此处使用CommonEventManager或自定义事件总线,通知主应用跳转
    console.info(`[MeikouFormProviderAbility] 触发应用内事件: ${eventName}`, params);
    // 示例:假设有一个全局事件管理器
    // GlobalEventManager.emit('CARD_ACTION', { event: eventName, data: params });
  }

  private saveFormIdAssociation(formId: string, formName: string): void {
    // 实现:将formId与卡片信息关联存储到AppStorage或Preferences中
    // AppStorage.SetOrCreate<string>(`form_${formId}`, formName);
  }
  private getFormMeta(formId: string): { name: string } | null {
    // 实现:从存储中根据formId获取卡片信息
    // const name = AppStorage.Get<string>(`form_${formId}`);
    // return name ? { name } : null;
    return { name: 'OrderStatusCard' }; // 示例返回
  }
  private clearFormIdAssociation(formId: string): void {
    // 实现:清理存储
  }
}

步骤2:卡片数据服务 (CardDataService.ets)
此服务作为数据适配层,为不同的卡片提供定制化的数据。

// CardDataService.ets
import { BusinessError } from '@ohos.base';
import { http } from '@ohos/net.http';
import { OrderService } from '../services/OrderService'; // 假设已存在
import { RecommendationService } from '../services/RecommendationService'; // 假设已存在

export class CardDataService {
  private orderService: OrderService = OrderService.getInstance();
  private recommendationService: RecommendationService = RecommendationService.getInstance();

  // 获取订单卡片初始数据(同步/轻量)
  getInitialOrderData(): Record<string, Object> {
    // 可以从本地缓存或轻量级API获取
    return {
      'title': '我的订单',
      'pendingPayCount': 0,
      'deliveringCount': 0,
      'recentOrder': '暂无',
      'lastUpdateTime': this.formatTime(Date.now())
    };
  }

  // 获取推荐卡片初始数据
  getInitialRecommendationData(): Record<string, Object> {
    return {
      'title': '热门推荐',
      'items': [
        { id: 'P1001', name: '商品A', price: 199, image: 'common/image1.jpg' },
        { id: 'P1002', name: '商品B', price: 299, image: 'common/image2.jpg' }
      ]
    };
  }

  // 为卡片获取动态数据(异步网络请求)
  async fetchDynamicData(cardName: string): Promise<Record<string, Object>> {
    switch (cardName) {
      case 'OrderStatusCard':
        return await this.fetchLiveOrderData();
      case 'HotRecommendationCard':
        return await this.fetchLiveRecommendationData();
      default:
        return { 'updatedAt': Date.now() };
    }
  }

  private async fetchLiveOrderData(): Promise<Record<string, Object>> {
    try {
      const orders = await this.orderService.getRecentOrders(3); // 调用主业务服务
      const pendingPay = orders.filter(o => o.status === '待付款').length;
      const delivering = orders.filter(o => o.status === '配送中').length;
      return {
        'pendingPayCount': pendingPay,
        'deliveringCount': delivering,
        'recentOrder': orders[0]?.title || '暂无',
        'lastUpdateTime': this.formatTime(Date.now()),
        'hasNew': pendingPay > 0
      };
    } catch (error) {
      console.error(`[CardDataService] 获取订单数据失败: ${error}`);
      return { 'error': '更新失败' };
    }
  }

  private async fetchLiveRecommendationData(): Promise<Record<string, Object>> {
    try {
      const recList = await this.recommendationService.getHotList(4);
      const items = recList.map(item => ({
        id: item.productId,
        name: item.productName,
        price: item.price,
        image: item.coverUrl
      }));
      return {
        'title': '实时推荐',
        'items': items,
        'updatedAt': Date.now()
      };
    } catch (error) {
      console.error(`[CardDataService] 获取推荐数据失败: ${error}`);
      return this.getInitialRecommendationData(); // 降级为初始数据
    }
  }

  async addToCart(productId: string): Promise<boolean> {
    // 调用购物车服务
    console.info(`[CardDataService] 加入购物车: ${productId}`);
    return true; // 模拟成功
  }

  private formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  }
}

步骤3:订单状态卡片UI (OrderStatusCard.ets)
这是卡片的视图层,使用ArkUI的卡片专用组件和装饰器。

// OrderStatusCard.ets
@Component
export struct OrderStatusCard {
  // 通过@LocalStorageProp或@Prop接收来自Provider的绑定数据
  @Prop title: string = '我的订单';
  @Prop pendingPayCount: number = 0;
  @Prop deliveringCount: number = 0;
  @Prop recentOrder: string = '';
  @Prop lastUpdateTime: string = '';
  @Prop hasNew: boolean = false;
  @Prop error: string = '';

  // 用于触发事件到FormProviderAbility
  private formId: string = (this as any)['$$formParam']?.formId;

  build() {
    Column({ space: 8 }) {
      // 标题栏
      Row() {
        Text(this.title)
          .fontSize(16)
          .fontColor('#333')
          .fontWeight(FontWeight.Medium)
          .layoutWeight(1)
        if (this.hasNew) {
          Badge({ count: 1, position: BadgePosition.RightTop }) {
            Image($r('app.media.ic_notification'))
              .width(14)
              .height(14)
          }
        }
        Text(this.lastUpdateTime)
          .fontSize(10)
          .fontColor('#999')
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 8 })

      Divider()
        .strokeWidth(0.5)
        .color('#F0F0F0')

      // 订单状态概览
      if (this.error) {
        Text(`状态:${this.error}`)
          .fontSize(12)
          .fontColor('#FF6600')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding(10)
      } else {
        Row({ space: 20 }) {
          Column({ space: 4 }) {
            Text(this.pendingPayCount.toString())
              .fontSize(24)
              .fontColor('#FF2D79')
              .fontWeight(FontWeight.Bold)
            Text('待付款')
              .fontSize(11)
              .fontColor('#666')
          }
          .onClick(() => this.sendEvent('NAVIGATE_TO_ORDER_DETAIL', { status: 'pending' }))

          Column({ space: 4 }) {
            Text(this.deliveringCount.toString())
              .fontSize(24)
              .fontColor('#0089FF')
              .fontWeight(FontWeight.Bold)
            Text('配送中')
              .fontSize(11)
              .fontColor('#666')
          }
          .onClick(() => this.sendEvent('NAVIGATE_TO_ORDER_DETAIL', { status: 'delivering' }))

          Column({ space: 4 }) {
            Text(this.recentOrder)
              .fontSize(12)
              .fontColor('#333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .width(80)
            Text('最近订单')
              .fontSize(11)
              .fontColor('#666')
          }
          .layoutWeight(1)
          .onClick(() => this.sendEvent('NAVIGATE_TO_ORDER_DETAIL', { orderId: 'latest' }))
        }
        .width('100%')
        .padding({ left: 20, right: 20, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
      }

      // 底部操作栏 (仅在2x4等大尺寸卡片显示)
      if (this.supportDimension('2*4')) {
        Row({ space: 0 }) {
          Button('一键刷新', { type: ButtonType.Capsule })
            .fontSize(10)
            .height(24)
            .backgroundColor('#F0F0F0')
            .onClick(() => this.sendEvent('REFRESH_DATA', {}))
          Blank()
          Button('查看全部', { type: ButtonType.Capsule })
            .fontSize(10)
            .height(24)
            .backgroundColor('#F0F0F0')
            .onClick(() => this.sendEvent('NAVIGATE_TO_ORDER_DETAIL', { viewAll: true }))
        }
        .width('100%')
        .padding({ left: 12, right: 12, bottom: 8 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({ radius: 8, color: '#08000000', offsetX: 0, offsetY: 2 })
  }

  // 判断当前卡片尺寸
  private supportDimension(dim: string): boolean {
    const currentDim = (this as any)['$$formParam']?.dimension;
    return currentDim === dim;
  }

  // 发送事件到FormProviderAbility
  private sendEvent(action: string, params: Object): void {
    if (!this.formId) return;
    const message = JSON.stringify({ action, ...params });
    // 调用卡片上下文方法触发事件
    (this as any)['$$formParam']?.formProvider?.sendEventToFormProvider(this.formId, message);
  }
}

四、 效果对比与最佳实践

4.1 卡片化服务与传统应用入口对比
对比维度 传统应用入口 (图标) 桌面元服务卡片
信息获取路径 多步操作:1. 点击图标 -> 2. 等待应用启动 -> 3. 找到对应页面 -> 4. 查看信息。 零步直达:关键信息(如订单数、推荐商品)直接、实时显示在桌面上。
交互效率 交互深、路径长,适合复杂、沉浸式操作。 交互轻、路径短,适合高频、微操作(如查看、刷新、快速下单)。
场景适配 被动响应,用户需要主动想起并打开应用。 主动触达,卡片信息可视,能随时吸引用户注意,融入碎片化时间。
功能承载 承载完整、复杂的功能集合。 承载原子化、单一场景的核心功能切片。
系统资源占用 占用内存和CPU较高,启动有延迟。 资源占用极低,由系统统一调度渲染,性能高效。
用户心智 “一个工具”。 “一个随时待命的服务”。
4.2 关键实践与优化建议
  1. 卡片设计原则

    • 信息精炼:每张卡片只解决一个核心用户诉求(如“查订单”、“看推荐”),信息密度适中。
    • 视觉统一:卡片设计语言应与主应用品牌保持一致,同时符合鸿蒙卡片设计规范(圆角、阴影、内边距)。
    • 交互明确:可点击区域应有清晰的视觉反馈,操作结果(如加入购物车成功)应有即时提示。
  2. 性能与功耗优化

    • 数据缓存策略CardDataService应实现智能缓存,对于实时性要求不高的数据(如热销榜),可适当延长缓存时间,减少网络请求。
    • 按需更新:合理配置updateDurationscheduledUpdateTime,避免不必要的定时更新。对于“订单状态”等用户敏感数据,可结合被动更新(通过postCardAction在用户完成支付后触发更新)。
    • 轻量级UI:卡片UI应尽可能使用简单组件,避免复杂的动画和嵌套过深的布局,确保滑动桌面时的流畅度。
  3. 跨端适配 (“一多”能力)

    • 利用ArkUI的响应式布局和@ohos.app.form.FormExtensionAbility提供的环境变量,检测卡片所在设备类型(手机、平板、智慧屏)。
    • build函数中根据不同的dimension和设备类型,动态选择不同的UI分支,为不同设备提供最合适的交互布局。
  4. 安全与隐私

    • 数据安全:卡片运行在受控的沙箱环境中,与主应用通过预定义接口通信,有效隔离风险。
    • 权限最小化:卡片能力本身不应申请不必要的权限。若需网络,其权限继承自主应用。
    • 用户可控:用户可随时长按卡片移除,所有数据即被清理,尊重用户控制权。

五、 总结与展望

将美寇商城核心功能转化为桌面元服务卡片,是应用从“功能实体”进化为“场景化服务”的关键一步。通过构建标准化的FormProviderAbility、高效的数据适配层CardDataService以及灵活的卡片UI组件,我们成功地将厚重的应用功能“切片”为轻巧、灵动、即用的桌面服务,使用户与商城核心价值的连接路径缩短至“零”。

展望未来,基于此卡片开发框架,可向更智能、更融合的方向演进:

  • 情景智能卡片:结合Intents Kit,使卡片能根据时间、地点、用户习惯动态变换内容。例如,工作日午间推送快餐优惠卡片,晚间推送生鲜折扣卡片。
  • 跨设备流转:利用HarmonyOS分布式能力,用户在手机上将商品加入购物车卡片,可在平板的卡片上直接结算,实现服务的无缝接续。
  • 卡片生态联动:探索与系统级或其他应用的卡片联动,例如“日历卡片”中的聚会日程旁,自动出现“美寇酒水推荐”卡片的提示。
  • 动态卡片与AIGC:结合集成的DeepSeek等AI能力,生成个性化的“每日购物建议”卡片,内容完全由AI根据用户偏好和实时热点驱动。

桌面卡片不仅是UI的延伸,更是鸿蒙“服务原子化”理念的落地。它让美寇商城以更优雅、更强大的方式融入用户的数字生活,真正实现“服务常在,触手可及”。

Logo

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

更多推荐