系列生态拓展篇·第39篇。DevOps篇后,有产品经理问:“功能、测试、上线都搞定了,怎么让用户在手机桌面上就能直接复购?怎么让我们的服务出现在华为的负一屏、小艺建议里?” 这涉及到鸿蒙生态的流量入口设计。今天我们将电商Demo的服务卡片(Service Widget)升级为万能卡片(Universal Card),接入小艺建议、智慧搜索、场景感知三大系统级入口,实现“服务找人”而非“人找服务”。全程基于API23,含官方文档未涉及的“卡片动态刷新策略”和“意图框架(Intent Framework)接入技巧”。
一、前言:为什么“万能卡片”是鸿蒙生态的流量密码?
在传统App时代,用户与应用的互动是“孤岛式”的:打开App -> 寻找功能 -> 使用 -> 关闭。而在鸿蒙生态中,服务卡片打破了这种孤岛,让服务可以脱离App独立存在,分布在手机的各个角落:
桌面卡片:用户一眼看到物流进度、优惠券。
负一屏(智慧助手):聚合高频服务,如“待办事项”、“快递动态”。
小艺建议:基于场景预测用户需求,主动推荐服务(如“下班时点外卖”)。
智慧搜索:全局搜索时,直接展示服务卡片,而非只是App名称。
核心价值:零层级触达。用户不需要打开App,在桌面上就能完成核心操作(如加购、查物流)。这极大地提升了用户体验和转化率。
今天我们将把电商Demo的“物流跟踪卡片”升级为一个智能、动态、可交互的万能卡片,并让它学会“察言观色”,在合适的时机主动出现。
二、核心概念辨析(服务卡片 vs 万能卡片)
特性 | 服务卡片 (Service Widget) | 万能卡片 (Universal Card) |
|---|---|---|
定义 | 应用提供的、可固定在桌面的UI组件 | 服务卡片的高级形态,深度融入系统级入口 |
交互性 | 支持简单点击、刷新 | 支持复杂交互、动态数据、场景感知 |
入口 | 桌面、文件夹 | 桌面、负一屏、小艺建议、搜索、锁屏 |
智能化 | 静态展示为主 | 基于用户习惯、时间、位置智能推荐 |
开发难度 | 较低,主要编写UI | 较高,需接入意图框架、场景感知 |
三、代码实现:从“静态展示”到“智能服务”
3.1 卡片UI升级:动态物流卡片
首先,升级之前的物流卡片,使其支持实时刷新和交互。
修改entry/src/main/ets/widget/pages/LogisticsCard.ets:
import { logistics } from '../../common/LogisticsUtil' import { distributedDataObject } from '@kit.ArkData' import { systemTimer } from '@kit.SystemTimerKit' // 引入定时器 @Entry @Component struct LogisticsCard { @State logisticsInfo: string = '加载中...' @State deliveryProgress: number = 0 @State arrivalTime: string = '--' @State courierPhone: string = '' // 新增:快递员电话 private ddo: distributedDataObject.DistributedDataObject = null! private timerId: number = -1 // 定时器ID aboutToAppear(): void { this.initData() this.startAutoRefresh() } aboutToDisappear(): void { this.stopAutoRefresh() } // 初始化数据 async initData(): Promise<void> { // ... 之前的DDO初始化和数据加载逻辑 ... const info = await logistics.getLatestLogistics('latest') this.logisticsInfo = info.status this.deliveryProgress = info.progress this.arrivalTime = info.arrivalTime this.courierPhone = info.courierPhone || '' } // 启动自动刷新(每30秒一次) startAutoRefresh(): void { this.timerId = systemTimer.setInterval(() => { this.initData() console.log('卡片数据自动刷新') }, 30000) // 30秒 } // 停止自动刷新 stopAutoRefresh(): void { if (this.timerId !== -1) { systemTimer.clearInterval(this.timerId) } } // 用户点击卡片,跳转到手机App详情页 navigateToDetail(): void { postCardAction(this, { action: 'router', bundleName: 'com.example.shop', abilityName: 'EntryAbility', params: { page: 'logistics_detail' } }) } // 用户点击拨打电话 callCourier(): void { if (this.courierPhone) { postCardAction(this, { action: 'call', number: this.courierPhone }) } } build() { Column() { // 顶部标题栏(可点击刷新) Row() { Text('物流跟踪') .fontSize(18) .fontColor(Color.White) Blank() Image($r('app.media.refresh')) .width(24) .height(24) .onClick(() => this.initData()) } .margin({ top: 20, left: 16, right: 16 }) // 进度圆环 Stack() { Progress({ value: this.deliveryProgress, total: 100, type: ProgressType.Ring }) .width(120) .height(120) .style({ strokeWidth: 10, backgroundColor: '#333', foregroundColor: '#0A59F7' }) Column() { Text(`${this.deliveryProgress}%`) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(Color.White) Text('已送达') .fontSize(12) .fontColor('#AAA') } } .margin({ top: 20 }) // 物流状态和预计送达 Text(this.logisticsInfo) .fontSize(16) .fontColor(Color.White) .margin({ top: 16 }) Text(`预计${this.arrivalTime}送达`) .fontSize(14) .fontColor('#AAA') .margin({ top: 8 }) // 新增:快递员联系方式 if (this.courierPhone) { Row() { Text('快递员:') .fontSize(14) .fontColor('#AAA') Text(this.courierPhone) .fontSize(14) .fontColor('#0A59F7') .onClick(() => this.callCourier()) .margin({ left: 8 }) } .margin({ top: 12 }) } // 底部操作区 Row() { Button('查看详情') .fontSize(12) .height(32) .backgroundColor('#0A59F7') .onClick(() => this.navigateToDetail()) Blank() Text('左滑查看更多') .fontSize(12) .fontColor('#666') } .margin({ top: 20, left: 16, right: 16 }) } .width('100%') .height('100%') .backgroundColor('#000') .borderRadius(16) .onClick(() => this.navigateToDetail()) } }3.2 接入意图框架(Intent Framework):让卡片“懂你”
为了让卡片出现在“小艺建议”和“智慧搜索”中,我们需要定义意图(Intent)。意图描述了卡片能提供的服务能力。
在entry/src/main/resources/base/profile/下创建intents.json:
{ "intents": [ { "name": "view_logistics", // 意图名称 "actions": ["ohos.want.action.viewLogistics"], // 动作 "uris": ["logistics://com.example.shop/detail"], // URI "entities": ["entity.order", "entity.delivery"], // 实体类型 "category": "launcher", // 分类 "keywords": ["物流", "快递", "订单", "送货"], // 关键词 "displayName": "$string:logistics_intent_name", "description": "$string:logistics_intent_desc", "icon": "$media:logistics_icon", "targetAbility": "EntryAbility", // 目标Ability "excludeFromHistory": false }, { "name": "buy_again", // 再次购买意图 "actions": ["ohos.want.action.buyAgain"], "entities": ["entity.product"], "category": "recommendation", // 推荐类,会出现在小艺建议中 "keywords": ["复购", "再次购买", "买过的东西"], "displayName": "$string:buy_again_intent_name", "targetAbility": "EntryAbility" } ] }在module.json5中注册意图:
{ "module": { "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ts", "description": "$string:EntryAbility_desc", "icon": "$media:icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:icon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "actions": [ "ohos.want.action.viewLogistics", "ohos.want.action.buyAgain" ], "entities": [ "entity.order", "entity.delivery", "entity.product" ], "uris": [ { "scheme": "logistics", "host": "com.example.shop" } ] } ] } ], "intents": [ { "name": "view_logistics", "srcEntry": "./resources/base/profile/intents.json" } ] } }3.3 场景感知:基于位置和时间的智能推荐
利用@kit.LocationKit和@kit.SensorServiceKit,让卡片在特定场景下主动出现。
创建entry/src/main/ets/service/SmartRecommendService.ets:
import { geoLocationManager } from '@kit.LocationKit' import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit' import { preferences } from '@kit.ArkData' export class SmartRecommendService { private static readonly STORE_KEY = 'smart_recommend' private static readonly HOME_LOCATION = { latitude: 39.9, longitude: 116.3 } // 家庭位置 private static readonly WORK_LOCATION = { latitude: 39.95, longitude: 116.35 } // 工作位置 /** * 判断是否处于“下班场景” * 条件:工作日17:00-19:00,且位置靠近家庭或工作地点 */ static async isOffWorkScenario(): Promise<boolean> { try { // 1. 检查时间(工作日17:00-19:00) const now = new Date() const day = now.getDay() // 0=周日, 1=周一... const hour = now.getHours() const isWeekday = day >= 1 && day <= 5 const isOffWorkTime = hour >= 17 && hour < 19 if (!isWeekday || !isOffWorkTime) return false // 2. 检查位置权限 const atManager = abilityAccessCtrl.createAtManager() const grantStatus = await atManager.checkAccessToken( globalThis.context.tokenID, Permissions.LOCATION ) if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) return false // 3. 获取当前位置 const location = await geoLocationManager.getCurrentLocation() // 4. 计算距离(简化版,实际应使用球面距离公式) const distanceToHome = this.calculateDistance( location.latitude, location.longitude, this.HOME_LOCATION.latitude, this.HOME_LOCATION.longitude ) const distanceToWork = this.calculateDistance( location.latitude, location.longitude, this.WORK_LOCATION.latitude, this.WORK_LOCATION.longitude ) // 5. 如果距离家或公司小于2公里,认为是下班场景 return distanceToHome < 2000 || distanceToWork < 2000 } catch (err) { console.error('场景判断失败:', err) return false } } /** * 保存用户偏好,用于个性化推荐 */ static async saveUserPreference(key: string, value: string): Promise<void> { const store = await preferences.getPreferences(globalThis.context, this.STORE_KEY) await store.put(key, value) await store.flush() } /** * 计算两点间距离(简化版) */ private static calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { // 实际开发中应使用 Haversine 公式 return Math.sqrt(Math.pow(lat1 - lat2, 2) + Math.pow(lon1 - lon2, 2)) * 111000 // 粗略换算为米 } }在卡片的aboutToAppear中调用:
aboutToAppear(): void { this.initData() this.startAutoRefresh() // 判断是否是下班场景,如果是,则推荐“再次购买”卡片 SmartRecommendService.isOffWorkScenario().then(isOffWork => { if (isOffWork) { // 通知系统,当前适合展示“再次购买”意图的卡片 postCardAction(this, { action: 'update_intent', intentName: 'buy_again' }) } }) }四、踩坑记录(官方文档没写的生态细节)
卡片刷新策略:卡片的自动刷新(通过
systemTimer)会消耗电量。系统对卡片的刷新频率有限制,过于频繁的刷新会被系统限制。建议使用事件驱动刷新(如通过DDO监听数据变化),而非定时刷新。意图框架的“冷启动”:当用户点击小艺建议中的卡片时,如果App进程不存在,系统会冷启动App。此时,需要在
EntryAbility的onCreate中解析want参数,根据意图跳转到正确的页面,否则会一直停留在首页。卡片的“存活周期”:卡片不是永久存在的。当用户卸载App、清除缓存或系统资源紧张时,卡片会被销毁。开发者需要处理卡片的
onDestroy生命周期,清理资源。多设备适配:卡片会在手机、平板、手表等不同设备上展示。必须使用响应式布局(如
GridRow、GridCol),并根据设备类型(deviceType)加载不同的UI资源。隐私与权限:场景感知需要位置、时间等信息,必须严格遵守隐私合规要求。在获取位置信息前,必须申请权限并解释用途。如果用户拒绝,应优雅降级,不使用场景感知功能。