|
|
@@ -5,17 +5,20 @@
|
|
|
:background-type="currentBackgroundType"
|
|
|
:image-url="currentBackgroundImage"
|
|
|
:video-url="currentBackgroundVideo"
|
|
|
- :is-playing="isPlaying"
|
|
|
:is-playback-started="isPlaybackStarted"
|
|
|
+ :is-paused="isPaused"
|
|
|
/>
|
|
|
|
|
|
<!-- 标题栏 -->
|
|
|
<DialogHeader
|
|
|
:title="currentSection?.name"
|
|
|
:back-text="backText"
|
|
|
- :is-playing="isPlaying"
|
|
|
+ :is-playing="autoAdvanceEnabled"
|
|
|
+ :is-paused="isPaused"
|
|
|
+ :is-muted="isMuted"
|
|
|
@back="goBackToMain"
|
|
|
@toggle-play="togglePlay"
|
|
|
+ @toggle-mute="toggleMute"
|
|
|
/>
|
|
|
|
|
|
<!-- 遮罩层 -->
|
|
|
@@ -29,6 +32,8 @@
|
|
|
:script-roles="scriptRoles"
|
|
|
:index="currentDialogueIndex"
|
|
|
:is-playback-started="isPlaybackStarted"
|
|
|
+ :is-muted="isMuted"
|
|
|
+ :is-paused="isPaused"
|
|
|
:previous-quest="previousQuestDialogue"
|
|
|
:poem-show="showPoem"
|
|
|
:poem-content="currentPoemContent"
|
|
|
@@ -43,8 +48,11 @@
|
|
|
<InputButtons
|
|
|
:can-prev="!isAtFirstDialogue"
|
|
|
:can-next="Boolean(currentDialogue)"
|
|
|
+ :sections="scriptData.sections"
|
|
|
+ :current-section-index="currentSectionIndex"
|
|
|
@prev="playPrevious"
|
|
|
@next="handleNextClick"
|
|
|
+ @section-change="handleSectionChange"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -70,7 +78,17 @@ import DialogCard from './dialog/DialogCard.vue'; // 对话卡片容
|
|
|
import InputButtons from './engine/InputButtons.vue'; // 底部控制按钮
|
|
|
|
|
|
// 音频播放器钩子
|
|
|
-const { playAudioChunk, stopPlayback, setOnPlaybackComplete, getIsPlaying } = useAudioPlayer();
|
|
|
+const {
|
|
|
+ playAudioChunk,
|
|
|
+ beginStream,
|
|
|
+ markStreamComplete,
|
|
|
+ stopPlayback,
|
|
|
+ pausePlayback,
|
|
|
+ resumePlayback,
|
|
|
+ setOnPlaybackComplete,
|
|
|
+ getIsPlaying,
|
|
|
+ setMuted: setStreamMuted
|
|
|
+} = useAudioPlayer();
|
|
|
|
|
|
// 路由实例
|
|
|
const router = useRouter();
|
|
|
@@ -108,9 +126,12 @@ const currentSectionIndex = ref(0); // 当前章节索引
|
|
|
const currentDialogueIndex = ref(0); // 当前对话索引
|
|
|
|
|
|
// 播放控制状态
|
|
|
-const isPlaying = ref(false); // 是否正在播放
|
|
|
+const autoAdvanceEnabled = ref(false); // 是否开启自动连播
|
|
|
+const isPaused = ref(false); // 当前媒体是否暂停
|
|
|
+const currentContentEnded = ref(false); // 当前对话媒体是否已经播放完成
|
|
|
const showMask = ref(true); // 是否显示遮罩层
|
|
|
const isPlaybackStarted = ref(false); // 播放是否已开始
|
|
|
+const isMuted = ref(localStorage.getItem('ai-course-muted') === 'true');
|
|
|
|
|
|
// 诗词显示状态
|
|
|
const showPoem = ref(false); // 是否显示诗词
|
|
|
@@ -122,6 +143,8 @@ const selectedOption = ref(''); // 选中的选项
|
|
|
// 音频对象
|
|
|
const backgroundAudio = ref(null); // 背景音频对象
|
|
|
const dialogueAudio = ref(null); // 对话音频对象
|
|
|
+let pendingAdvanceTimer = null; // 待执行的自动跳转任务
|
|
|
+let pendingRecoveryTimer = null; // 网络异常后的对话恢复任务
|
|
|
|
|
|
// 会话相关状态
|
|
|
const activeConversationId = ref(null); // 活跃会话ID
|
|
|
@@ -199,38 +222,108 @@ const getPreviousDialogue = () => {
|
|
|
return null;
|
|
|
};
|
|
|
|
|
|
+const clearPendingAdvance = () => {
|
|
|
+ if (pendingAdvanceTimer) {
|
|
|
+ clearTimeout(pendingAdvanceTimer);
|
|
|
+ pendingAdvanceTimer = null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const clearPendingRecovery = () => {
|
|
|
+ if (pendingRecoveryTimer) {
|
|
|
+ clearTimeout(pendingRecoveryTimer);
|
|
|
+ pendingRecoveryTimer = null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const scheduleAutoAdvance = (delay = 0) => {
|
|
|
+ clearPendingAdvance();
|
|
|
+ if (!autoAdvanceEnabled.value || isPaused.value || isAtLastDialogueOfCourse.value) return;
|
|
|
+
|
|
|
+ const sectionIndex = currentSectionIndex.value;
|
|
|
+ const dialogueIndex = currentDialogueIndex.value;
|
|
|
+ pendingAdvanceTimer = setTimeout(() => {
|
|
|
+ pendingAdvanceTimer = null;
|
|
|
+ const positionUnchanged = sectionIndex === currentSectionIndex.value &&
|
|
|
+ dialogueIndex === currentDialogueIndex.value;
|
|
|
+ if (!positionUnchanged || !autoAdvanceEnabled.value || isPaused.value) return;
|
|
|
+
|
|
|
+ if (!playNext()) {
|
|
|
+ autoAdvanceEnabled.value = false;
|
|
|
+ }
|
|
|
+ }, delay);
|
|
|
+};
|
|
|
+
|
|
|
+const markCurrentContentEnded = (advanceDelay = 0) => {
|
|
|
+ currentContentEnded.value = true;
|
|
|
+ if (isAtLastDialogueOfCourse.value) {
|
|
|
+ autoAdvanceEnabled.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ scheduleAutoAdvance(advanceDelay);
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
- * 停止所有音频播放
|
|
|
+ * 停止并重置所有引擎级音频,用于导航或组件销毁。
|
|
|
*/
|
|
|
const stopAllAudio = () => {
|
|
|
if (backgroundAudio.value) {
|
|
|
backgroundAudio.value.pause();
|
|
|
backgroundAudio.value.currentTime = 0;
|
|
|
+ backgroundAudio.value = null;
|
|
|
}
|
|
|
if (dialogueAudio.value) {
|
|
|
dialogueAudio.value.pause();
|
|
|
dialogueAudio.value.currentTime = 0;
|
|
|
+ dialogueAudio.value = null;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 暂停当前媒体但保留播放位置。 */
|
|
|
+const pauseCurrentMedia = () => {
|
|
|
+ backgroundAudio.value?.pause();
|
|
|
+ dialogueAudio.value?.pause();
|
|
|
+ pausePlayback().catch(e => console.error('流式语音暂停失败:', e));
|
|
|
+};
|
|
|
+
|
|
|
+/** 从当前位置继续当前媒体。 */
|
|
|
+const resumeCurrentMedia = () => {
|
|
|
+ if (backgroundAudio.value) {
|
|
|
+ backgroundAudio.value.play().catch(e => console.error('背景音继续播放失败:', e));
|
|
|
+ } else {
|
|
|
+ playBackgroundAudio();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dialogueAudio.value && !dialogueAudio.value.ended) {
|
|
|
+ dialogueAudio.value.play().catch(e => console.error('对话语音继续播放失败:', e));
|
|
|
}
|
|
|
+ resumePlayback().catch(e => console.error('流式语音继续播放失败:', e));
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 播放背景音频
|
|
|
*/
|
|
|
const playBackgroundAudio = () => {
|
|
|
- if (backgroundAudio.value) {
|
|
|
- backgroundAudio.value.pause();
|
|
|
- backgroundAudio.value.currentTime = 0;
|
|
|
+ const audioUrl = currentSection.value?.backgroundAudio?.url;
|
|
|
+ const shouldPlay = currentBackgroundType.value === 'imageAudio' &&
|
|
|
+ audioUrl && isPlaybackStarted.value && !isPaused.value;
|
|
|
+
|
|
|
+ if (!shouldPlay) {
|
|
|
+ backgroundAudio.value?.pause();
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- // 背景音频在 isPlaying 或 isPlaybackStarted 状态下播放
|
|
|
- if (currentBackgroundType.value === 'imageAudio' &&
|
|
|
- currentSection.value?.backgroundAudio?.url &&
|
|
|
- (isPlaying.value || isPlaybackStarted.value)) {
|
|
|
- backgroundAudio.value = new Audio(currentSection.value.backgroundAudio.url);
|
|
|
- backgroundAudio.value.loop = true;
|
|
|
- backgroundAudio.value.volume = 1;
|
|
|
- backgroundAudio.value.play().catch(e => console.error('背景音播放失败:', e));
|
|
|
+ if (!backgroundAudio.value || backgroundAudio.value._courseSourceUrl !== audioUrl) {
|
|
|
+ backgroundAudio.value?.pause();
|
|
|
+ const audio = new Audio(audioUrl);
|
|
|
+ audio._courseSourceUrl = audioUrl;
|
|
|
+ audio.loop = true;
|
|
|
+ audio.volume = 1;
|
|
|
+ audio.muted = isMuted.value;
|
|
|
+ backgroundAudio.value = audio;
|
|
|
}
|
|
|
+
|
|
|
+ backgroundAudio.value.play().catch(e => console.error('背景音播放失败:', e));
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -242,9 +335,10 @@ const playBackgroundVideo = () => {
|
|
|
|
|
|
/**
|
|
|
* 播放对话语音
|
|
|
- * @param {Boolean} isAutoPlay - 是否自动播放下一条
|
|
|
*/
|
|
|
-const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
+const playDialogueAudio = () => {
|
|
|
+ clearPendingAdvance();
|
|
|
+ currentContentEnded.value = false;
|
|
|
if (dialogueAudio.value) {
|
|
|
dialogueAudio.value.pause();
|
|
|
dialogueAudio.value.currentTime = 0;
|
|
|
@@ -252,51 +346,22 @@ const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
|
|
|
if (currentDialogue.value?.voiceoverUrl && currentDialogue.value?.type !== 'video') {
|
|
|
const audio = new Audio(currentDialogue.value.voiceoverUrl);
|
|
|
+ audio.muted = isMuted.value;
|
|
|
dialogueAudio.value = audio;
|
|
|
|
|
|
audio.onended = () => {
|
|
|
- if (isAtLastDialogueOfCourse.value) {
|
|
|
- isPlaying.value = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (isAutoPlay && isPlaying.value) {
|
|
|
- setTimeout(() => {
|
|
|
- if (!playNext(true)) {
|
|
|
- isPlaying.value = false;
|
|
|
- stopAllAudio();
|
|
|
- }
|
|
|
- }, 100);
|
|
|
- }
|
|
|
+ markCurrentContentEnded(100);
|
|
|
};
|
|
|
|
|
|
+ if (isPaused.value || !isPlaybackStarted.value) return;
|
|
|
audio.play().catch(e => {
|
|
|
console.error('对话语音播放失败:', e);
|
|
|
- if (isAtLastDialogueOfCourse.value) {
|
|
|
- isPlaying.value = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- if (isAutoPlay && isPlaying.value) {
|
|
|
- setTimeout(() => {
|
|
|
- if (!playNext(true)) {
|
|
|
- isPlaying.value = false;
|
|
|
- stopAllAudio();
|
|
|
- }
|
|
|
- }, 2000);
|
|
|
- }
|
|
|
+ markCurrentContentEnded(2000);
|
|
|
});
|
|
|
} else if (currentDialogue.value?.type !== 'video') {
|
|
|
- if (isAtLastDialogueOfCourse.value) {
|
|
|
- isPlaying.value = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- if (isAutoPlay && isPlaying.value && currentDialogue.value?.type !== 'user') {
|
|
|
- setTimeout(() => {
|
|
|
- if (!playNext(true)) {
|
|
|
- isPlaying.value = false;
|
|
|
- stopAllAudio();
|
|
|
- }
|
|
|
- }, 2000);
|
|
|
+ if (currentDialogue.value?.type !== 'user' &&
|
|
|
+ !isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
+ markCurrentContentEnded(2000);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
@@ -305,23 +370,36 @@ const playDialogueAudio = (isAutoPlay = false) => {
|
|
|
* 切换播放/暂停状态
|
|
|
*/
|
|
|
const togglePlay = () => {
|
|
|
- isPlaying.value = !isPlaying.value;
|
|
|
- if (isPlaying.value) {
|
|
|
- // 关闭遮罩层(如果显示的话)
|
|
|
- if (showMask.value) {
|
|
|
- showMask.value = false;
|
|
|
- isPlaybackStarted.value = true;
|
|
|
- }
|
|
|
- playBackgroundAudio();
|
|
|
- if (!getIsPlaying() && !conversationInProgress.value) {
|
|
|
- if (currentDialogue.value?.type === 'video') {
|
|
|
- // 视频类型由VideoDisplay组件处理
|
|
|
- } else {
|
|
|
- playSequence();
|
|
|
- }
|
|
|
- }
|
|
|
+ if (autoAdvanceEnabled.value) {
|
|
|
+ autoAdvanceEnabled.value = false;
|
|
|
+ isPaused.value = true;
|
|
|
+ clearPendingAdvance();
|
|
|
+ pauseCurrentMedia();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ autoAdvanceEnabled.value = true;
|
|
|
+ const wasPaused = isPaused.value;
|
|
|
+ isPaused.value = false;
|
|
|
+
|
|
|
+ if (showMask.value) {
|
|
|
+ startPlayback();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (wasPaused) {
|
|
|
+ resumeCurrentMedia();
|
|
|
} else {
|
|
|
- stopAllAudio();
|
|
|
+ playBackgroundAudio();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentContentEnded.value) {
|
|
|
+ scheduleAutoAdvance(100);
|
|
|
+ } else if (!dialogueAudio.value && !getIsPlaying() && !conversationInProgress.value &&
|
|
|
+ currentDialogue.value?.type !== 'video' &&
|
|
|
+ currentDialogue.value?.type !== 'user' &&
|
|
|
+ !isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
+ playSequence();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -329,21 +407,14 @@ const togglePlay = () => {
|
|
|
* 处理视频播放完成
|
|
|
*/
|
|
|
const handleVideoEnded = () => {
|
|
|
- if (isPlaying.value) {
|
|
|
- setTimeout(() => {
|
|
|
- if (!playNext(true)) {
|
|
|
- isPlaying.value = false;
|
|
|
- stopAllAudio();
|
|
|
- }
|
|
|
- }, 1500);
|
|
|
- }
|
|
|
+ markCurrentContentEnded(1500);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 播放对话序列
|
|
|
*/
|
|
|
const playSequence = () => {
|
|
|
- if (!isPlaying.value) return;
|
|
|
+ if (!autoAdvanceEnabled.value || isPaused.value) return;
|
|
|
|
|
|
if (currentDialogue.value?.type === 'user') return;
|
|
|
|
|
|
@@ -354,19 +425,14 @@ const playSequence = () => {
|
|
|
currentPoemContent.value = currentDialogue.value.content;
|
|
|
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
- playDialogueAudio(true);
|
|
|
+ playDialogueAudio();
|
|
|
} else {
|
|
|
- setTimeout(() => {
|
|
|
- if (!playNext(true)) {
|
|
|
- isPlaying.value = false;
|
|
|
- stopAllAudio();
|
|
|
- }
|
|
|
- }, 500);
|
|
|
+ markCurrentContentEnded(500);
|
|
|
}
|
|
|
} else if (isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
showPoem.value = false;
|
|
|
} else {
|
|
|
- playDialogueAudio(true);
|
|
|
+ playDialogueAudio();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -376,6 +442,8 @@ const playSequence = () => {
|
|
|
const playPrevious = () => {
|
|
|
if (isAtFirstDialogue.value) return;
|
|
|
|
|
|
+ clearPendingAdvance();
|
|
|
+ clearPendingRecovery();
|
|
|
stopAllAudio();
|
|
|
recoverQuestDialogue();
|
|
|
stopPlayback(false);
|
|
|
@@ -416,17 +484,64 @@ const playPrevious = () => {
|
|
|
// 诗词类型播放语音
|
|
|
if (currentDialogue.value?.type === 'poem') {
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
- playDialogueAudio(isPlaying.value);
|
|
|
+ playDialogueAudio();
|
|
|
} else {
|
|
|
- // 诗词没有语音,延迟后继续切换上一句
|
|
|
- if (!isAtFirstDialogue.value) {
|
|
|
- setTimeout(() => {
|
|
|
- playPrevious();
|
|
|
- }, 100);
|
|
|
- }
|
|
|
+ currentContentEnded.value = true;
|
|
|
+ scheduleAutoAdvance(500);
|
|
|
+ }
|
|
|
+ } else if (!isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
+ playDialogueAudio();
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 切换课程总静音状态。只改变输出音量,不改变任何播放进度。
|
|
|
+ */
|
|
|
+const toggleMute = () => {
|
|
|
+ isMuted.value = !isMuted.value;
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从指定环节的第一条对话开始
|
|
|
+ * @param {Number} sectionIndex - 目标环节索引
|
|
|
+ */
|
|
|
+const handleSectionChange = (sectionIndex) => {
|
|
|
+ const targetSection = props.scriptData.sections?.[sectionIndex];
|
|
|
+ if (!targetSection?.dialogues?.length) return;
|
|
|
+ if (sectionIndex === currentSectionIndex.value && currentDialogueIndex.value === 0) return;
|
|
|
+
|
|
|
+ clearPendingAdvance();
|
|
|
+ clearPendingRecovery();
|
|
|
+ stopAllAudio();
|
|
|
+ recoverQuestDialogue();
|
|
|
+ stopPlayback(false);
|
|
|
+ if (conversationInProgress.value) {
|
|
|
+ stopStream();
|
|
|
+ }
|
|
|
+
|
|
|
+ currentSectionIndex.value = sectionIndex;
|
|
|
+ currentDialogueIndex.value = 0;
|
|
|
+ currentContentEnded.value = false;
|
|
|
+ showPoem.value = false;
|
|
|
+ currentPoemContent.value = '';
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ // 初始遮罩未点击时只切换展示位置,不提前播放内容。
|
|
|
+ if (!isPlaybackStarted.value) return;
|
|
|
+
|
|
|
+ if (currentDialogue.value?.type === 'poem') {
|
|
|
+ showPoem.value = true;
|
|
|
+ currentPoemContent.value = currentDialogue.value.content;
|
|
|
+ if (currentDialogue.value?.voiceoverUrl) {
|
|
|
+ playDialogueAudio();
|
|
|
+ } else {
|
|
|
+ markCurrentContentEnded(500);
|
|
|
}
|
|
|
+ } else if (isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
+ showPoem.value = false;
|
|
|
} else {
|
|
|
- playDialogueAudio(isPlaying.value);
|
|
|
+ playDialogueAudio();
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
@@ -442,17 +557,19 @@ const handleNextClick = () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- playNext(false);
|
|
|
+ playNext();
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 播放下一条对话
|
|
|
- * @param {Boolean} isAutoPlay - 是否自动播放
|
|
|
* @returns {Boolean} 是否成功切换
|
|
|
*/
|
|
|
-const playNext = (isAutoPlay = false) => {
|
|
|
+const playNext = () => {
|
|
|
if (isAtLastDialogueOfCourse.value) return false;
|
|
|
|
|
|
+ clearPendingAdvance();
|
|
|
+ clearPendingRecovery();
|
|
|
+ currentContentEnded.value = false;
|
|
|
if (dialogueAudio.value) {
|
|
|
dialogueAudio.value.pause();
|
|
|
dialogueAudio.value.currentTime = 0;
|
|
|
@@ -474,12 +591,9 @@ const playNext = (isAutoPlay = false) => {
|
|
|
currentPoemContent.value = currentDialogue.value.content;
|
|
|
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
- playDialogueAudio(isPlaying.value);
|
|
|
+ playDialogueAudio();
|
|
|
} else {
|
|
|
- // 诗词没有语音,延迟后继续切换下一句
|
|
|
- setTimeout(() => {
|
|
|
- playNext(isAutoPlay);
|
|
|
- }, 100);
|
|
|
+ markCurrentContentEnded(500);
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
@@ -490,7 +604,7 @@ const playNext = (isAutoPlay = false) => {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- playDialogueAudio(isPlaying.value);
|
|
|
+ playDialogueAudio();
|
|
|
return true;
|
|
|
} else if (currentSectionIndex.value < props.scriptData.sections.length - 1) {
|
|
|
currentSectionIndex.value++;
|
|
|
@@ -505,16 +619,14 @@ const playNext = (isAutoPlay = false) => {
|
|
|
currentPoemContent.value = currentDialogue.value.content;
|
|
|
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
- playDialogueAudio(isPlaying.value);
|
|
|
+ playDialogueAudio();
|
|
|
} else {
|
|
|
- setTimeout(() => {
|
|
|
- playNext(isAutoPlay);
|
|
|
- }, 100);
|
|
|
+ markCurrentContentEnded(500);
|
|
|
}
|
|
|
} else if (isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
showPoem.value = false;
|
|
|
} else {
|
|
|
- playDialogueAudio(isPlaying.value);
|
|
|
+ playDialogueAudio();
|
|
|
}
|
|
|
});
|
|
|
return true;
|
|
|
@@ -526,7 +638,10 @@ const playNext = (isAutoPlay = false) => {
|
|
|
* 返回主页面 - 触发父组件的 goBack 方法
|
|
|
*/
|
|
|
const goBackToMain = () => {
|
|
|
+ clearPendingAdvance();
|
|
|
+ clearPendingRecovery();
|
|
|
stopAllAudio();
|
|
|
+ stopPlayback(false);
|
|
|
emit('goBack');
|
|
|
};
|
|
|
|
|
|
@@ -535,6 +650,8 @@ const goBackToMain = () => {
|
|
|
*/
|
|
|
const startPlayback = () => {
|
|
|
isPlaybackStarted.value = true;
|
|
|
+ isPaused.value = false;
|
|
|
+ currentContentEnded.value = false;
|
|
|
showMask.value = false;
|
|
|
|
|
|
// 启动背景音频
|
|
|
@@ -547,9 +664,7 @@ const startPlayback = () => {
|
|
|
if (currentDialogue.value?.voiceoverUrl) {
|
|
|
playDialogueAudio();
|
|
|
} else {
|
|
|
- setTimeout(() => {
|
|
|
- playNext();
|
|
|
- }, 500);
|
|
|
+ markCurrentContentEnded(500);
|
|
|
}
|
|
|
} else if (isEvaluationDialogue(currentDialogue.value?.type)) {
|
|
|
showPoem.value = false;
|
|
|
@@ -724,8 +839,14 @@ const showQuestAnswerDialogue = (roleName) => {
|
|
|
*/
|
|
|
const delayRecoverQuestDialogue = () => {
|
|
|
currentDialogue.value.content = "当前网络无反应,请稍后重试!";
|
|
|
- setTimeout(() => {
|
|
|
- recoverQuestDialogue();
|
|
|
+ clearPendingRecovery();
|
|
|
+ const sectionIndex = currentSectionIndex.value;
|
|
|
+ const dialogueIndex = currentDialogueIndex.value;
|
|
|
+ pendingRecoveryTimer = setTimeout(() => {
|
|
|
+ pendingRecoveryTimer = null;
|
|
|
+ if (sectionIndex === currentSectionIndex.value && dialogueIndex === currentDialogueIndex.value) {
|
|
|
+ recoverQuestDialogue();
|
|
|
+ }
|
|
|
}, 1500);
|
|
|
};
|
|
|
|
|
|
@@ -771,6 +892,7 @@ const handlePoemReadingComplete = (result) => {
|
|
|
currentDialogue.value.content = result.content;
|
|
|
currentDialogue.value.voiceoverUrl = result.audioUrl;
|
|
|
}
|
|
|
+ markCurrentContentEnded(300);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -778,6 +900,8 @@ const handlePoemReadingComplete = (result) => {
|
|
|
* @param {Object} userMessage - 用户消息
|
|
|
*/
|
|
|
const doSendMessageStream = async (userMessage) => {
|
|
|
+ beginStream();
|
|
|
+ currentContentEnded.value = false;
|
|
|
conversationInAbortController.value = new AbortController();
|
|
|
conversationInProgress.value = true;
|
|
|
receiveMessageFullText.value = '';
|
|
|
@@ -797,6 +921,7 @@ const doSendMessageStream = async (userMessage) => {
|
|
|
if (code !== 0) {
|
|
|
console.log(`对话异常! ${msg}`);
|
|
|
stopStream();
|
|
|
+ stopPlayback(false);
|
|
|
delayRecoverQuestDialogue();
|
|
|
return;
|
|
|
}
|
|
|
@@ -816,20 +941,20 @@ const doSendMessageStream = async (userMessage) => {
|
|
|
(error) => {
|
|
|
console.log(`对话异常! ${error}`);
|
|
|
stopStream();
|
|
|
+ stopPlayback(false);
|
|
|
delayRecoverQuestDialogue();
|
|
|
throw error;
|
|
|
},
|
|
|
() => {
|
|
|
console.log(`结束对话!`);
|
|
|
+ markStreamComplete();
|
|
|
stopStream();
|
|
|
- if (isAtLastDialogueOfCourse.value && currentDialogue.value?.type === 'digital') {
|
|
|
- isPlaying.value = false;
|
|
|
- }
|
|
|
}
|
|
|
);
|
|
|
} catch (error) {
|
|
|
console.error('发送消息失败:', error);
|
|
|
stopStream();
|
|
|
+ stopPlayback(false);
|
|
|
delayRecoverQuestDialogue();
|
|
|
}
|
|
|
};
|
|
|
@@ -850,23 +975,7 @@ const stopStream = async () => {
|
|
|
*/
|
|
|
const handleAudioPlaybackComplete = () => {
|
|
|
console.log('智能问答音频播放完成');
|
|
|
- setOnPlaybackComplete(null);
|
|
|
- stopAllAudio();
|
|
|
-
|
|
|
- if (isAtLastDialogueOfCourse.value) {
|
|
|
- isPlaying.value = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (isPlaying.value) {
|
|
|
- setOnPlaybackComplete(handleAudioPlaybackComplete);
|
|
|
- if (playNext(true)) {
|
|
|
- // playNext 内部已调用 playDialogueAudio
|
|
|
- } else {
|
|
|
- isPlaying.value = false;
|
|
|
- stopAllAudio();
|
|
|
- }
|
|
|
- }
|
|
|
+ markCurrentContentEnded(100);
|
|
|
};
|
|
|
|
|
|
/** 生命周期钩子与监听 **/
|
|
|
@@ -878,11 +987,24 @@ watch(currentSectionIndex, () => {
|
|
|
playBackgroundAudio();
|
|
|
});
|
|
|
|
|
|
+watch(isMuted, (muted) => {
|
|
|
+ if (backgroundAudio.value) {
|
|
|
+ backgroundAudio.value.muted = muted;
|
|
|
+ }
|
|
|
+ if (dialogueAudio.value) {
|
|
|
+ dialogueAudio.value.muted = muted;
|
|
|
+ }
|
|
|
+ setStreamMuted(muted);
|
|
|
+ localStorage.setItem('ai-course-muted', String(muted));
|
|
|
+}, { immediate: true });
|
|
|
+
|
|
|
/**
|
|
|
* 监听剧本数据变化 - 重置所有状态
|
|
|
*/
|
|
|
watch(() => props.scriptData, (newVal, oldVal) => {
|
|
|
if (newVal && oldVal && newVal !== oldVal) {
|
|
|
+ clearPendingAdvance();
|
|
|
+ clearPendingRecovery();
|
|
|
stopAllAudio();
|
|
|
recoverQuestDialogue();
|
|
|
stopPlayback(false);
|
|
|
@@ -891,7 +1013,10 @@ watch(() => props.scriptData, (newVal, oldVal) => {
|
|
|
}
|
|
|
currentSectionIndex.value = 0;
|
|
|
currentDialogueIndex.value = 0;
|
|
|
- isPlaying.value = false;
|
|
|
+ autoAdvanceEnabled.value = false;
|
|
|
+ isPaused.value = false;
|
|
|
+ resumePlayback().catch(() => {});
|
|
|
+ currentContentEnded.value = false;
|
|
|
showMask.value = true;
|
|
|
isPlaybackStarted.value = false;
|
|
|
currentDialogueCache.value = null;
|
|
|
@@ -914,6 +1039,8 @@ onMounted(() => {
|
|
|
*/
|
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener('keydown', handleKeydown);
|
|
|
+ clearPendingAdvance();
|
|
|
+ clearPendingRecovery();
|
|
|
stopAllAudio();
|
|
|
stopPlayback(false);
|
|
|
});
|