HarmonyOS 7 闪控窗排队信息分层与轻操作:连载04(共08篇)【共创稿事节】
HarmonyOS 7 闪控窗排队信息分层与轻操作:连载04(共08篇)【共创稿事节】
闪控球适合持续呈现一两项关键字段,但用户临近展厅入口时,往往需要同时确认活动名称、当前序号、预计等候时间以及入场凭证入口。如果让这些信息全部挤进简短标题,系统提供的固定模板就会失去可读性。HarmonyOS 7 的闪控窗允许应用加载专门的 ArkUI 内容页,在独立的小窗口里组织信息层级和少量操作,适合把排队摘要扩展为可直接使用的状态卡片。
上一篇已经用 floatingBall 展示排队简要状态。这一篇继续使用同一组 EventInfo、EntryPass 和 QueueSnapshot,完成闪控窗的内容页、操作分区和启动桥接。核心约束仍然是:排队阶段由业务归约器决定,窗口负责把结果呈现给用户;刷新请求和返回凭证动作应进入已有业务链路,不应在窗口里自行更改排队资格。

一、把窗口分成四个有明确职责的区域
对于一张城市艺术夜展凭证,小窗口内最重要的信息通常是“我参加什么活动”“现在排到哪里”“接下来要做什么”。将它们从上往下安排,可以避免用户反复寻找叫号状态。设计上建议把活动名称与阶段放在首屏,序号作为最大字号,等待时间紧随其后,再提供刷新与查看凭证的操作。
| 区域 | 内容 | 表达原则 |
|---|---|---|
| 标题区 | 活动名称、当前阶段 | 文案短、状态明确 |
| 核心进度区 | 当前序号、预计等待时间 | 数字突出,不以颜色作为唯一依据 |
| 操作区 | 刷新进度、查看入场凭证 | 控制在两项常用动作以内 |
| 辅助信息区 | 场馆入口、数据更新时间 | 辅助判断信息是否过期 |
小窗口与完整页面不是尺寸缩放的关系。主页面有活动封面、路线、场馆介绍和历史凭证;闪控窗只保留当前这一张入场凭证的相关信息。多张凭证并存时,需要先由用户在主页面确定关注对象,再用稳定的 ticketId 形成窗口投影,不能用活动列表的第一行代替用户选择。
当业务阶段从 WAITING 进入 CALLED,最重要的信息应从序号改为“请前往入口”;到达 ADMITTED 后,则不再突出等候时间。处理方式是先生成一个适合窗口的投影对象,再由页面做条件渲染,而不是在每个 Text() 组件里重复编写 switch 语句。
二、复用排队快照构建窗口投影
在 logic/QueueFloatProjection.ets 中,把连载02关联后的活动资料与连载01的排队快照转为 QueueFloatPanel。该对象只包含界面必要字段,不保存核验密钥,不负责修改原始业务记录。为了方便分辨数据是否过期,投影中保留原始 seq 和可展示的更新时间文本。
import { EntryPhase, QueueSnapshot, phaseLabel } from '../model/QueueModel';
import { EventInfo, EntryPass } from '../model/EventCatalog';
export interface QueueFloatPanel {
ticketId: string;
seq: number;
eventTitle: string;
phaseText: string;
positionText: string;
waitText: string;
venueText: string;
updatedText: string;
canOpenPass: boolean;
}
export function toFloatPanel(event: EventInfo, pass: EntryPass,
snapshot: QueueSnapshot): QueueFloatPanel {
const matches: boolean = pass.ticketId === snapshot.ticketId &&
pass.eventId === snapshot.eventId && event.eventId === pass.eventId;
const waiting: boolean = snapshot.phase === EntryPhase.WAITING;
const called: boolean = snapshot.phase === EntryPhase.CALLED;
return {
ticketId: pass.ticketId,
seq: snapshot.seq,
eventTitle: event.title,
phaseText: matches ? phaseLabel(snapshot.phase) : '进度待更新',
positionText: matches && waiting ? `${snapshot.position}` : (matches && called ? '请前往入口' : '—'),
waitText: matches && waiting ? `预计等待${snapshot.waitMinutes}分钟` : '查看最新入场信息',
venueText: event.venue,
updatedText: matches ? new Date(snapshot.updatedAt).toLocaleTimeString() : '等待新数据',
canOpenPass: matches
};
}
这个函数仍然执行活动、凭证和快照的三方匹配。窗口的展示内容可以精简,但身份校验不能精简。当匹配失败时,页面保留活动资料并提示“进度待更新”,避免把其他凭证的序号拼到当前活动里。终止阶段用清楚的状态文字覆盖进度区域,用户能够理解本次入场记录已经发生变化。
投影里的时间字符串由界面格式化。如果应用面向跨地区用户,还应结合活动所属时区决定时间文案。序号是队列提供的字段,等候分钟数来自快照,不应由窗口内一个单独的定时器自行递减。用户即使看到估计时间接近零,也仍需等待业务事件确认入场阶段变化。
三、ArkUI 内容页实现层级布局
闪控窗可以通过 setUIContext() 加载工程内的页面,页面路径必须登记在 resources/base/profile/main_pages.json 中。相比直接把主页面搬入窗口,新增 pages/FloatViewPage.ets 更容易控制字重、留白和按钮尺寸。这里采用 @LocalStorageLink 接收启动时传入的窗口数据,并将点击动作交给业务入口转发层。
QueueFloatActions 是应用自身的轻操作注册器,包含 onRefresh 和 onOpenPass 两个回调,后面由窗口控制器绑定实现;它不对应系统 API。
// logic/QueueFloatActions.ets
export class QueueFloatActions {
static onRefresh: (() => void) | undefined;
static onOpenPass: (() => void) | undefined;
}
pages/FloatViewPage.ets:
import { QueueFloatPanel } from '../logic/QueueFloatProjection';
import { QueueFloatActions } from '../logic/QueueFloatActions';
@Entry({ useSharedStorage: true })
@Component
struct FloatViewPage {
@LocalStorageLink('queuePanel') panel: QueueFloatPanel = {
ticketId: '', seq: 0, eventTitle: '活动入场',
phaseText: '进度待更新', positionText: '—',
waitText: '查看最新入场信息', venueText: '',
updatedText: '', canOpenPass: false
};
build() {
Column({ space: 12 }) {
Row() {
Text(this.panel.eventTitle)
.fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1)
Text(this.panel.phaseText).fontSize(14).fontColor('#2563EB')
}.width('100%')
Column({ space: 6 }) {
Text('当前排队信息').fontSize(13).fontColor('#64748B')
Text(this.panel.positionText)
.fontSize(35).fontWeight(FontWeight.Bold).fontColor('#1348A8')
Text(this.panel.waitText).fontSize(15)
}.width('100%').padding(14)
.backgroundColor('#EEF5FF').borderRadius(16)
Text(this.panel.venueText).fontSize(13).fontColor('#475569')
Row({ space: 10 }) {
Button('刷新进度').layoutWeight(1)
.onClick(() => QueueFloatActions.onRefresh?.())
Button('查看凭证').layoutWeight(1)
.enabled(this.panel.canOpenPass)
.onClick(() => QueueFloatActions.onOpenPass?.())
}.width('100%')
Text(`更新于 ${this.panel.updatedText}`)
.fontSize(12).fontColor('#64748B')
}.width('100%').padding(16)
.backgroundColor(Color.White)
}
}
这段布局把主状态放在同一个背景容器中,让较小尺寸下的阅读顺序更稳定。两个按钮不设置自动提交排队信息的行为;“刷新进度”向业务层发起重新获取快照的请求,“查看凭证”恢复主窗口,再由主窗口根据 ticketId 定位凭证详情。这样既能减少窗口内操作的复杂度,也可以让凭证详情沿用原来的安全校验流程。
如果文案长度超过预期,优先压缩场馆地址的展示长度,保留完整阶段和操作按钮。不能为了让一个数字更醒目而隐藏失效状态,也不能把“已取消”显示为灰色但继续保留可点击的入场操作。
四、创建闪控窗与注入窗口数据
HarmonyOS 7(API 26)的 floatView 提供 isFloatViewEnabled()、create()、getFloatViewLimits() 以及 FloatViewController 的 setUIContext()、setWindowSize()、start() 等接口。窗口采用 Stage 模型,使用前需在模块配置中声明 ohos.permission.FLOAT_VIEW,并按照用户授权流程取得权限。闪控窗与已启动的闪控球、画中画存在互斥关系,从上篇的球切换到窗口前,需要先完成停止和状态清理。
下面的网关只负责创建窗口和连接 UI 内容,业务状态仍由原有模型负责。LocalStorage 从 toFloatPanel() 创建初始值,再交给独立页面读取。窗口标题栏和系统级拖动行为由窗口能力管理,页面无需自行编写一套拖拽手势。
import { floatView } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { EventInfo, EntryPass } from '../model/EventCatalog';
import { QueueSnapshot } from '../model/QueueModel';
import { toFloatPanel } from './QueueFloatProjection';
import { QueueFloatActions } from './QueueFloatActions';
export class QueueFloatGateway {
private controller?: floatView.FloatViewController;
private storage?: LocalStorage;
private started: boolean = false;
async open(ctx: common.UIAbilityContext, event: EventInfo,
pass: EntryPass, snapshot: QueueSnapshot,
refresh: () => void): Promise<boolean> {
if (!canIUse('SystemCapability.Window.SessionManager') ||
!floatView.isFloatViewEnabled()) return false;
try {
if (this.controller) return this.started;
const config: floatView.FloatViewConfiguration = {
context: ctx,
templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE
};
const current = await floatView.create(config);
this.controller = current;
this.storage = new LocalStorage({
queuePanel: toFloatPanel(event, pass, snapshot)
});
current.onStateChange((info: floatView.FloatViewStateChangeInfo) => {
this.started = info.state === floatView.FloatViewState.STARTED;
if (info.state === floatView.FloatViewState.STOPPED) {
current.offStateChange();
QueueFloatActions.onRefresh = undefined;
QueueFloatActions.onOpenPass = undefined;
this.storage = undefined;
if (this.controller === current) this.controller = undefined;
}
});
QueueFloatActions.onRefresh = refresh;
QueueFloatActions.onOpenPass = (): void => {
const params: Record<string, Object> = { ticketId: pass.ticketId };
current.restoreMainWindow(params)
.catch((e: Error) => console.error(`返回凭证页失败:${e.message}`));
};
await current.setUIContext('pages/FloatViewPage', this.storage);
const limits = floatView.getFloatViewLimits(
floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
await current.setWindowSize(limits.maxSize);
await current.start();
return true;
} catch (e) {
console.error(`闪控窗开启失败:${String(e)}`);
this.controller?.offStateChange();
this.controller = undefined;
this.storage = undefined;
QueueFloatActions.onRefresh = undefined;
QueueFloatActions.onOpenPass = undefined;
return false;
}
}
}
setWindowSize() 的尺寸使用系统返回的像素范围,而 ArkUI 页面布局中的数值通常按逻辑布局单位理解,不能混成一套固定值。系统窗口还可能因为设备形态变化改变尺寸,后续可以通过尺寸相关回调适配紧凑布局。上例的 start() 完成仅代表启动请求得到处理,后续需观察状态事件中的 STARTED;STOPPED 到达时也应释放按钮回调和数据引用,避免旧窗口的处理函数被新窗口沿用。

五、窗口操作要与业务动作严格分离
“刷新进度”只是一次主动拉取请求。即使网络返回了更旧的进度,也必须先经过连载01的 reduceQueue() 才能更新界面。对于快速连续点击,可以在请求层设置单飞控制:已有刷新请求尚未完成时,复用当前请求或暂时禁用按钮,防止同一凭证产生多路竞争更新。按钮反馈应区分“正在刷新”“暂无新变化”和“获取失败”。
“查看凭证”则更需要避免越权。控制器可以在用户点击窗口后调用 restoreMainWindow(),向主窗口传递 ticketId。主窗口接到参数后,必须根据本地会话和服务端凭证归属重新定位目标,而不是把这个字符串当成已通过核验的授权结果。闪控窗仅承担跳转意图,凭证详情与核验逻辑仍留在完整应用页面。

六、结合窗口状态排查常见异常
| 异常表现 | 重点位置 | 建议处理 |
|---|---|---|
| 点击开启后看不到窗口 | 支持状态、授权结果与状态回调 | 先检查 STARTED 是否到达 |
| 窗口创建后立即失败 | 页面路径、模板与尺寸 | 对齐 main_pages.json 和尺寸范围 |
| 与闪控球切换时出错 | 两种窗口是否仍然同时活动 | 等前一个窗口完成停止再启动 |
| 点击刷新但数字不变 | 是否收到更新版本、是否被归约器拒绝 | 对比 ticketId 与 seq |
| 关闭窗口后按钮仍响应 | 是否释放窗口动作注册器 | 在停止回调中清理持有关系 |
| 凭证页打开了错误活动 | 是否只凭列表下标定位 | 按凭证 ID 再查活动归属 |
这类问题应优先从“业务事件版本 → 投影结果 → LocalStorage 中的字段 → 窗口生命周期”逐层追踪。若业务快照本身旧,调高界面刷新频率无济于事;若快照已新而窗口仍旧,则应关注投影替换和跨 UI 内容的状态传递。日志建议记录阶段、版本和错误码即可,避免输出完整票证或个人信息。
七、把扩展留在明确的层次
这一阶段形成了完整的闪控窗基本形态:原有活动和凭证实体提供身份数据,排队快照提供进度事实,投影层组织窗口文案,ArkUI 页面负责视觉层级,floatView 控制器负责窗口创建和生命周期。后续加入高频进度消息时,只需要让新的投影结果进入 LocalStorage,并通过窗口状态控制刷新节奏,无须重新设计入场凭证和排队规则。
闪控球和闪控窗分别满足“快速扫一眼”和“完成少量操作”的需要。它们可以共享相同的业务状态,但不应拥有彼此独立的排队计算逻辑。保持一个事实来源、一个当前关注凭证、清楚的窗口切换次序,才能让轻量交互在业务状态不断变化时依然可靠。
更多推荐


所有评论(0)