添加学号追踪

This commit is contained in:
feie9456 2026-01-05 12:02:12 +08:00
parent c6d10b6221
commit d14b2e5ca8
2 changed files with 158 additions and 2 deletions

View File

@ -31,12 +31,61 @@ const trackerSessionId = (() => {
} }
})(); })();
// =========================
//
// =========================
const STUDENT_ID_KEY = 'sound-speed:student-id';
const studentId = ref<string | null>(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 postTracker = (event: string, data?: unknown) => {
const body = JSON.stringify({ const body = JSON.stringify({
event, event,
data, data,
ts: Date.now(), ts: Date.now(),
sessionId: trackerSessionId, sessionId: trackerSessionId,
studentId: studentId.value ?? undefined,
step: currentStepTitle.value, step: currentStepTitle.value,
}); });
@ -229,6 +278,7 @@ const guideImgRef = ref<HTMLImageElement | null>(null);
const closeGuide = () => { const closeGuide = () => {
showGuide.value = false; showGuide.value = false;
showStudentIdModal.value = true;
}; };
const onGuideClick = (e: MouseEvent) => { const onGuideClick = (e: MouseEvent) => {
@ -741,6 +791,7 @@ const updateCablePaths = (onlyPins?: Set<PinId>) => {
}; };
onMounted(() => { onMounted(() => {
loadStudentId();
scheduleUpdateCablePaths(); scheduleUpdateCablePaths();
window.addEventListener('resize', scheduleUpdateCablePaths); window.addEventListener('resize', scheduleUpdateCablePaths);
}); });
@ -764,6 +815,21 @@ onBeforeUnmount(() => {
</div> </div>
</Transition> </Transition>
<Transition name="guide">
<div v-if="showStudentIdModal" class="studentid-overlay" @pointerdown.stop>
<div class="studentid-modal" role="dialog" aria-modal="true" aria-label="学号确认">
<div class="studentid-title">请输入学号</div>
<input v-model="studentIdInput" class="studentid-input" type="text" inputmode="numeric"
autocomplete="off" placeholder="学号" @keydown.enter.prevent="continueWithStudentId" />
<div class="studentid-actions">
<button class="studentid-btn" type="button" @click="chooseGuestMode">游客模式</button>
<button class="studentid-btn primary" type="button" :disabled="!canContinueWithStudentId"
@click="continueWithStudentId">继续</button>
</div>
</div>
</div>
</Transition>
<div v-if="tooltip.visible" class="tooltip" :class="`tooltip--${tooltip.side}`" :style="tooltipStyle"> <div v-if="tooltip.visible" class="tooltip" :class="`tooltip--${tooltip.side}`" :style="tooltipStyle">
<div class="tooltip-inner">{{ tooltip.text }}</div> <div class="tooltip-inner">{{ tooltip.text }}</div>
</div> </div>
@ -798,7 +864,8 @@ onBeforeUnmount(() => {
<DraggableInstrumentWrapper :style="notebookStyle" :instrument="notebook" @start-drag="startDrag"> <DraggableInstrumentWrapper :style="notebookStyle" :instrument="notebook" @start-drag="startDrag">
<Notebook class="machine" :wiring="wiringConnected" <Notebook class="machine" :wiring="wiringConnected"
:osc="{ vdch1: oscStatus.vdch1, vdch2: oscStatus.vdch2, currentModeIndex: oscStatus.currentModeIndex, xyMode: oscStatus.xyMode, power: oscStatus.power }" :osc="{ vdch1: oscStatus.vdch1, vdch2: oscStatus.vdch2, currentModeIndex: oscStatus.currentModeIndex, xyMode: oscStatus.xyMode, power: oscStatus.power }"
:signal-frequency-k-hz="signalFrequencyKHz" :lissajous-line-streak="lissajousLineStreak" /> :signal-frequency-k-hz="signalFrequencyKHz" :lissajous-line-streak="lissajousLineStreak"
:student-id="studentId ?? undefined" :tracker-session-id="trackerSessionId" />
</DraggableInstrumentWrapper> </DraggableInstrumentWrapper>
<DraggableInstrumentWrapper :style="oscilloscopeStyle" :instrument="oscilloscope" @start-drag="startDrag"> <DraggableInstrumentWrapper :style="oscilloscopeStyle" :instrument="oscilloscope" @start-drag="startDrag">
@ -838,6 +905,84 @@ onBeforeUnmount(() => {
justify-content: center; 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 { .guide-img {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;

View File

@ -21,6 +21,8 @@ const props = defineProps<{
osc: OscStatus; osc: OscStatus;
signalFrequencyKHz: number; signalFrequencyKHz: number;
lissajousLineStreak: number; lissajousLineStreak: number;
studentId?: string;
trackerSessionId?: string;
}>(); }>();
const inRange = (v: number, a: number, b: number) => v >= a && v <= b; 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 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 { try {
if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) { if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
const blob = new Blob([body], { type: 'application/json' }); const blob = new Blob([body], { type: 'application/json' });
@ -183,6 +191,7 @@ const submitRecord = async () => {
const payload = { const payload = {
ts: Date.now(), ts: Date.now(),
studentId: props.studentId,
raw: { raw: {
naturalFreqKHz: rawData.value.naturalFreqKHz, naturalFreqKHz: rawData.value.naturalFreqKHz,
rows: rawData.value.rows, rows: rawData.value.rows,
@ -194,6 +203,8 @@ const submitRecord = async () => {
validDistancesMm: parsedDistances.value, validDistancesMm: parsedDistances.value,
}, },
context: { context: {
trackerSessionId: props.trackerSessionId,
studentId: props.studentId,
wiring: props.wiring, wiring: props.wiring,
osc: props.osc, osc: props.osc,
signalFrequencyKHz: props.signalFrequencyKHz, signalFrequencyKHz: props.signalFrequencyKHz,