HarmonyOS 6.1 生态整合实战:服务卡片与“万能卡片”的生态玩法
系列生态拓展篇·第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资源。 -
隐私与权限:场景感知需要位置、时间等信息,必须严格遵守隐私合规要求。在获取位置信息前,必须申请权限并解释用途。如果用户拒绝,应优雅降级,不使用场景感知功能。
更多推荐



所有评论(0)