diff --git a/spx-gui/src/apps/xbuilder/pages/editor/index.vue b/spx-gui/src/apps/xbuilder/pages/editor/index.vue index e683866cb..0d3d1d7c2 100644 --- a/spx-gui/src/apps/xbuilder/pages/editor/index.vue +++ b/spx-gui/src/apps/xbuilder/pages/editor/index.vue @@ -221,8 +221,8 @@ async function checkChangesNotToBeSaved(es: EditorState) { zh: '离开编辑器' }), content: t({ - en: `Project edits will not be saved if you leave now. Are you sure to leave?`, - zh: `若现在离开,对项目的修改将不会被保存。确定要离开吗?` + en: `Project edits will not be saved. Are you sure to leave?`, + zh: `对项目的修改将不会被保存,确定要离开吗?` }), cancelText: t({ en: 'Keep editing', diff --git a/spx-gui/src/components/editor/editing.ts b/spx-gui/src/components/editor/editing.ts index de4132a4d..1c755974f 100644 --- a/spx-gui/src/components/editor/editing.ts +++ b/spx-gui/src/components/editor/editing.ts @@ -151,6 +151,17 @@ export class Editing extends Disposable { return this.dirtyRef.value } + /** + * Mark the current unsaved changes as no longer pending, clearing the dirty state. + * + * This only resets dirty tracking; it does not revert the project content. It is used when + * the user intentionally leaves without keeping the edits (e.g. finishing a tutorial course + * in effect-free mode), so leaving the editor does not prompt the "leave editor" confirmation. + */ + resetChanges() { + this.dirtyRef.value = false + } + private startDirtyMonitoring() { this.addDisposer( watch( diff --git a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue index 4894b1177..c1c89565c 100644 --- a/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue +++ b/spx-gui/src/components/tutorials/TutorialCourseSuccessModal.vue @@ -8,6 +8,7 @@ import type { CourseSeries } from '@/apis/course-series' import { useI18n } from '@/utils/i18n' import { UIButton, UIImg, UIModal, UIModalClose } from '@/components/ui' +import { useEditorCtxRef } from '@/components/editor/EditorContextProvider.vue' import { DefaultException, useMessageHandle } from '@/utils/exception' import successImg from './success.png' @@ -25,6 +26,13 @@ const emit = defineEmits<{ const i18n = useI18n() const router = useRouter() +const editorCtxRef = useEditorCtxRef() + +// Leaving via the actions below intentionally discards the current (effect-free) tutorial +// edits, so reset the dirty state to skip the editor's "leave editor" confirmation. +function skipConfirmForLeavingEditor() { + editorCtxRef.value?.state.editing.resetChanges() +} const courseCompleteMessage = computed(() => { return i18n.t({ @@ -37,10 +45,17 @@ function handleCancel() { emit('cancelled') } -function handleBrowseTutorials() { - emit('cancelled') - router.push('/tutorials') -} +const { fn: handleBackToCourseSeries } = useMessageHandle( + async () => { + skipConfirmForLeavingEditor() + emit('cancelled') + await router.push(`/course-series/${props.series.id}`) + }, + { + en: 'Failed to go back to course series', + zh: '返回系列课程失败' + } +) const hasNextCourse = computed(() => { const currentCourse = props.course @@ -64,6 +79,7 @@ const { fn: handleStartNextCourse } = useMessageHandle( const tutorial = props.tutorial const nextCourse = await getCourse(currentSeries.courseIDs[findIndex + 1]) emit('cancelled') + skipConfirmForLeavingEditor() await tutorial.startCourse(nextCourse, currentSeries) }, { @@ -96,8 +112,8 @@ const { fn: handleStartNextCourse } = useMessageHandle(