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

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

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

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

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;无论网络缓冲或资源准备先后到达,旧讲解都不会盖掉用户最后选择的展品。

相关推荐
gqc584 分钟前
Markdown 写完怎么验证?我现在的 4 步本地检查流程
前端
普通人617 分钟前
uniapp模版JS
开发语言·javascript·小程序·uni-app·1024程序员节
KumaoHub17 分钟前
棋牌游戏开发中聊天文本的长度限制与安全显示
javascript
计算机魔术师32 分钟前
我重算了新云的账,发现亏的不是算力,是时间
前端
pigdreams35 分钟前
Google 官方 Android CLI:让AI直接读布局结构而不是用adb截图验证
android·人工智能·adb
G018_star sky♬1 小时前
nodeJS他是如何用JavaScript封装C++的呢
开发语言·javascript·c++
wiseyao12191 小时前
uniapp scroll-view滚动到最后
前端·javascript·uni-app
陈随易1 小时前
从今天起,你只需要一个浏览器之Ego lite
前端·后端·程序员
怕浪猫1 小时前
Step 5 Preview 模型写 3D 象棋,两个小时上线
前端·javascript·面试
Highcharts.js2 小时前
Highcharts甘特图开发带导航功能的项目管理图示例
开发语言·前端·javascript·甘特图·highcharts·项目管理图