From 3b44d0174b90764586676daa29102abab247652f Mon Sep 17 00:00:00 2001 From: chenyuan Date: Wed, 5 Aug 2026 10:01:23 +0800 Subject: [PATCH] fix: make student demo handshake reliable --- src/views/studentDemo/index.vue | 14 +++++++++-- src/views/teacherEnd/trainingTask/index.vue | 28 +++++++++++++++------ tests/student-demo-mode.static.test.cjs | 5 ++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/views/studentDemo/index.vue b/src/views/studentDemo/index.vue index e15575f..3f4da12 100644 --- a/src/views/studentDemo/index.vue +++ b/src/views/studentDemo/index.vue @@ -7,11 +7,17 @@ import { exchangeStudentDemoSession } from "@/api/studentDemo"; import { saveStudentDemoSession } from "@/utils/studentDemo"; const message = ref("正在连接教师演示会话…"); +let readyTimer; + +function notifyReady() { + if (window.opener) window.opener.postMessage({ type: "STUDENT_DEMO_READY" }, window.location.origin); +} function receiveTicket(event) { if (event.origin !== window.location.origin || event.source !== window.opener || event.data?.type !== "STUDENT_DEMO_TICKET") return; exchangeStudentDemoSession(event.data.ticket).then((res) => { saveStudentDemoSession(res.data); + window.clearInterval(readyTimer); window.removeEventListener("message", receiveTicket); window.location.replace("/index"); }).catch(() => { message.value = "演示链接已失效,请返回教师端重新打开。"; }); @@ -20,9 +26,13 @@ function receiveTicket(event) { onMounted(() => { if (!window.opener) { message.value = "演示链接已失效,请返回教师端重新打开。"; return; } window.addEventListener("message", receiveTicket); - window.opener.postMessage({ type: "STUDENT_DEMO_READY" }, window.location.origin); + notifyReady(); + readyTimer = window.setInterval(notifyReady, 500); +}); +onBeforeUnmount(() => { + window.clearInterval(readyTimer); + window.removeEventListener("message", receiveTicket); }); -onBeforeUnmount(() => window.removeEventListener("message", receiveTicket)); diff --git a/src/views/teacherEnd/trainingTask/index.vue b/src/views/teacherEnd/trainingTask/index.vue index 1573ae3..842f024 100644 --- a/src/views/teacherEnd/trainingTask/index.vue +++ b/src/views/teacherEnd/trainingTask/index.vue @@ -573,17 +573,29 @@ async function openStudentDemo() { if (!selectedTeachingClass.value) return; const child = window.open("/student-demo", "_blank"); if (!child) { proxy?.$modal?.msgError?.("浏览器拦截了演示窗口,请允许弹窗后重试"); return; } + let ticket = ""; + let childReady = false; + const cleanup = () => window.removeEventListener("message", listener); + const sendTicket = () => { + if (!childReady || !ticket) return; + child.postMessage({ type: "STUDENT_DEMO_TICKET", ticket }, window.location.origin); + cleanup(); + }; + const listener = (event) => { + if (event.origin !== window.location.origin || event.source !== child || event.data?.type !== "STUDENT_DEMO_READY") return; + childReady = true; + sendTicket(); + }; + window.addEventListener("message", listener); + const timeout = window.setTimeout(cleanup, 60 * 1000); try { const res = await createStudentDemoSession(selectedTeachingClassId.value); - const ticket = res?.data?.ticket; - const listener = (event) => { - if (event.origin !== window.location.origin || event.source !== child || event.data?.type !== "STUDENT_DEMO_READY") return; - child.postMessage({ type: "STUDENT_DEMO_TICKET", ticket }, window.location.origin); - window.removeEventListener("message", listener); - }; - window.addEventListener("message", listener); - window.setTimeout(() => window.removeEventListener("message", listener), 60 * 1000); + ticket = res?.data?.ticket || ""; + if (!ticket) throw new Error("missing demo ticket"); + sendTicket(); } catch (error) { + window.clearTimeout(timeout); + cleanup(); child.close(); proxy?.$modal?.msgError?.("无法创建学生端讲解会话"); } diff --git a/tests/student-demo-mode.static.test.cjs b/tests/student-demo-mode.static.test.cjs index 69b9ef7..91c86e3 100644 --- a/tests/student-demo-mode.static.test.cjs +++ b/tests/student-demo-mode.static.test.cjs @@ -19,7 +19,12 @@ assert(api.includes("/api/student-demo-sessions/exchange"), "ticket exchange API assert(teacherPage.includes("学生端讲解"), "teacher entry missing"); assert(teacherPage.includes("window.open("), "teacher entry must open a new window"); assert(teacherPage.includes("STUDENT_DEMO_READY"), "teacher must wait for child readiness"); +assert( + teacherPage.indexOf('window.addEventListener("message", listener)') < teacherPage.indexOf("createStudentDemoSession("), + "teacher must listen for child readiness before requesting the demo ticket" +); assert(demoPage.includes("STUDENT_DEMO_TICKET"), "demo page must receive ticket via postMessage"); +assert(demoPage.includes("window.setInterval"), "demo page must retry its readiness notification until it receives the ticket"); assert(demoPage.includes("window.location.origin"), "postMessage must validate origin"); assert(!teacherPage.includes("ticket="), "ticket must not be sent in URL"); assert(navbar.includes("isStudentDemo()"), "demo mode must not request a profile for its temporary user");