HarmonyOS 7 实况窗「景区排队一眼即得」功能开发实战
·
高校周边通 · 国庆特别版:HarmonyOS 7 实况窗「景区排队一眼即得」功能开发实战

本文代码基于 HarmonyOS 7 / API 26 官方文档中的原始示例撰写,函数名、字段名、错误码均来自官网最新版本(更新时间:2026-09-09)。
官方原文链接:
项目背景:「高校周边通」v3.6.0 国庆版本新增 「景区排队一眼即得」 功能:用户在 App 内查询国庆热门景区排队情况后,系统会把"前方 X 位 / 预计 Y 分钟"实时同步到状态栏胶囊和锁屏卡片上,无需反复打开 App 刷新。
一、为什么要在国庆版本接入实况窗?
国庆黄金周大学生扎堆出行,故宫、长城、外滩等热门景点排队动辄 1-2 小时。「高校周边通」的核心场景之一就是"周边景点排队查询",原本用户需要反复打开 App 才能看到最新进度。实况窗让用户把"看一眼就够"的状态信息推到状态栏(胶囊态)和锁屏(卡片态),彻底解放双手。
官方文档给出的关键约束:
- 起始版本:
4.1.0(11) - Phone/Tablet 可正常调用,其他设备无效果
- 仅 Stage 模型可用
- 必须先在 AppGallery Connect 申请"实况窗服务"权益
二、liveViewManager 官方核心结构
liveViewManager.LiveView {
id: number; // 业务唯一标识
event: string; // 场景类型字面量,如 'PICK_UP' / 'TAXI' / 'ORDER'
sequence: number; // 更新序列号,必须单调递增
isMute: boolean; // 是否静音
liveViewData: {
primary: { // 卡片态(通知中心/锁屏)
title: string;
content: RichText[];
keepTime: number;
clickAction: Want; // 必须用 wantAgent.getWantAgent 构建
extensionData?: { text: string; type: ExtensionType };
layoutData?: { layoutType: LayoutType; title?: string; content?: string; ... };
};
capsule: { // 状态栏胶囊态
type: CapsuleType;
status: number;
icon: string;
title: string;
content: string;
backgroundColor: string; // '#AARRGGBB'
};
};
}
三、完整实战:景区排队实况窗
3.1 实况窗管理模块(完全对齐官方示例)
/**
* @file ScenicQueueLiveView.ets
* @description 高校周边通 · 景区排队一眼即得
*/
import { liveViewManager } from '@kit.LiveViewKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Want, wantAgent } from '@kit.AbilityKit';
const DOMAIN = 0x0000;
const TAG = 'ScenicQueueLiveView';
export class ScenicQueueLiveView {
// 国庆景区排队专用 ID(同一景区复用同一 ID,便于多端共享)
private static readonly ID_PREFIX = 20261001;
private static sequence = 0;
/**
* 构建 WantAgent(完全按官方示例)
*/
private static async buildWantAgent(): Promise<Want> {
const wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: 'com.example.campus.explorer',
abilityName: 'EntryAbility'
} as Want
],
actionType: wantAgent.OperationType.START_ABILITIES,
requestCode: 0,
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
try {
const agent = await wantAgent.getWantAgent(wantAgentInfo);
return agent;
} catch (e) {
const err: BusinessError = e as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to get wantAgent: ${err.message}`);
throw e as Error;
}
}
/**
* 前置检查:开关是否打开
*/
static async isEnabled(): Promise<boolean> {
try {
return await liveViewManager.isLiveViewEnabled();
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG,
`isLiveViewEnabled failed. Code: ${err.code}, message: ${err.message}`);
return false;
}
}
/**
* 创建景区排队实况窗(官方示例改写)
*/
static async startQueue(
spotId: number,
spotName: string,
queueAhead: number,
waitMinutes: number
): Promise<void> {
try {
ScenicQueueLiveView.sequence += 1;
const liveView: liveViewManager.LiveView = {
id: ScenicQueueLiveView.ID_PREFIX + spotId,
event: 'PICK_UP', // 官方示例使用的字符串字面量
sequence: ScenicQueueLiveView.sequence,
isMute: false,
liveViewData: {
primary: {
title: `${spotName} · 排队中`,
content: [
{ text: '当前排队:前方还有 ' },
{ text: `${queueAhead}`, textColor: '#FFFF0000' },
{ text: ' 位' }
],
keepTime: 30,
clickAction: await ScenicQueueLiveView.buildWantAgent(),
extensionData: {
text: `预计 ${waitMinutes} 分钟`,
type: liveViewManager.ExtensionType.EXTENSION_TYPE_COMMON_TEXT
},
layoutData: {
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
title: '排队号',
content: String(queueAhead),
underlineColor: '#FFFF0000',
descPic: 'rider.png'
}
},
capsule: {
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1,
icon: 'scenic.png',
title: '排队中',
content: `${waitMinutes}min`,
backgroundColor: '#FFE60019' // 国庆中国红
}
}
};
liveViewManager.startLiveView(liveView).then(
(liveViewResult: liveViewManager.LiveViewResult) => {
hilog.info(DOMAIN, TAG,
`Succeeded in starting liveView: ${JSON.stringify(liveViewResult)}`);
}
).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG,
`Failed to start liveView: ${err.code} ${err.message}`);
});
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.error(DOMAIN, TAG,
`Failed to start liveView: ${e.code} ${e.message}`);
}
}
/**
* 更新实况窗(轮询到新进度时调用)
*/
static async updateQueue(
spotId: number,
spotName: string,
queueAhead: number,
waitMinutes: number
): Promise<void> {
try {
ScenicQueueLiveView.sequence += 1;
const liveView: liveViewManager.LiveView = {
id: ScenicQueueLiveView.ID_PREFIX + spotId,
event: 'PICK_UP',
sequence: ScenicQueueLiveView.sequence,
isMute: true,
liveViewData: {
primary: {
title: '即将到号',
content: [
{ text: '请前往检票口,您前面还有 ' },
{ text: `${queueAhead}`, textColor: '#FFFF0000' },
{ text: ' 位' }
],
keepTime: 30,
clickAction: await ScenicQueueLiveView.buildWantAgent(),
extensionData: {
text: `预计 ${waitMinutes} 分钟`,
type: liveViewManager.ExtensionType.EXTENSION_TYPE_COMMON_TEXT
},
layoutData: {
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
title: '排队号',
content: String(queueAhead),
underlineColor: '#FF43A047',
descPic: 'rider.png'
}
},
capsule: {
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1,
icon: 'scenic.png',
title: '即将到号',
content: `${waitMinutes}min`,
backgroundColor: '#FF43A047'
}
}
};
liveViewManager.updateLiveView(liveView).then(
(liveViewResult: liveViewManager.LiveViewResult) => {
hilog.info(DOMAIN, TAG,
`Succeeded in updating liveView: ${JSON.stringify(liveViewResult)}`);
}
).catch((err: BusinessError) => {
hilog.error(DOMAIN, TAG,
`Failed to update liveView: ${err.code} ${err.message}`);
});
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.error(DOMAIN, TAG,
`Failed to update liveView: ${e.code} ${e.message}`);
}
}
/**
* 结束实况窗
*/
static async stopQueue(spotId: number): Promise<void> {
ScenicQueueLiveView.sequence += 1;
const liveView: liveViewManager.LiveView = {
id: ScenicQueueLiveView.ID_PREFIX + spotId,
event: 'PICK_UP',
sequence: ScenicQueueLiveView.sequence,
isMute: true
} as liveViewManager.LiveView;
try {
await liveViewManager.stopLiveView(liveView);
hilog.info(DOMAIN, TAG, `Stop liveView for spot ${spotId}`);
} catch (error) {
const err = error as BusinessError;
hilog.warn(DOMAIN, TAG,
`stopLiveView: Code ${err.code}, message: ${err.message}`);
}
}
}
3.2 UI 层:景区详情页
/**
* @file ScenicDetailPage.ets
* @description 高校周边通 · 景区详情页
*/
import { ScenicQueueLiveView } from '../utils/ScenicQueueLiveView';
interface QueueStatus {
spotId: number;
spotName: string;
queueAhead: number;
waitMinutes: number;
}
@Entry
@Component
struct ScenicDetailPage {
@State status: QueueStatus = {
spotId: 1001,
spotName: '故宫博物院',
queueAhead: 128,
waitMinutes: 45
};
@State liveViewOn: boolean = false;
async aboutToAppear() {
this.liveViewOn = await ScenicQueueLiveView.isEnabled();
if (this.liveViewOn) {
await ScenicQueueLiveView.startQueue(
this.status.spotId,
this.status.spotName,
this.status.queueAhead,
this.status.waitMinutes
);
}
}
aboutToDisappear() {
ScenicQueueLiveView.stopQueue(this.status.spotId);
}
build() {
Column() {
// 国庆主题头图
Row() {
Text(`${this.status.spotName} · 国庆特辑`)
.fontSize(18)
.fontColor(Color.White)
}
.width('100%').height(80)
.linearGradient({
angle: 90,
colors: [['#FFE60019', 0.0], ['#FFFF0000', 1.0]]
})
// 当前排队信息
Column() {
Text(`前方还有 ${this.status.queueAhead} 位`)
.fontSize(36).fontWeight(FontWeight.Bold)
.fontColor('#1A1B26')
Text(`预计等待 ${this.status.waitMinutes} 分钟`)
.fontSize(14).fontColor('#999999')
.margin({ top: 8 })
}
.padding(24)
.width('90%')
.backgroundColor('#F8F8F8')
.borderRadius(12)
.margin({ top: 24 })
// 模拟进度刷新
Button('模拟:减少 10 位')
.margin({ top: 16 })
.onClick(async () => {
this.status.queueAhead = Math.max(0, this.status.queueAhead - 10);
this.status.waitMinutes = Math.max(1, this.status.waitMinutes - 3);
if (this.liveViewOn) {
await ScenicQueueLiveView.updateQueue(
this.status.spotId,
this.status.spotName,
this.status.queueAhead,
this.status.waitMinutes
);
}
})
if (this.liveViewOn) {
Text('✓ 实况窗已开启:胶囊 / 锁屏均可查看')
.fontSize(12)
.fontColor('#43A047')
.margin({ top: 8 })
}
}
.width('100%').height('100%')
.backgroundColor(Color.White)
}
}
四、官方错误码速查
| 错误码 | 错误信息 | 排查 |
|---|---|---|
401 | Parameter error | 必填参数缺失 |
1003500004 | LiveView is not enabled | 用户未在设置中开启 |
1003500005 | The right of liveView is not enabled | 未在 AGC 申请权益 |
1003500006 | The liveView already exists | 同 ID 已存在 |
1003500008 | Over max number liveViews per second | 1 秒内创建/更新超过 5 个 |
1003500009 | The liveView does not exist | 更新时 ID 不存在 |
1003500011 | The liveView sequence is incorrect | sequence 必须单调递增 |
五、节后运维
国庆结束后,建议在 v3.6.1 版本中:
- 给每个景区的实况窗增加 24 小时 TTL
- 集成
liveViewManager.getActiveLiveView()(API 18+)查询仍存活的活动 - 关闭订单即触发
stopQueue()
六、写在最后
「景区排队一眼即得」是 HarmonyOS 7 实况窗在「高校周边通」里的典型应用。它把"打开 App 刷新"的动作简化成"看状态栏胶囊",让国庆出行真正变得轻松。这正是 HarmonyOS 7 强调的"零层级直达"理念的最佳实践。
更多推荐



所有评论(0)