From d14b2e5ca8c673082bf44ab86425f4ab33c08706 Mon Sep 17 00:00:00 2001 From: feie9456 Date: Mon, 5 Jan 2026 12:02:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AD=A6=E5=8F=B7=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 147 +++++++++++++++++++++++++++++++++++- src/components/Notebook.vue | 13 +++- 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/src/App.vue b/src/App.vue index c3af7df..6ade17f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -31,12 +31,61 @@ const trackerSessionId = (() => { } })(); +// ========================= +// 学号(入门引导确认后弹窗) +// ========================= + +const STUDENT_ID_KEY = 'sound-speed:student-id'; + +const studentId = ref(null); +const showStudentIdModal = ref(false); +const studentIdInput = ref(''); + +const loadStudentId = () => { + try { + const saved = window.localStorage.getItem(STUDENT_ID_KEY); + if (typeof saved === 'string' && saved.trim()) { + studentId.value = saved.trim(); + studentIdInput.value = saved.trim(); + } + } catch { + // ignore + } +}; + +const persistStudentId = (v: string | null) => { + try { + if (!v) window.localStorage.removeItem(STUDENT_ID_KEY); + else window.localStorage.setItem(STUDENT_ID_KEY, v); + } catch { + // ignore + } +}; + +const normalizedStudentIdInput = computed(() => studentIdInput.value.trim()); +const canContinueWithStudentId = computed(() => normalizedStudentIdInput.value.length > 0); + +const chooseGuestMode = () => { + studentId.value = null; + studentIdInput.value = ''; + persistStudentId(null); + showStudentIdModal.value = false; +}; + +const continueWithStudentId = () => { + if (!canContinueWithStudentId.value) return; + studentId.value = normalizedStudentIdInput.value; + persistStudentId(studentId.value); + showStudentIdModal.value = false; +}; + const postTracker = (event: string, data?: unknown) => { const body = JSON.stringify({ event, data, ts: Date.now(), sessionId: trackerSessionId, + studentId: studentId.value ?? undefined, step: currentStepTitle.value, }); @@ -229,6 +278,7 @@ const guideImgRef = ref(null); const closeGuide = () => { showGuide.value = false; + showStudentIdModal.value = true; }; const onGuideClick = (e: MouseEvent) => { @@ -741,6 +791,7 @@ const updateCablePaths = (onlyPins?: Set) => { }; onMounted(() => { + loadStudentId(); scheduleUpdateCablePaths(); window.addEventListener('resize', scheduleUpdateCablePaths); }); @@ -764,6 +815,21 @@ onBeforeUnmount(() => { + +
+ +
+
+
{{ tooltip.text }}
@@ -798,7 +864,8 @@ onBeforeUnmount(() => { + :signal-frequency-k-hz="signalFrequencyKHz" :lissajous-line-streak="lissajousLineStreak" + :student-id="studentId ?? undefined" :tracker-session-id="trackerSessionId" /> @@ -838,6 +905,84 @@ onBeforeUnmount(() => { justify-content: center; } +.studentid-overlay { + position: fixed; + inset: 0; + z-index: 9998; + background: rgba(0, 0, 0, 0.70); + display: flex; + align-items: center; + justify-content: center; +} + +.studentid-modal { + width: 420px; + max-width: calc(100vw - 40px); + background: + linear-gradient(rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.96)), + repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.035) 0px, + rgba(0, 0, 0, 0.035) 1px, + rgba(0, 0, 0, 0.0) 10px, + rgba(0, 0, 0, 0.0) 18px + ); + border: 2px solid rgba(20, 20, 20, 0.70); + border-radius: 14px; + box-shadow: 0 16px 28px rgba(0, 0, 0, 0.35); + padding: 14px 14px 12px; + color: rgba(20, 20, 20, 0.95); +} + +.studentid-title { + font-size: 14px; + font-weight: 700; + margin-bottom: 10px; +} + +.studentid-input { + width: 100%; + box-sizing: border-box; + border-radius: 10px; + border: 2px solid rgba(20, 20, 20, 0.45); + background: rgba(255, 255, 255, 0.78); + padding: 10px 10px; + font-size: 14px; + outline: none; +} + +.studentid-input:focus { + border-color: rgba(20, 20, 20, 0.70); + box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.12); +} + +.studentid-actions { + margin-top: 12px; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.studentid-btn { + appearance: none; + border: 2px solid rgba(20, 20, 20, 0.55); + background: rgba(255, 255, 255, 0.78); + border-radius: 999px; + padding: 8px 14px; + font-size: 13px; + cursor: pointer; +} + +.studentid-btn.primary { + background: rgba(20, 20, 20, 0.90); + color: rgba(255, 255, 255, 0.95); +} + +.studentid-btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + .guide-img { width: 100vw; height: 100vh; diff --git a/src/components/Notebook.vue b/src/components/Notebook.vue index 3fcd571..fe0a468 100644 --- a/src/components/Notebook.vue +++ b/src/components/Notebook.vue @@ -21,6 +21,8 @@ const props = defineProps<{ osc: OscStatus; signalFrequencyKHz: number; lissajousLineStreak: number; + studentId?: string; + trackerSessionId?: string; }>(); const inRange = (v: number, a: number, b: number) => v >= a && v <= b; @@ -144,7 +146,13 @@ const postJson = async (url: string, data: unknown) => { }; const postTracker = (event: string, data?: unknown) => { - const body = JSON.stringify({ event, data, ts: Date.now() }); + const body = JSON.stringify({ + event, + data, + ts: Date.now(), + sessionId: props.trackerSessionId, + studentId: props.studentId, + }); try { if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) { const blob = new Blob([body], { type: 'application/json' }); @@ -183,6 +191,7 @@ const submitRecord = async () => { const payload = { ts: Date.now(), + studentId: props.studentId, raw: { naturalFreqKHz: rawData.value.naturalFreqKHz, rows: rawData.value.rows, @@ -194,6 +203,8 @@ const submitRecord = async () => { validDistancesMm: parsedDistances.value, }, context: { + trackerSessionId: props.trackerSessionId, + studentId: props.studentId, wiring: props.wiring, osc: props.osc, signalFrequencyKHz: props.signalFrequencyKHz,