Date: 2026-05-07
Status: Draft
Touches: PPT/src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue(仅前端)
DialogueChatView 触发会话结束后(无论自动 / 手动),都会通过 fetchReportSafe 调用 engine.completeSession() + engine.getReport() 拉报告。轮询最坏 30 × 2s = 60s。期间的 UX 现状:
| 路径 | 触发 | 期间 UI 表现 |
|---|---|---|
自动完成(流事件 event.isComplete === true) |
engine.isComplete watcher → fetchReportSafe | state === 'finalizing':仅在底部 control zone 一行小字 + spinner |
| 倒计时归零 | startCountdown 把 isComplete 置 true → watcher → fetchReportSafe |
同上 |
| 手动结束(点"结束并查看报告") | handleExitConfirm → fetchReportSafe |
无任何 loading:弹窗关闭 → 用户盯着原界面 → 父组件突然换页 |
两个问题:
completeSession() 在 fetchReportSafe 内部执行,所以 engine.isComplete 在调用栈里才变 true,期间 state === 'finalizing' 的判定 engine.isComplete && reportFetchInflight 无法满足,底部条不显示)。DialogueChatView 的 loading 界面,覆盖至 report 拉到 / 失败为止。fetchReportSafe 已经 catch 并 return null,父组件 handleDialogueComplete(null) 已渲染错误态(TopicDiscussionPreview.vue:442-449),loading 不重复实现错误 UI。state-finalizing 死代码。<template>
<div class="dialogue-chat-view">
<!-- 早分支:finalizing 全屏占位 -->
<FinalizingScreen v-if="reportFetchInflight" :stats="finalizingStats" />
<!-- 否则正常渲染 header / chat / control zone -->
<template v-else>
<!-- ... 原有结构 ... -->
</template>
</div>
</template>
触发源唯一为 reportFetchInflight.value === true。该 ref 已存在(DialogueChatView.vue:630),由 fetchReportSafe 在 try-finally 里设置。三条结束路径都经过 fetchReportSafe,覆盖完整。
为什么不依赖 engine.isComplete:手动路径下 completeSession() 在 fetchReportSafe 内部,isComplete 与 reportFetchInflight 不同步。用单变量触发是消除时序差的最干净做法。
整屏垂直居中,背景白(与 .dialogue-chat-view 一致):
┌──────────────────────────────────┐
│ │
│ │
│ [ spinner ] │
│ │
│ AI 正在为你整理报告... │
│ │
│ ┌────────────────────┐ │
│ │ 3 轮 · 9 句 │ │
│ │ 累计时长 4:32 │ │
│ └────────────────────┘ │
│ │
│ │
└──────────────────────────────────┘
.spinner 样式(橙色、spin keyframe),尺寸放大到 28-32px。#374151。#f9fafb)、圆角、内边距 16-20px、字号 12px,#6b7280。数据来源(全部前端就地取数,不依赖后端):
| 字段 | 来源 | 备注 |
|---|---|---|
| 轮次 | engine.currentRound.value |
已显示在 header |
| 学生句数 | engine.messages.value.filter(m => m.role === 'student' && m.status !== 'error').length |
排除失败消息 |
| 累计时长 | engine.messages.value.filter(m => m.role === 'student').reduce((s, m) => s + (m.audioDuration ?? 0), 0) |
audioDuration 已写入 student message(见 2026-05-07-audio-duration-and-replay-design.md),单位秒;旧数据 NULL 则按 0 计 |
时长用现有的 formatSeconds(s) 渲染(DialogueChatView.vue:694)。
方案 X:根级 v-if 替换——不是浮层。reportFetchInflight === true 时整棵原有树(header + 聊天区 + silence-hint + control-zone + 各种 modal)全部不渲染,只渲染 finalizing 屏。
为什么替换不是浮层:
:disabled="reportFetchInflight",比 v-if 早返回更啰嗦。实现形态:把 finalizing 屏内联在 DialogueChatView.vue 的 <template> 里,不抽组件——内容简单(spinner + 文案 + 一个数据卡),无复用价值。
启用 v-if 早分支后,control zone 里的 'finalizing' 状态层永远不会被渲染(外层提前替换),属于死代码:
删 state computed 内 'finalizing' 分支:
if (engine.isComplete.value) return reportFetchInflight.value ? 'finalizing' : 'done'
改成:
if (engine.isComplete.value) return 'done'
(reportFetchInflight === true 时根级 v-if 已经接管,不再走 state machine。)
删 state 类型联合中的 'finalizing' literal。
删 control zone 里整个 <div class="state-layer state-center" :style="stateStyle('finalizing')"> 块及其内容。
完全不在 finalizing 屏内处理。fetchReportSafe 现有逻辑:
async function fetchReportSafe() {
reportFetchInflight.value = true
try {
await engine.completeSession()
return await engine.getReport()
} catch (err) {
console.warn('[speaking] getReport failed:', err)
return null
} finally {
reportFetchInflight.value = false
}
}
无论成功 / 失败 / 超时,reportFetchInflight 都会归 false → finalizing 屏自动消失 → 父组件 complete 事件接管。父组件 handleDialogueComplete(null) 已经在 'completed' 阶段渲染 reportError 文案(TopicDiscussionPreview.vue:443-449),覆盖三种失败语义(超时、failed、incomplete)。
不增加重试按钮——重试入口在父组件 report-stage 已隐含(用户看到错误后可走 restart / 关闭重开),finalizing 屏只负责"等待中"语义。
finalizing 屏不提供"取消"按钮:
completeSession 已经触发后端状态变更,回退无意义。getReport 即使打断,下次进同一 session 仍会拉到(或拉不到)同样的报告。useDialogueEngine——reportFetchInflight 是 view-local state,不下沉到 composable。TopicDiscussionPreview——complete 事件契约不变,错误态渲染逻辑不变。人工验证三条路径:
| 场景 | 操作 | 期望 |
|---|---|---|
| 自动完成 | 完成最后一轮 → 看 finalizing | 立即出现全屏 loading,原 header / 控制条消失;报告拉到后父组件切到报告页 |
| 倒计时归零 | 等 countdown 到 0 | 同上 |
| 手动结束 | 点三点 → 结束并查看报告 | 弹窗关闭后立即出现全屏 loading(不再是空白等待) |
| 报告失败 | 模拟 getReport reject |
finalizing 屏消失,父组件报告页显示错误文案 |
| 数据卡 0 句场景 | 极端:刚开 session 就立刻结束 | 卡片显示"0 轮 · 0 句 · 0:00",不报错 |