高校周边通 · 国庆特别版: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)
  }
}

四、官方错误码速查

错误码错误信息排查
401Parameter error必填参数缺失
1003500004LiveView is not enabled用户未在设置中开启
1003500005The right of liveView is not enabled未在 AGC 申请权益
1003500006The liveView already exists同 ID 已存在
1003500008Over max number liveViews per second1 秒内创建/更新超过 5 个
1003500009The liveView does not exist更新时 ID 不存在
1003500011The liveView sequence is incorrectsequence 必须单调递增

五、节后运维

国庆结束后,建议在 v3.6.1 版本中:

  1. 给每个景区的实况窗增加 24 小时 TTL
  2. 集成 liveViewManager.getActiveLiveView()(API 18+)查询仍存活的活动
  3. 关闭订单即触发 stopQueue()

六、写在最后

「景区排队一眼即得」是 HarmonyOS 7 实况窗在「高校周边通」里的典型应用。它把"打开 App 刷新"的动作简化成"看状态栏胶囊",让国庆出行真正变得轻松。这正是 HarmonyOS 7 强调的"零层级直达"理念的最佳实践。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