茶器艺科智造HarmonyOS应用实战-56-新Toast会直接取消旧Toast,导出错误为何一闪而过:加入队列、优先级与安全区
茶器艺科智造HarmonyOS应用实战-56-新Toast会直接取消旧Toast,导出错误为何一闪而过:加入队列、优先级与安全区
应用内 Toast 同时承接切片完成、连接成功、贴图更新、读取失败、智能体错误和 Web 侧 STL 消息。茶器艺科智造当前只有一份文本和一个计时器:任何新消息到来都会清掉旧计时器、覆盖文本并重新计时。于是“正在导出”之后紧接一个普通提示时,重要错误也可能很快被下一条消息替换;固定底部 96vp 又没有显式合并窗口安全区。解决方法不是把所有时长加倍,而是为消息加类型、优先级、最短展示、去重键和可操作性,再由队列统一决定谁能显示。

1. 实际问题:单槽位让最后到达者无条件获胜
当前 showToast() 先取消已有 timeout,再把 chaqiToastText 替换成新文本并显示。时长会限制在 1500–10000ms,但这个下限只对“没有新消息打断”的情况有效。
private showToast(
msg: string,
duration: number = 2000
): void {
if (this.chaqiToastTimerId >= 0) {
clearTimeout(this.chaqiToastTimerId)
this.chaqiToastTimerId = -1
}
this.chaqiToastText = msg
this.chaqiToastVisible = true
const d = Math.max(
1500,
Math.min(10000, duration)
)
this.chaqiToastTimerId = setTimeout((): void => {
this.chaqiToastVisible = false
this.chaqiToastTimerId = -1
}, d) as number
}
源码没有消息 ID、类别、队列、去重、优先级或“至少展示到何时”。因此,旧消息实际展示时间可能远小于传入 duration。本文把“导出错误一闪而过”视为结构允许的风险,没有声称已经在设备上复现某条具体错误。
2. 来源审计:页面与Web都写入同一个槽位
页面内有切片完成、连接、图片大小、读取不完整、格式不支持、贴图更新、智能体错误、视角重置和 STL 流程等多处调用。CupWeb3d.ets 还通过 eventHub 发出 JSON,只包含 m 文本和 d 时长;页面解析后仍调用同一个 showToast。
private readonly onChaqiToast = (
data: object | string
): void => {
let msg = ''
let dur = 3200
if (typeof data === 'string') {
try {
const value = JSON.parse(data) as
Record<string, Object>
msg = String(value['m'] ?? '')
const parsedDuration = Number(value['d'])
if (!isNaN(parsedDuration) &&
parsedDuration > 0) {
dur = parsedDuration
}
} catch (_error) {
msg = data
}
}
if (msg.length > 0) {
this.showToast(msg, dur)
}
}
调用方只能通过时长暗示重要性,管理器无法知道“读取失败”应压过“已重置视角”,也无法识别连续的导出进度是否属于同一任务。修复应先升级消息协议,再替换计时逻辑。

