From 8c19e826b3adfa7dd13e35dd14441d93fe779ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Thu, 13 Aug 2026 17:43:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20NIT=20=E5=90=88=E5=B9=B6=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=BC=80=E5=9C=BA=E6=9C=97=E8=AF=BB=20onEnd=20?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E8=87=AA=E5=8A=A8=E8=BF=9B=E5=85=A5=EF=BC=88?= =?UTF-8?q?=E9=98=B2=203s=20=E6=88=AA=E6=96=AD=EF=BC=89/continuePlay=20?= =?UTF-8?q?=E7=A9=BA=E5=AE=88=E5=8D=AB/gotoResult=20=E9=98=B2=E5=8F=8C?= =?UTF-8?q?=E8=B7=B3/=E8=AE=A1=E6=97=B6=E5=99=A8=E5=8D=B8=E8=BD=BD?= =?UTF-8?q?=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-08-13-story-theater-v2.md | 7 +++-- ui-src/src/components/SceneTheater.vue | 27 ++++++++++++++++--- ui-src/src/pages/play/play.vue | 7 ++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-08-13-story-theater-v2.md b/docs/superpowers/plans/2026-08-13-story-theater-v2.md index 0946d3d..454c3cb 100644 --- a/docs/superpowers/plans/2026-08-13-story-theater-v2.md +++ b/docs/superpowers/plans/2026-08-13-story-theater-v2.md @@ -1378,9 +1378,12 @@ git add ui-src/src/components/SceneTheater.vue ui-src/src/components/StoryPlayer git commit -m "feat: 全自动演出——开场自动进入/回复自动继续/终局台词演绎/已播行折叠(v2.1)" ``` -> 审查记录(0f8a371 审查员 1 APPROVE + 1 SHOULD-FIX): +> 审查记录(0f8a371 双审查 APPROVE + d571e89 SHOULD-FIX + NIT 合并提交): > - SHOULD-FIX(已修):终局演绎块与剧情流块是并列 v-if 而非互斥——终局播放期间 `curNode && isStoryNode` 仍为 true,旧节点块(已播行 + ActionCard,choosing=false 时可点击)残留可见可交互。修复:剧情流块与 v1 卡片块加 `!playingFinal` 守卫,三块互斥。 -> - NIT(记录):SceneTheater 3s 上限会截断长音频朗读(v1 既有公式,自动推进后更易暴露);终局长台词由 StoryPlayer MAX_LINE_MS 兜底,无需改 +> - NIT1(已修):continuePlay 无空守卫,点击与 3s 自动同帧时 reply/feedback 为 null 抛 TypeError → 开头加 `if (!this.reply && !this.feedback) return` +> - NIT2(已修):SceneTheater 开场计时器不随卸载清理,跳过后残留 done 触发 → minTimer/autoTimer 存字段,beforeUnmount 清理 +> - NIT3(已修):开场朗读 3s 上限必然截断长文本(30 字约 8-12s 朗读被砍)→ 改 speak onEnd 驱动自动进入(最短展示 1.2s 防无 TTS 闪退,15s 兜底防 onEnd 缺失卡死) +> - NIT4(已修):终局 StoryPlayer done 与「跳过」双触发 gotoResult → doneJumped 一次性守卫 --- diff --git a/ui-src/src/components/SceneTheater.vue b/ui-src/src/components/SceneTheater.vue index 82c80dd..fd60171 100644 --- a/ui-src/src/components/SceneTheater.vue +++ b/ui-src/src/components/SceneTheater.vue @@ -79,11 +79,32 @@ export default { }, mounted() { this.speaking = true - speak(this.content, this.audio) - // v2.1:朗读结束自动进入决策幕(「跳过」仍可手动跳过) - setTimeout(() => { this.speaking = false; this.$emit('done') }, Math.min(3000, this.content.length * 300)) + // v2.1:朗读结束自动进入决策幕——最短展示 1.2s,之后 speak onEnd 即进入; + // 无 TTS/onEnd 缺失最迟 15s 兜底(「跳过」仍可手动跳过) + this.speechDone = false + this.ready = false + let done = false + const finishAuto = () => { + if (done) return + done = true + this.speaking = false + this.$emit('done') + } + this.minTimer = setTimeout(() => { this.ready = true; if (this.speechDone) finishAuto() }, 1200) + this.autoTimer = setTimeout(finishAuto, 15000) + const onSpeechEnd = () => { + this.speechDone = true + if (this.ready) finishAuto() + } + if (!this.content) { + finishAuto() + return + } + speak(this.content, this.audio, onSpeechEnd) }, beforeUnmount() { + clearTimeout(this.minTimer) + clearTimeout(this.autoTimer) stopSpeak() }, methods: { diff --git a/ui-src/src/pages/play/play.vue b/ui-src/src/pages/play/play.vue index fc5a104..983b634 100644 --- a/ui-src/src/pages/play/play.vue +++ b/ui-src/src/pages/play/play.vue @@ -178,7 +178,8 @@ export default { branchTimer: null, replyTimer: null, playingFinal: false, - finalNode: null + finalNode: null, + doneJumped: false } }, onLoad(options) { @@ -298,6 +299,7 @@ export default { if (data.final.final_node && parseScript(data.final.final_node.script)) { this.finalNode = data.final.final_node this.playingFinal = true + this.doneJumped = false } else { playSound('finish') uni.redirectTo({ url: '/pages/result/result' }) @@ -313,11 +315,14 @@ export default { if (this.reply && !this.choosing) this.continuePlay() }, gotoResult() { + if (this.doneJumped) return // 防重复跳转(done 与跳过双触发) + this.doneJumped = true playSound('finish') uni.redirectTo({ url: '/pages/result/result' }) }, continuePlay() { if (this.replyTimer) { clearTimeout(this.replyTimer); this.replyTimer = null } + if (!this.reply && !this.feedback) return // 空守卫:点击与 3s 自动同帧时防 TypeError if (this.isStoryNode) { this.curNode = this.reply.next this.reply = null