系列生态拓展篇·第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.etsimport { 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(): Promisevoid { // ... 之前的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.etsimport { 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(): Promiseboolean { 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): Promisevoid { 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资源。隐私与权限场景感知需要位置、时间等信息必须严格遵守隐私合规要求。在获取位置信息前必须申请权限并解释用途。如果用户拒绝应优雅降级不使用场景感知功能。