3. typed消息:把重要性和生命周期写进数据
建议定义 ToastMessage。kind 用于视觉和无障碍语义;priority 用于调度;dedupeKey 把同一任务的进度合并;minMs 保证最短阅读时间;maxMs 防止普通消息永久占位;sticky 只给需要用户确认的错误;action 提供“重试”或“查看详情”。
type ToastKind =
| 'info'
| 'success'
| 'warning'
| 'error'
interface ToastAction {
label: string
actionId: string
}
interface ToastMessage {
id: string
kind: ToastKind
priority: number
text: string
createdAtMs: number
minMs: number
maxMs: number
sticky: boolean
dedupeKey?: string
action?: ToastAction
source: 'page' | 'web3d' | 'agent'
}
id 必须唯一,dedupeKey 才是同一逻辑消息的稳定键。例如 STL 导出可用 stl-export:<jobId>,进度更新替换同组 pending 项;error 使用新 ID 并提高优先级。priority 的范围和各类默认值要集中定义,不让调用点随手写 999。
文本也应有长度和字符策略。来自 Web 或异常的原始字符串先映射为用户文案,详细堆栈写受控日志;不要把路径、内部对象或敏感值塞进 Toast。
4. 调度规则:同级排队,高优先级才允许抢占
建议的默认规则是:没有 current 时立即展示;新消息优先级小于或等于 current 时进入队尾;更高优先级只有在 current 已满足 minMs 或 current 允许被抢占时才切换;error 可以抢占普通进度,但被抢占的可恢复消息按剩余时间回队。队列要设上限并优先丢弃重复低优先级 info。
class ToastQueue {
private current?: ToastRuntime
private pending: ToastMessage[] = []
private timerId: number = -1
private generation: number = 0
private readonly maxPending: number = 8
enqueue(message: ToastMessage): void {
const normalized = normalizeToast(message)
this.mergePendingByDedupeKey(normalized)
if (this.current === undefined) {
this.showNext()
return
}
if (this.canPreempt(
normalized,
this.current
)) {
this.preemptCurrent(normalized)
return
}
this.insertByPriority(normalized)
this.trimLowPriorityOverflow()
}
}
maxPending=8 是建议初值,不是系统上限。若错误消息超过队列容量,不能静默丢失;应转入持久的错误中心、任务详情或日志,并让顶部状态可发现。Toast 只适合短时反馈,不应承担全部故障恢复。
5. 计时器需要generation,旧回调不能关闭新消息
每次展示新 current 都递增 generation,并在 timeout 中捕获。旧计时器即便在 clear 与执行边界相撞,也只能结束它所属的那一代。sticky 消息不安排自动关闭;普通消息在 maxMs 后完成并拉取下一条。
private present(message: ToastMessage): void {
this.clearTimer()
const generation = ++this.generation
const now = Date.now()
this.current = {
message,
shownAtMs: now,
earliestDismissAtMs: now + message.minMs
}
this.publishCurrent()
if (!message.sticky) {
this.timerId = setTimeout((): void => {
if (generation !== this.generation) {
return
}
this.finishCurrent('timeout')
}, message.maxMs) as number
}
}
private finishCurrent(reason: string): void {
this.clearTimer()
this.current = undefined
this.publishHidden()
this.showNext()
}
若用户点击 action,先记录 message ID 和 actionId,再结束 current;action 执行失败可以入队一个新的 error,不能重新使用旧 ID。页面离开时应取消 timer、增加 generation、清掉内存队列,或把队列所有权提升到应用级;两种生命周期语义要选一种并写清。

