导览音频切换太快,旧讲解不能覆盖新展品

在数字博物馆里连续点"云雷纹铜尊""鎏金羽人灯""错金银兽首",最晚准备完成的那段讲解不一定属于最后选中的展品。若每个异步回调都直接把标题和播放状态写回页面,用户已经站在兽首前,耳机里却开始播铜尊介绍。

展厅页面把展品、展厅路线和音频控制放在同一块深色画布上。我把"选中的展品"和"正在准备的音频请求"分开,只有仍属于当前选择的回调才允许改变播放器状态。

展品选择先产生一个会话版本

ts 复制代码
export interface Exhibit {
  id: string
  hallId: string
  title: string
  audioUri: string
  durationMs: number
}

export interface AudioState {
  exhibitId: string
  phase: 'idle' | 'preparing' | 'ready' | 'playing' | 'paused' | 'error'
  requestId: number
  positionMs: number
  message: string
}

export class GuideController {
  selectedExhibitId: string = ''
  audio: AudioState = {
    exhibitId: '', phase: 'idle', requestId: 0, positionMs: 0, message: ''
  }

  select(exhibit: Exhibit): number {
    const requestId = this.audio.requestId + 1
    this.selectedExhibitId = exhibit.id
    this.audio = {
      exhibitId: exhibit.id,
      phase: 'preparing',
      requestId,
      positionMs: 0,
      message: `正在准备《${exhibit.title}》`
    }
    return requestId
  }
}

每次点击都增加 requestId,原播放器是否真正停止由音频适配器处理,页面状态先进入新的准备阶段,避免沿用前一件展品的进度条。

回调需要同时核对展品和版本

ts 复制代码
markReady(exhibitId: string, requestId: number): boolean {
  if (this.audio.exhibitId !== exhibitId || this.audio.requestId !== requestId) {
    return false
  }
  this.audio = { ...this.audio, phase: 'ready', message: '可以开始讲解' }
  return true
}

markError(exhibitId: string, requestId: number, reason: string): boolean {
  if (this.audio.exhibitId !== exhibitId || this.audio.requestId !== requestId) {
    return false
  }
  this.audio = { ...this.audio, phase: 'error', message: reason }
  return true
}

旧请求准备失败也会被忽略,不会在新展品页面冒出无关的错误提示。用户重新点回同一件展品时会产生新的请求版本,旧版本仍然不能重新接管。

讲解进度按展品保存

用户从青铜厅走到丝路厅,再折返回来,应该续播之前的位置。进度由 exhibitId 索引,不能只保存一个全局 positionMs。

ts 复制代码
export interface PlaybackMemory {
  exhibitId: string
  positionMs: number
  updatedAt: number
}

export function savePosition(
  memories: PlaybackMemory[],
  next: PlaybackMemory
): PlaybackMemory[] {
  const rest = memories.filter((item: PlaybackMemory) => item.exhibitId !== next.exhibitId)
  return [...rest, next]
}

export function positionOf(memories: PlaybackMemory[], exhibitId: string): number {
  return memories.find((item: PlaybackMemory) => item.exhibitId === exhibitId)?.positionMs ?? 0
}

正在播放时每隔一段时间保存一次,退出展厅时再做最后一次写入。更新过于频繁会增加存储开销,间隔过长又容易丢进度,因此保存频率应由控制器统一节流。

路线高亮只跟随展厅 ID

路线面板上的序厅、青铜厅、丝路厅和当代厅并不等于数组下标。导览路线可能按无障碍模式删除某一站,页面用 hallId 计算高亮和下一站。

ts 复制代码
export interface HallRoute {
  hallId: string
  order: number
}

export function nextHall(route: HallRoute[], currentHallId: string): string | undefined {
  const sorted = route.slice().sort((a: HallRoute, b: HallRoute) => a.order - b.order)
  const index = sorted.findIndex((item: HallRoute) => item.hallId === currentHallId)
  return index >= 0 ? sorted[index + 1]?.hallId : undefined
}

当前音频、路线和展品画面都以本地数据演示。接入真实音频播放器后,适配器只需要接收 exhibitId 和 requestId;无论网络缓冲或资源准备先后到达,旧讲解都不会盖掉用户最后选择的展品。

相关推荐
福兮说12 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理12 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆66613 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
凤城老人13 小时前
从 PyQt6 到 Electron:给 Edge TTS 做一个“多角色配音机“的踩坑手记
javascript·typescript·electron
Dovis(誓平步青云)13 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan14 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_14 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒15 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技15 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端