6. Web协议升级:m和d兼容读取,新增kind与dedupeKey
迁移期可以继续接受旧 {m,d},但默认归类为 info;新格式带协议版本、kind、code、jobId 和 progress。页面只接受白名单 kind,限制时长和文本长度,并由 code 映射 priority。外部消息不能直接指定任意高优先级。
interface WebToastPayloadV2 {
v: 2
m: string
kind: string
code?: string
jobId?: string
progress?: number
}
function fromWebToast(
raw: string
): ToastMessage {
const value = JSON.parse(raw) as
Record<string, Object>
const version = Number(value['v'] ?? 1)
const text = safeToastText(
String(value['m'] ?? '')
)
if (version < 2) {
return infoToast(text, 'web3d')
}
const kind = allowToastKind(
String(value['kind'] ?? 'info')
)
const code = safeCode(value['code'])
return policyToast({
kind,
text,
source: 'web3d',
code,
dedupeKey: buildWebDedupeKey(value)
})
}
旧 JSON 解析失败时直接把整串当文案,可能显示结构化垃圾;建议改为统一的“3D 预览返回了无法解析的消息”,原文只进入截断日志。Web 侧导出开始、进度、成功、失败使用相同 jobId,队列才能合并进度并保留最终错误。
7. 安全区:bottom=96改为由窗口和底栏共同计算
ChaqiToastOverlay.ets 当前固定 margin({ bottom: 96 }),最大八行,外层 hitTestBehavior(HitTestMode.Transparent)。固定值没有表达底部系统避让区、浮动 Tab 高度和额外间距的来源;透明命中也意味着当前浮层没有可点击 action。
@Component
export struct ChaqiToastOverlay {
@Prop message: ToastMessage = defaultToast()
@Prop bottomSafeInsetVp: number = 0
@Prop tabOverlayHeightVp: number = 0
onAction: (id: string) => void = () => {}
private bottomOffsetVp(): number {
return this.bottomSafeInsetVp +
this.tabOverlayHeightVp + 12
}
build() {
Stack() {
this.ToastCard()
}
.margin({ bottom: this.bottomOffsetVp() })
}
}
建议由页面读取 bottom avoid area,和当前手机/宽屏底栏实际占用高度一起传入。键盘显示时 Toast 放在键盘上方还是贴窗口底部,需要单独定义。若 message 有 action,卡片必须可命中且不让全屏透明层吞掉后方操作;具体 ArkUI 命中层级要用组件测试确认。
错误内容超过八行时不应简单拉长 Toast。主文案保持短,详情转到弹窗或错误页;action 标签要明确,例如“重试导出”“查看详情”,不要只有“确定”。
8. 无障碍与去重:消息可读、可操作、不过度打扰
状态不能只靠边框颜色区分。浮层应给出“错误”“成功”等文字前缀或图标语义,设置合适的辅助说明,并验证屏幕阅读器能在新消息出现时感知。连续进度不应每 1% 都重复播报,可按 10% 或阶段去重;最终成功和失败必须播报一次。
function announceText(
message: ToastMessage
): string {
switch (message.kind) {
case 'error':
return '错误:' + message.text
case 'warning':
return '提醒:' + message.text
case 'success':
return '成功:' + message.text
default:
return message.text
}
}
function progressDedupeKey(
jobId: string
): string {
return 'stl-export:' + jobId + ':progress'
}
无障碍公告 API、焦点策略和 action 的组件写法应按项目目标 API 文档核实。错误变成 sticky 时不能强制把焦点从用户当前操作抢走;可以提供清晰的关闭按钮和任务详情入口。用户主动关闭后,重复同一错误应遵循冷却和状态变化规则。
9. 验证矩阵、故障排查与证据边界
| 输入顺序 | 预期 current | pending/合并行为 | 关键断言 |
|---|---|---|---|
| info A → info B | A 满足策略后再 B | B 排队 | A 不被立即覆盖 |
| progress 1→2→3 | 最新进度 | 同 dedupeKey 合并 | 队列不膨胀 |
| info → export error | error | info 可回队或结束 | error 优先 |
| error → success | error | success 排队 | error 不一闪而过 |
| error sticky → action | error 到点击 | action 后关闭 | 可恢复 |
| 旧 timer → 新 current | 新消息 | 旧 generation 忽略 | 不误关新消息 |
| 队列超过8条 | 高优先级保留 | 丢弃/合并低级 info | 错误另有记录 |
| 底部安全区变化 | 同一消息 | offset 重算 | 不被手势区遮住 |
| 键盘弹出 | 按产品规则移动 | 不重复入队 | 文本和按钮可见 |
| 页面离开 | 隐藏或移交应用级 | timer 清理 | 无离页回调改 State |
| 现象 | 优先核对 | 可能原因 | 处理方向 |
|---|---|---|---|
| 错误仍被成功提示盖掉 | priority 与 canPreempt | 所有消息仍同级 | 由 code/kind 集中映射 |
| 同一进度排满队列 | dedupeKey | jobId 未传或 key 每次变化 | 稳定任务键合并 |
| 新消息莫名消失 | timer generation | 旧 timeout 关闭新 current | 代次核对 |
| Toast挡住底栏 | bottom inset 与底栏高度 | 仍固定 96vp | 动态合并三个偏移 |
| 按钮点不到 | hitTest 和层级 | 沿用全透明命中 | 仅操作卡片可命中 |
| 页面离开仍弹消息 | 队列所有权 | eventHub/timer 未清 | 明确页面级或应用级 |
| 屏幕阅读反复播报 | 进度更新频率 | 每条都公告 | 阶段化播报 |
| 详情泄露路径 | 文案映射 | 直接显示底层异常 | 诊断码与用户文案分离 |
本文静态核对了当前单文本、单 timer 的 showToast(),新消息先 clear 旧 timer、时长 clamp、eventHub 的 m/d 协议、多种成功与错误调用点,以及浮层固定 bottom 96、最多八行和透明命中。这些是 live 源码事实。它们说明新消息会覆盖旧消息,但不能证明某次导出错误已经在真机一闪而过。
ToastMessage、优先队列、generation、sticky/action、Web v2 协议、动态安全区和无障碍策略均为本文建议,未改入参考项目。本文没有运行构建、组件测试、屏幕阅读器、键盘/手势导航或真实 STL 导出。因此队列调度、点击命中、辅助公告和各窗口模式的偏移仍需在实现后逐项验证;短暂反馈之外的关键错误还应落到可回看的任务记录,不能只靠 Toast。
更多推荐
所有评论(0)